From 777f6b9b07be9039f7971a70eb6a21ea773a7247 Mon Sep 17 00:00:00 2001 From: Garbage Mule Date: Fri, 24 Feb 2012 05:11:06 +0100 Subject: [PATCH] So, you got a big source code revamp. Big whoop, wanna fight about it? --- .gitignore | 18 + MobArena.jar | Bin 219934 -> 310838 bytes README => README.md | 2 - build.xml | 88 + language files/cz/announcements.properties | 58 + language files/da/announcements.properties | 51 + language files/de/announcements.properties | 53 + language files/fr/announcements.properties | 51 + language files/nl/announcements.properties | 52 + .../pirate/announcements.properties | 51 + resources/plugin.yml | 67 +- resources/res/classes.yml | 15 + resources/res/config.yml | 117 +- resources/res/global-settings.yml | 3 + resources/res/magicspells.yml | 7 +- resources/res/rewards.yml | 9 + resources/res/settings.yml | 33 + resources/res/waves.yml | 32 + src/com/garbagemule/MobArena/Arena.java | 302 ---- src/com/garbagemule/MobArena/ArenaClass.java | 283 ++++ src/com/garbagemule/MobArena/ArenaImpl.java | 1270 +++++++++++++++ .../MobArena/ArenaListenerImpl.java | 789 +++++++++ src/com/garbagemule/MobArena/ArenaLog.java | 141 -- src/com/garbagemule/MobArena/ArenaMaster.java | 147 -- .../garbagemule/MobArena/ArenaMasterImpl.java | 591 +++++++ .../MobArena/ArenaMasterStandard.java | 388 ----- src/com/garbagemule/MobArena/ArenaPlayer.java | 78 +- .../MobArena/ArenaPlayerStatistics.java | 179 ++- .../garbagemule/MobArena/ArenaStandard.java | 1413 ----------------- .../garbagemule/MobArena/MABlockListener.java | 84 - src/com/garbagemule/MobArena/MAClass.java | 149 -- src/com/garbagemule/MobArena/MACommands.java | 1309 --------------- .../MobArena/MAEntityListener.java | 79 - src/com/garbagemule/MobArena/MAListener.java | 724 --------- src/com/garbagemule/MobArena/MAMessages.java | 68 +- .../MobArena/MAPlayerListener.java | 96 -- .../garbagemule/MobArena/MASpawnThread.java | 516 +++--- src/com/garbagemule/MobArena/MAUtils.java | 1316 +-------------- src/com/garbagemule/MobArena/Messenger.java | 103 ++ src/com/garbagemule/MobArena/MobArena.java | 322 ++-- .../garbagemule/MobArena/MobArenaHandler.java | 117 +- .../MobArena/MobArenaListener.java | 25 - .../garbagemule/MobArena/MobArenaPlugin.java | 36 - .../garbagemule/MobArena/MonsterManager.java | 130 ++ src/com/garbagemule/MobArena/Msg.java | 6 + src/com/garbagemule/MobArena/PlayerData.java | 108 ++ .../garbagemule/MobArena/RewardManager.java | 63 + .../MobArena/commands/Command.java | 20 + .../MobArena/commands/CommandHandler.java | 256 ++- .../MobArena/commands/CommandInfo.java | 33 + .../MobArena/commands/Commands.java | 31 + .../MobArena/commands/MACommand.java | 40 - .../commands/admin/DisableCommand.java | 55 + .../commands/admin/EnableCommand.java | 57 +- .../MobArena/commands/admin/ForceCommand.java | 90 ++ .../MobArena/commands/admin/KickCommand.java | 47 + .../commands/admin/RestoreCommand.java | 43 + .../commands/setup/AddArenaCommand.java | 51 + .../commands/setup/AddClassPermCommand.java | 47 + .../commands/setup/AddContainerCommand.java | 47 + .../commands/setup/AddSpawnpointCommand.java | 41 + .../MobArena/commands/setup/ArenaCommand.java | 37 + .../commands/setup/AutoDegenerateCommand.java | 50 + .../commands/setup/AutoGenerateCommand.java | 54 + .../commands/setup/CheckDataCommand.java | 33 + .../commands/setup/ConfigCommand.java | 39 + .../commands/setup/ContainersCommand.java | 38 + .../commands/setup/EditArenaCommand.java | 75 + .../setup/ExpandLobbyRegionCommand.java | 56 + .../commands/setup/ExpandRegionCommand.java | 56 + .../commands/setup/ListClassPermsCommand.java | 57 + .../commands/setup/ListClassesCommand.java | 34 + .../commands/setup/ProtectCommand.java | 75 + .../commands/setup/RemoveArenaCommand.java | 50 + .../commands/setup/RemoveClassCommand.java | 42 + .../setup/RemoveClassPermCommand.java | 47 + .../setup/RemoveContainerCommand.java | 35 + .../setup/RemoveSpawnpointCommand.java | 35 + .../commands/setup/SetArenaCommand.java | 41 + .../commands/setup/SetClassCommand.java | 65 + .../commands/setup/SetLobbyRegionCommand.java | 41 + .../commands/setup/SetRegionCommand.java | 52 +- .../commands/setup/SetWarpCommand.java | 42 + .../commands/setup/ShowRegionCommand.java | 59 + .../commands/setup/SpawnpointsCommand.java | 38 + .../commands/user/ArenaListCommand.java | 63 +- .../MobArena/commands/user/JoinCommand.java | 94 +- .../MobArena/commands/user/LeaveCommand.java | 51 +- .../commands/user/NotReadyCommand.java | 62 +- .../commands/user/PlayerListCommand.java | 63 +- .../MobArena/commands/user/SpecCommand.java | 70 + .../MobArena/events/ArenaEndEvent.java | 43 + .../events/ArenaPlayerDeathEvent.java | 37 + .../MobArena/events/ArenaPlayerJoinEvent.java | 50 + .../events/ArenaPlayerLeaveEvent.java | 50 + .../MobArena/events/ArenaStartEvent.java | 43 + .../MobArena/events/NewWaveEvent.java | 55 + .../garbagemule/MobArena/framework/Arena.java | 231 +++ .../{ => framework}/ArenaListener.java | 10 +- .../MobArena/framework/ArenaMaster.java | 136 ++ .../MobArena/health/HealthStrategy.java | 13 + .../MobArena/health/HealthStrategyHeroes.java | 21 + .../health/HealthStrategyStandard.java | 11 + .../AbstractLeaderboardColumn.java | 57 + .../leaderboards/ClassLeaderboardColumn.java | 19 + .../leaderboards/IntLeaderboardColumn.java | 19 + .../MobArena/leaderboards/Leaderboard.java | 61 +- .../leaderboards/LeaderboardColumn.java | 127 +- .../leaderboards/PlayerLeaderboardColumn.java | 19 + .../MobArena/leaderboards/Stats.java | 47 +- .../MobArena/listeners/MAGlobalListener.java | 278 ++++ .../listeners/MagicSpellsListener.java | 42 +- .../listeners/MagicSpellsOldListener.java | 62 - ...Listener.java => SpoutScreenListener.java} | 15 +- .../MobArena/region/ArenaRegion.java | 517 ++++++ .../MobArena/region/RegionPoint.java | 13 + .../MobArena/region/RegionSerializer.java | 143 ++ .../repairable/RepairableContainer.java | 34 +- .../MobArena/spout/ClassPopup.java | 4 + .../garbagemule/MobArena/spout/Spouty.java | 15 +- src/com/garbagemule/MobArena/time/Time.java | 27 + .../MobArena/time/TimeStrategy.java | 24 + .../MobArena/time/TimeStrategyLocked.java | 27 + .../MobArena/time/TimeStrategyNull.java | 15 + src/com/garbagemule/MobArena/util/Config.java | 177 --- .../MobArena/util/ConfigUtils.java | 47 - src/com/garbagemule/MobArena/util/Delays.java | 25 +- src/com/garbagemule/MobArena/util/Enums.java | 22 +- .../garbagemule/MobArena/util/FileUtils.java | 79 +- .../MobArena/util/InventoryItem.java | 269 ---- .../garbagemule/MobArena/util/ItemParser.java | 219 ++- .../garbagemule/MobArena/util/MutableInt.java | 31 + .../garbagemule/MobArena/util/TextUtils.java | 8 + .../MobArena/util/VersionChecker.java | 90 ++ .../classparsing/ClassIterationStrategy.java | 15 - .../util/classparsing/ClassParser.java | 46 - .../DirClassIterationStrategy.java | 52 - .../JarClassIterationStrategy.java | 62 - .../MobArena/util/config/Config.java | 446 ++++++ .../MobArena/util/config/ConfigSection.java | 97 ++ .../MobArena/util/config/ConfigUtils.java | 79 + .../MobArena/util/data/PlainText.java | 155 -- .../MobArena/util/data/Totals.java | 158 -- .../garbagemule/MobArena/util/data/XML.java | 182 --- .../garbagemule/MobArena/util/data/YAML.java | 63 - .../util/inventory/InventoryManager.java | 198 +++ .../util/inventory/InventoryUtils.java | 35 + .../util/inventory/SerializableInventory.java | 73 + .../util/inventory/SerializableItem.java | 55 + .../MobArena/waves/AbstractWave.java | 267 +--- .../MobArena/waves/BossAbility.java | 86 +- .../MobArena/waves/BossAbilityThread.java | 52 + .../garbagemule/MobArena/waves/BossWave.java | 189 --- .../MobArena/waves/DefaultWave.java | 81 - .../garbagemule/MobArena/waves/MABoss.java | 48 + .../MobArena/waves/MACreature.java | 23 +- .../MobArena/waves/NormalWave.java | 101 -- .../MobArena/waves/SheepBouncer.java | 96 ++ .../MobArena/waves/SpecialWave.java | 78 - .../garbagemule/MobArena/waves/SwarmWave.java | 76 - src/com/garbagemule/MobArena/waves/Wave.java | 214 ++- .../MobArena/waves/WaveManager.java | 137 ++ .../MobArena/waves/WaveParser.java | 380 +++++ .../MobArena/{util => waves}/WaveUtils.java | 147 +- .../MobArena/waves/enums/BossHealth.java | 21 + .../MobArena/waves/enums/SwarmAmount.java | 21 + .../MobArena/waves/enums/WaveBranch.java | 25 + .../MobArena/waves/enums/WaveError.java | 32 + .../MobArena/waves/enums/WaveGrowth.java | 27 + .../MobArena/waves/enums/WaveType.java | 57 + .../MobArena/waves/types/BossWave.java | 97 ++ .../MobArena/waves/types/DefaultWave.java | 61 + .../MobArena/waves/types/SpecialWave.java | 51 + .../MobArena/waves/types/SupplyWave.java | 61 + .../MobArena/waves/types/SwarmWave.java | 40 + .../MobArena/waves/types/UpgradeWave.java | 51 + .../garbagemule/register/payment/Method.java | 183 --- .../garbagemule/register/payment/Methods.java | 236 --- .../register/payment/methods/BOSE6.java | 234 --- .../register/payment/methods/BOSE7.java | 223 --- .../register/payment/methods/EE17.java | 223 --- .../register/payment/methods/MCUR.java | 136 -- .../register/payment/methods/iCo4.java | 167 -- .../register/payment/methods/iCo5.java | 243 --- .../register/payment/methods/iCo6.java | 159 -- test/config.yml | 5 +- .../mobarena/standard/TestNewConfig.java | 212 --- .../mobarena/standard/TestUserCommands.java | 208 --- test/mock/bukkit/MockPlayer.java | 135 ++ test/mock/mobarena/MockArena.java | 540 ------- test/mock/mobarena/MockArenaMaster.java | 232 --- test/mock/mobarena/MockMobArena.java | 58 +- test/mock/util/MockLogger.java | 26 +- 193 files changed, 11912 insertions(+), 12726 deletions(-) create mode 100644 .gitignore rename README => README.md (79%) create mode 100644 build.xml create mode 100644 language files/cz/announcements.properties create mode 100644 language files/da/announcements.properties create mode 100644 language files/de/announcements.properties create mode 100644 language files/fr/announcements.properties create mode 100644 language files/nl/announcements.properties create mode 100644 language files/pirate/announcements.properties create mode 100644 resources/res/classes.yml create mode 100644 resources/res/global-settings.yml create mode 100644 resources/res/rewards.yml create mode 100644 resources/res/settings.yml create mode 100644 resources/res/waves.yml delete mode 100644 src/com/garbagemule/MobArena/Arena.java create mode 100644 src/com/garbagemule/MobArena/ArenaClass.java create mode 100644 src/com/garbagemule/MobArena/ArenaImpl.java create mode 100644 src/com/garbagemule/MobArena/ArenaListenerImpl.java delete mode 100644 src/com/garbagemule/MobArena/ArenaLog.java delete mode 100644 src/com/garbagemule/MobArena/ArenaMaster.java create mode 100644 src/com/garbagemule/MobArena/ArenaMasterImpl.java delete mode 100644 src/com/garbagemule/MobArena/ArenaMasterStandard.java delete mode 100644 src/com/garbagemule/MobArena/ArenaStandard.java delete mode 100644 src/com/garbagemule/MobArena/MABlockListener.java delete mode 100644 src/com/garbagemule/MobArena/MAClass.java delete mode 100644 src/com/garbagemule/MobArena/MACommands.java delete mode 100644 src/com/garbagemule/MobArena/MAEntityListener.java delete mode 100644 src/com/garbagemule/MobArena/MAListener.java delete mode 100644 src/com/garbagemule/MobArena/MAPlayerListener.java create mode 100644 src/com/garbagemule/MobArena/Messenger.java delete mode 100644 src/com/garbagemule/MobArena/MobArenaListener.java delete mode 100644 src/com/garbagemule/MobArena/MobArenaPlugin.java create mode 100644 src/com/garbagemule/MobArena/MonsterManager.java create mode 100644 src/com/garbagemule/MobArena/PlayerData.java create mode 100644 src/com/garbagemule/MobArena/RewardManager.java create mode 100644 src/com/garbagemule/MobArena/commands/Command.java create mode 100644 src/com/garbagemule/MobArena/commands/CommandInfo.java create mode 100644 src/com/garbagemule/MobArena/commands/Commands.java delete mode 100644 src/com/garbagemule/MobArena/commands/MACommand.java create mode 100644 src/com/garbagemule/MobArena/commands/admin/DisableCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/admin/ForceCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/admin/KickCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/admin/RestoreCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AddArenaCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AddClassPermCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AddContainerCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AddSpawnpointCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ArenaCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AutoDegenerateCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/AutoGenerateCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/CheckDataCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ConfigCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ContainersCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/EditArenaCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ExpandLobbyRegionCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ExpandRegionCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ListClassPermsCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ListClassesCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ProtectCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/RemoveArenaCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/RemoveClassCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/RemoveClassPermCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/RemoveContainerCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/RemoveSpawnpointCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/SetArenaCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/SetClassCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/SetLobbyRegionCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/SetWarpCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/ShowRegionCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/setup/SpawnpointsCommand.java create mode 100644 src/com/garbagemule/MobArena/commands/user/SpecCommand.java create mode 100644 src/com/garbagemule/MobArena/events/ArenaEndEvent.java create mode 100644 src/com/garbagemule/MobArena/events/ArenaPlayerDeathEvent.java create mode 100644 src/com/garbagemule/MobArena/events/ArenaPlayerJoinEvent.java create mode 100644 src/com/garbagemule/MobArena/events/ArenaPlayerLeaveEvent.java create mode 100644 src/com/garbagemule/MobArena/events/ArenaStartEvent.java create mode 100644 src/com/garbagemule/MobArena/events/NewWaveEvent.java create mode 100644 src/com/garbagemule/MobArena/framework/Arena.java rename src/com/garbagemule/MobArena/{ => framework}/ArenaListener.java (90%) create mode 100644 src/com/garbagemule/MobArena/framework/ArenaMaster.java create mode 100644 src/com/garbagemule/MobArena/health/HealthStrategy.java create mode 100644 src/com/garbagemule/MobArena/health/HealthStrategyHeroes.java create mode 100644 src/com/garbagemule/MobArena/health/HealthStrategyStandard.java create mode 100644 src/com/garbagemule/MobArena/leaderboards/AbstractLeaderboardColumn.java create mode 100644 src/com/garbagemule/MobArena/leaderboards/ClassLeaderboardColumn.java create mode 100644 src/com/garbagemule/MobArena/leaderboards/IntLeaderboardColumn.java create mode 100644 src/com/garbagemule/MobArena/leaderboards/PlayerLeaderboardColumn.java create mode 100644 src/com/garbagemule/MobArena/listeners/MAGlobalListener.java delete mode 100644 src/com/garbagemule/MobArena/listeners/MagicSpellsOldListener.java rename src/com/garbagemule/MobArena/listeners/{MASpoutScreenListener.java => SpoutScreenListener.java} (64%) create mode 100644 src/com/garbagemule/MobArena/region/ArenaRegion.java create mode 100644 src/com/garbagemule/MobArena/region/RegionPoint.java create mode 100644 src/com/garbagemule/MobArena/region/RegionSerializer.java create mode 100644 src/com/garbagemule/MobArena/time/Time.java create mode 100644 src/com/garbagemule/MobArena/time/TimeStrategy.java create mode 100644 src/com/garbagemule/MobArena/time/TimeStrategyLocked.java create mode 100644 src/com/garbagemule/MobArena/time/TimeStrategyNull.java delete mode 100644 src/com/garbagemule/MobArena/util/Config.java delete mode 100644 src/com/garbagemule/MobArena/util/ConfigUtils.java delete mode 100644 src/com/garbagemule/MobArena/util/InventoryItem.java create mode 100644 src/com/garbagemule/MobArena/util/MutableInt.java create mode 100644 src/com/garbagemule/MobArena/util/VersionChecker.java delete mode 100644 src/com/garbagemule/MobArena/util/classparsing/ClassIterationStrategy.java delete mode 100644 src/com/garbagemule/MobArena/util/classparsing/ClassParser.java delete mode 100644 src/com/garbagemule/MobArena/util/classparsing/DirClassIterationStrategy.java delete mode 100644 src/com/garbagemule/MobArena/util/classparsing/JarClassIterationStrategy.java create mode 100644 src/com/garbagemule/MobArena/util/config/Config.java create mode 100644 src/com/garbagemule/MobArena/util/config/ConfigSection.java create mode 100644 src/com/garbagemule/MobArena/util/config/ConfigUtils.java delete mode 100644 src/com/garbagemule/MobArena/util/data/PlainText.java delete mode 100644 src/com/garbagemule/MobArena/util/data/Totals.java delete mode 100644 src/com/garbagemule/MobArena/util/data/XML.java delete mode 100644 src/com/garbagemule/MobArena/util/data/YAML.java create mode 100644 src/com/garbagemule/MobArena/util/inventory/InventoryManager.java create mode 100644 src/com/garbagemule/MobArena/util/inventory/InventoryUtils.java create mode 100644 src/com/garbagemule/MobArena/util/inventory/SerializableInventory.java create mode 100644 src/com/garbagemule/MobArena/util/inventory/SerializableItem.java create mode 100644 src/com/garbagemule/MobArena/waves/BossAbilityThread.java delete mode 100644 src/com/garbagemule/MobArena/waves/BossWave.java delete mode 100644 src/com/garbagemule/MobArena/waves/DefaultWave.java create mode 100644 src/com/garbagemule/MobArena/waves/MABoss.java delete mode 100644 src/com/garbagemule/MobArena/waves/NormalWave.java create mode 100644 src/com/garbagemule/MobArena/waves/SheepBouncer.java delete mode 100644 src/com/garbagemule/MobArena/waves/SpecialWave.java delete mode 100644 src/com/garbagemule/MobArena/waves/SwarmWave.java create mode 100644 src/com/garbagemule/MobArena/waves/WaveManager.java create mode 100644 src/com/garbagemule/MobArena/waves/WaveParser.java rename src/com/garbagemule/MobArena/{util => waves}/WaveUtils.java (77%) create mode 100644 src/com/garbagemule/MobArena/waves/enums/BossHealth.java create mode 100644 src/com/garbagemule/MobArena/waves/enums/SwarmAmount.java create mode 100644 src/com/garbagemule/MobArena/waves/enums/WaveBranch.java create mode 100644 src/com/garbagemule/MobArena/waves/enums/WaveError.java create mode 100644 src/com/garbagemule/MobArena/waves/enums/WaveGrowth.java create mode 100644 src/com/garbagemule/MobArena/waves/enums/WaveType.java create mode 100644 src/com/garbagemule/MobArena/waves/types/BossWave.java create mode 100644 src/com/garbagemule/MobArena/waves/types/DefaultWave.java create mode 100644 src/com/garbagemule/MobArena/waves/types/SpecialWave.java create mode 100644 src/com/garbagemule/MobArena/waves/types/SupplyWave.java create mode 100644 src/com/garbagemule/MobArena/waves/types/SwarmWave.java create mode 100644 src/com/garbagemule/MobArena/waves/types/UpgradeWave.java delete mode 100644 src/com/garbagemule/register/payment/Method.java delete mode 100644 src/com/garbagemule/register/payment/Methods.java delete mode 100644 src/com/garbagemule/register/payment/methods/BOSE6.java delete mode 100644 src/com/garbagemule/register/payment/methods/BOSE7.java delete mode 100644 src/com/garbagemule/register/payment/methods/EE17.java delete mode 100644 src/com/garbagemule/register/payment/methods/MCUR.java delete mode 100644 src/com/garbagemule/register/payment/methods/iCo4.java delete mode 100644 src/com/garbagemule/register/payment/methods/iCo5.java delete mode 100644 src/com/garbagemule/register/payment/methods/iCo6.java delete mode 100644 test/garbagemule/mobarena/standard/TestNewConfig.java delete mode 100644 test/garbagemule/mobarena/standard/TestUserCommands.java delete mode 100644 test/mock/mobarena/MockArena.java delete mode 100644 test/mock/mobarena/MockArenaMaster.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..695b0e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# We don't want to add the Eclipse files. +.project +.classpath + +# We can also do without .class files. +*.class + +# And we definitely don't need OS files. +.DS_Store* +Thumbs.db + +# Exclude some folders +bin/ +build/ +misc/ +lib/ +test/ +.settings/ \ No newline at end of file diff --git a/MobArena.jar b/MobArena.jar index 3d29c797be3bb6462012b14dcd5f6d1921597333..1ec76a8ae946e95fa636c92c515fa25b280e98ac 100644 GIT binary patch literal 310838 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

{oHkut1%9;9BbsL8?F*912yn*JD`QzKY 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
+=G8j7;?0zau;KI2cPlJ=w# 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

Hlyx^MOGfU33shAiFddN+03J0|lwbT@S1ymt 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%0D|@r( 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 b+SU=XnyLpuqyya=_{T~Yq#D+s^MU*i*PIbh literal 219934 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�f{;yUoqx|U^la*QCZ4uZ`BJJgOD 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�T`h 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 l9CZ@FAAlyJp8$W(1-K>)x#JRix&Z!31Y1fhNI@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/language files/cz/announcements.properties b/language files/cz/announcements.properties new file mode 100644 index 0000000..a2e961b --- /dev/null +++ b/language files/cz/announcements.properties @@ -0,0 +1,58 @@ +ARENA_START=Necht zacne masakr!|Arena zacala! +ARENA_END=Arena skoncila.|Arena skoncila. +ARENA_DOES_NOT_EXIST=Takova arena neexistuje. Napis /ma arenas pro seznam aren.|Arena nesla najit. +JOIN_NOT_ENABLED=Areny jsou v tuto chvili vypnute.|Areny nejsou zapnute. +JOIN_IN_OTHER_ARENA=Uz jsi v arene! Musis tuhle opustit nez pujdes do jine.|Jsi v jine arene. +JOIN_ARENA_NOT_ENABLED=Tato arena neni dostupna.|Tato arena je vypnuta. +JOIN_ARENA_NOT_SETUP=Tato arena jeste nebyla nastavena.|Arena neni nastavena. +JOIN_ARENA_EDIT_MODE=Tato arena se zrovna upravuje.|Arena se upravuje. +JOIN_ARENA_PERMISSION=Nemas opravneni ke vstupu do teto areny.|K tomuhle nemas opravneni! +JOIN_FEE_REQUIRED=Insufficient funds. Price: %|Price: % +JOIN_FEE_PAID=Price to join was: %|Paid: % +JOIN_ARENA_IS_RUNNING=Tato arena uz zacala, musis pockat az skonci.|Arena uz probiha, zkus to pozdeji! +JOIN_ALREADY_PLAYING=Uz hrajes!|Uz hrajes! +JOIN_ARG_NEEDED=Musis upresnit arenu. Napis /ma arenas pro seznam aren. +JOIN_TOO_FAR=YJsi prilis daleko od areny aby ses mohl pripojit/sledovat.|Prilis daleko od areny. +JOIN_EMPTY_INV=Musis si nejdriv vyprazdnit inventar.|Vyprazdni si inventar. +JOIN_PLAYER_LIMIT_REACHED=V arene je uz maximum hracu.|Uz nejsou volna mista. +JOIN_STORE_INV_FAIL=Tvuj inventar se nepodarilo ulozit. Zkus to znovu. +JOIN_EXISTING_INV_RESTORED=Tvuj inventar byl navracen. +JOIN_PLAYER_JOINED=Pridal ses do areny. Uzi si to!|Pridal ses do areny. +LEAVE_NOT_PLAYING=Nejsi v arene.|Nejsi v arene. +LEAVE_PLAYER_LEFT=Opustils arenu. Diky za boj!|Opustils arenu. +PLAYER_DIED=% zemrel!|% padl v boji! +SPEC_PLAYER_SPECTATE=Uzi si show!|Uzi si podivanou! +SPEC_NOT_RUNNING=Tato arena neprobiha.|Arena neprobiha. +SPEC_ARG_NEEDED=Musis upresnit arenu. Napis /ma arenas pro seznam aren.|Je pozadovano jmeno areny. +SPEC_EMPTY_INV=Musis si nejdriv vyprazdnit inventar.|Vyprazdni si inventar. +SPEC_ALREADY_PLAYING=Nemuzes byt divakem kdyz jsi bojovnikem!|Vzdyt uz bojujes! +NOT_READY_PLAYERS=Nepripraveni hraci: % +FORCE_START_RUNNING=Arena uz zacala. +FORCE_START_NOT_READY=Boj nemuze zacit, ani jeden hrac neni pripraveny. +FORCE_START_STARTED=Arena zacala, at se vam to libi nebo ne. +FORCE_END_EMPTY=V arene nikdo neni. +FORCE_END_ENDED=Arena ukoncena. +FORCE_END_IDLE=Nebyl jsi dost rychly! +REWARDS_GIVE=Tady jsou vsechyn tve odmeny! +LOBBY_DROP_ITEM=Ne ne ne, tady se o veci delit nesmi!|Veci se tu hazet nesmi. +LOBBY_PLAYER_READY=Jsi pripraven!|Jsi pripraven! +LOBBY_PICK_CLASS=Nejdriv si vyber typ bojovnika!|Vyber si typ bojovnika! +LOBBY_NOT_ENOUGH_PLAYERS=Tak malo bojovniku do areny nemuze. Je potreba alespon % hracu.|Je potreba vice bojovniku! +LOBBY_RIGHT_CLICK=Klikni levym, ne pravym.|Klikni levym. +LOBBY_CLASS_PICKED=Rozhodl ses, ze budes %!|% +LOBBY_CLASS_RANDOM=Na zacatku boje ti bude nahodne pridelen typ bojovnika. +LOBBY_CLASS_PERMISSION=Nemas opravneni byt timto typem bojovnika!|Smula, ale k tomu nemas opravneni! +WARP_TO_ARENA=Nemuzes se objevit v arene uprostred boje! +WARP_FROM_ARENA=Warping not allowed in the arena! +Vlna_DEFAULT=Vlna #%!|Vlna #%! +Vlna_SPECIAL=Vlna #%! [SPECIAL]|Vlna #%! [SPECIAL] +Vlna_SWARM=Vlna #%! [SWARM]|Vlna #%! [SWARM] +Vlna_BOSS=Vlna #%! [BOSS]|Vlna #%! [BOSS] +Vlna_BOSS_ABILITY=Boss pouzil schopnost: %!|Boss: % +Vlna_BOSS_LOW_HEALTH=Boss je temer mrtvy!|Bossovi uz nezbyva moc zivota! +Vlna_REWARD=Ziskal jsi tuto odmenu: %|Byl jsi odmenen: % +MISC_LIST_PLAYERS=Bojovnici stale na nohou: % +MISC_LIST_ARENAS=Dostupne areny: % +MISC_COMMAND_NOT_ALLOWED=Tenhle prikaz v arene pouzit nemuzes! +MISC_NO_ACCESS=K tomuto prikazu nemas pristup. +MISC_NONE= diff --git a/language files/da/announcements.properties b/language files/da/announcements.properties new file mode 100644 index 0000000..42d61f2 --- /dev/null +++ b/language files/da/announcements.properties @@ -0,0 +1,51 @@ +ARENA_START=Lad massakren begynde! +ARENA_END=Arena slut. +ARENA_DOES_NOT_EXIST=Arenaen findes ikke. Skriv /ma arenas for en liste. +JOIN_PLAYER_JOINED=Du joinede arenaen! +JOIN_NOT_ENABLED=MobArena er ikke aktiveret. +JOIN_IN_OTHER_ARENA=Du er allerede i en arena. Forlad den først. +JOIN_ARENA_NOT_ENABLED=Arenaen er deaktiveret. +JOIN_ARENA_NOT_SETUP=Arenaen er ikke blevet sat op endnu. +JOIN_ARENA_PERMISSION=Du har ikke rettigheder til den arena! +JOIN_FEE_REQUIRED=Du er for fattig. Pris: % +JOIN_FEE_PAID=Prisen for at joine var: % +JOIN_ARENA_IS_RUNNING=Arenaen er allerede startet. +JOIN_ALREADY_PLAYING=Du spiller allerede! +JOIN_ARG_NEEDED=Arenanavn påkrævet. Skriv /ma arenas for en liste. +JOIN_TOO_FAR=Du er for langt fra denne arena til at kunne joine. +JOIN_EMPTY_INV=Du skal tømme dit inventory før du kan joine arenaen. +JOIN_PLAYER_LIMIT_REACHED=Der kan ikke være flere spillere i arenaen. +JOIN_STORE_INV_FAIL=Kunne ikke gemme dit inventory. Prøv igen. +LEAVE_PLAYER_LEFT=Du forlod arenaen. +LEAVE_NOT_PLAYING=Du er ikke i en arena. +PLAYER_DIED=% døde! +SPEC_PLAYER_SPECTATE=Nyd forestillingen! +SPEC_NOT_RUNNING=Arenaen er ikke startet. +SPEC_ARG_NEEDED=Arenanavn påkrævet. Skriv /ma arenas for en liste. +SPEC_EMPTY_INV=Du skal tømme dit inventory først. +SPEC_ALREADY_PLAYING=Du spiller allerede! +NOT_READY_PLAYERS=Ikke klar: % +FORCE_START_STARTED=Arena tvunget til at starte. +FORCE_START_RUNNING=Arenaen er allerede startet. +FORCE_START_NOT_READY=Ingen spillere er klar. +FORCE_END_ENDED=Arena tvunget til at slutte. +FORCE_END_EMPTY=Der er ingen i arenaen. +FORCE_END_IDLE=Du var ikke hurtig nok! +REWARDS_GIVE=Her er alle dine præmier! +LOBBY_CLASS_PICKED=Du har valgt % som din klasse! +LOBBY_CLASS_RANDOM=Du får tildelt en klasse ved arena start. +LOBBY_CLASS_PERMISSION=Du har ikke rettigheder til den klasse! +LOBBY_PLAYER_READY=Du er markeret som klar! +LOBBY_DROP_ITEM=Du kan ikke dele items i lobbyen! +LOBBY_PICK_CLASS=Du skal først vælge en klasse! +LOBBY_RIGHT_CLICK=Slå skiltene; ikke højre-klik. +WARP_TO_ARENA=Du kan ikke warpe til arenaen efter, den er startet. +WARP_FROM_ARENA=Du kan ikke warpe fra arenaen. +WAVE_DEFAULT=Bølge #%! +WAVE_SPECIAL=Bølge #%! [SPECIEL] +WAVE_REWARD=Du har lige vundet en præmie: % +MISC_LIST_ARENAS=Tilgængelige arenaer: % +MISC_LIST_PLAYERS=Levende spillere: % +MISC_COMMAND_NOT_ALLOWED=Du kan ikke bruge den kommando i arenaen. +MISC_NO_ACCESS=Du har ikke adgang til den kommando. +MISC_NONE= diff --git a/language files/de/announcements.properties b/language files/de/announcements.properties new file mode 100644 index 0000000..04993f3 --- /dev/null +++ b/language files/de/announcements.properties @@ -0,0 +1,53 @@ +ARENA_START=Lasst die Spiele beginnen +ARENA_END=Arena beendet +ARENA_DOES_NOT_EXIST=Diese Arena existiert nicht tippe /ma um alle Arenen anzuzeigen. +JOIN_PLAYER_JOINED=Du bist nun in der Arena. +Viel Spass! +JOIN_NOT_ENABLED=Die Mobarena wurde deaktiviert +JOIN_IN_OTHER_ARENA=Du bist momentan in einer Arena, verlasse diese jetzt. +JOIN_ARENA_NOT_ENABLED=Diese Arena ist nicht aktiviert. +JOIN_ARENA_NOT_SETUP=Diese Arena wurde noch nicht eingerichtet. +JOIN_ARENA_PERMISSION=Du bist nicht berechtigt diese Arena zu betretten. +JOIN_FEE_REQUIRED=Ungenügender Kontostand. Preis: % +JOIN_FEE_PAID=Der Preis um bei zu tretten: % +JOIN_ARENA_IS_RUNNING=Der Kampf in dieser Arena ist bereits zu gange. +JOIN_ALREADY_PLAYING=Du spielst bereits! +JOIN_ARG_NEEDED=Du must den Namen der Arena eingeben. Gib /ma arenas ein für eine Liste. +JOIN_TOO_FAR=Du bist zu weit weg um bei zu tretten oder zu zuschauen. +JOIN_EMPTY_INV=Du must dein Inventar leeren um der Arena bei zu tretten. +JOIN_PLAYER_LIMIT_REACHED=Die Spieler Limite ist erreicht. +JOIN_STORE_INV_FAIL=Momentan nicht möglich dein Inventar zu speichern. Probiere es noch einmal. +LEAVE_PLAYER_LEFT=Du hast die Arena verlassen. Vielen Dank fürs Spielen! +LEAVE_NOT_PLAYING=Du kannst die Arena nicht verlassen wenn du nich in der Arena bist! Du Depp. +PLAYER_DIED=% ist gestorben! +SPEC_PLAYER_SPECTATE=Amüsier dich am Kampf! +SPEC_NOT_RUNNING=Diese Arena läuft im Moment nicht. +SPEC_ARG_NEEDED=Du must den Namen der Arena eingeben. Gib /ma arenas ein für eine Liste. +SPEC_EMPTY_INV=Du must dein Inventar leeren um der Arena bei zu tretten! +SPEC_ALREADY_PLAYING=Du kannst dir nicht selber zuschauen wie du kämpfst! +NOT_READY_PLAYERS=Noch nicht bereit sind: % +FORCE_START_STARTED=Gezwungener Start des Kampfes. +FORCE_START_RUNNING=Die Arena wurde bereits gestartet. +FORCE_START_NOT_READY=Kann den Start des Kampfes nicht erzwingen, keine Spieler vorhanden. +FORCE_END_ENDED=Gezwunges Kampf Ende. +FORCE_END_EMPTY=Niemand ist in der Arena. +FORCE_END_IDLE=Du warst nicht schnell genug! +REWARDS_GIVE=Hier sind deine Preise! +LOBBY_CLASS_PICKED=Du hast % als deine Kampfklasse gewählt! +LOBBY_CLASS_RANDOM=Du wirst beim Start des Spiels einer zufälligen Kampfklasse zugeortnet. +LOBBY_CLASS_PERMISSION=Du hast keine Berechtigung diese Kampfklasse zu wählen! +LOBBY_NOT_ENOUGH_PLAYERS=Nicht genung Spieler um den Kampf zu starten. Es benötigt mindestens % Spieler. +LOBBY_PLAYER_READY=Du hast dich als bereit gemeldet! +LOBBY_DROP_ITEM=Du darfts momentant deine Tools nicht an andere weitergeben! +LOBBY_PICK_CLASS=Du must zuerst eine Kampfklasse auswählen! +LOBBY_RIGHT_CLICK=Du must das Schild schlagen. Nicht rechtsklicken. +WARP_TO_ARENA=Du kannst dich jetzt nich in die Arena teleportieren! +WARP_FROM_ARENA=Du darfst dich jetzt nicht aus der Arena teleportiern! +WAVE_DEFAULT=Sei bereit für die %. Welle! +WAVE_SPECIAL=Mach dich für eine spezielle %. Welle bereit! +WAVE_REWARD=Du hast einen Preis: % +MISC_LIST_ARENAS=Verfügbare Arenas: % +MISC_LIST_PLAYERS=Live Spieler: % +MISC_COMMAND_NOT_ALLOWED=Du darfst diesen Befehl nicht in der Arena benutzen! +MISC_NO_ACCESS=Du hast keinen Zugriff auf diesen Befehl. +MISC_NONE= diff --git a/language files/fr/announcements.properties b/language files/fr/announcements.properties new file mode 100644 index 0000000..6f2b9d3 --- /dev/null +++ b/language files/fr/announcements.properties @@ -0,0 +1,51 @@ +ARENA_START=Que le massacre commence ! +ARENA_END=L'arène est terminée. +ARENA_DOES_NOT_EXIST=Cette arène n'existe pas. Entrez "/ma arenas" pour la liste. +JOIN_PLAYER_JOINED=Vous avez rejoint l'arène. Prennez plaisir ! +JOIN_NOT_ENABLED=MobArena n'est pas actif. +JOIN_IN_OTHER_ARENA=Vous êtes déjà dans l'arène! Quittez-la avant. +JOIN_ARENA_NOT_ENABLED=Cetta arène n'est pas active. +JOIN_ARENA_NOT_SETUP=Cette arène n'a pas encore été configurée. +JOIN_ARENA_PERMISSION=Vous n'avez pas la permission pour rejoindre cette arène. +JOIN_FEE_REQUIRED=Pas assez d'argent. Prix: % +JOIN_FEE_PAID=Le prix pour rejoindre était de: % +JOIN_ARENA_IS_RUNNING=L'arène est déjà occupée. +JOIN_ALREADY_PLAYING=Vous êtes déjà en train de jouer. +JOIN_ARG_NEEDED=Vous devez spécifier une arène. Entrez "/ma arenas" pour la liste. +JOIN_TOO_FAR=Vous êtes trop loins de l'arène pour pouvoir joindre en tant que joueur ou specateur. +JOIN_EMPTY_INV=Vous devez vider votre inventaire afin de joindre l'arène. +JOIN_PLAYER_LIMIT_REACHED=La limite de joueur pour cette arène a été atteinte. +JOIN_STORE_INV_FAIL=La sauvegarde de l'inventaire a échouée. Essayez encore. +LEAVE_PLAYER_LEFT=Vous avez quitté l'arène. Merci d'avoir joué ! +LEAVE_NOT_PLAYING=Vous n'êtes pas dans l'arène. +PLAYER_DIED=% est mort ! +SPEC_PLAYER_SPECTATE=Profitez bien du spectacle ! +SPEC_NOT_RUNNING=Cette arène n'est pas en cours. +SPEC_ARG_NEEDED=Vous devez spécifier une arène. Entrez "/ma arenas" pour la liste. +SPEC_EMPTY_INV=Vous devez vider votre inventaire. +SPEC_ALREADY_PLAYING=Vous ne pouvez pas être spectateur en tant que joueur de l'arène ! +NOT_READY_PLAYERS=Non prêt: % +FORCE_START_STARTED=Début forcé de l'arène. +FORCE_START_RUNNING=L'arène a commencée. +FORCE_START_NOT_READY=Ne peut pas forcer le début, il n'y a pas de joueurs prêts. +FORCE_END_ENDED=Fin forcée de l'arène. +FORCE_END_EMPTY=Il n'y a personne dans l'arène. +FORCE_END_IDLE=Vous n'avez pas été assez rapide ! +REWARDS_GIVE=Voici vos récompenses ! +LOBBY_CLASS_PICKED=Vous avez choisi % comme classe ! +LOBBY_CLASS_RANDOM=Une classe aléatoire va vous être attribuée. +LOBBY_CLASS_PERMISSION=Vous n'avez pas les permissions pour utiliser cette classe ! +LOBBY_PLAYER_READY=Vous avez été marqué comme prêt ! +LOBBY_DROP_ITEM=Pas de partage avant le début de l'arène ! +LOBBY_PICK_CLASS=Vous devez d'abord choisir une classe ! +LOBBY_RIGHT_CLICK=Vous devez frapper le panneau, pas cliquer du droit dessus. +WARP_TO_ARENA=Vous ne pouvez pas vous téléporter à l'arène pendant la bataille ! +WARP_FROM_ARENA=La téléportation est interdite dans l'arène ! +WAVE_DEFAULT=Soyez prêt pour la vague #% ! +WAVE_SPECIAL=Soyez prêt pour la vague #% ! [SPECIALE] +WAVE_REWARD=Vous avez gagné une récompense: % +MISC_LIST_ARENAS=Les arènes disponibles: % +MISC_LIST_PLAYERS=Les joueurs en live: % +MISC_COMMAND_NOT_ALLOWED=Vous ne pouvez pas utiliser cette commande dans l'arène ! +MISC_NO_ACCESS=Vous n'avez pas accès à cette commande. +MISC_NONE= diff --git a/language files/nl/announcements.properties b/language files/nl/announcements.properties new file mode 100644 index 0000000..64b86b5 --- /dev/null +++ b/language files/nl/announcements.properties @@ -0,0 +1,52 @@ +ARENA_START=Laat de slachting beginnen! +ARENA_END=Arena voltooid. +ARENA_DOES_NOT_EXIST=Die arena bestaat niet. Typ /ma arenas voor een lijst. +JOIN_PLAYER_JOINED=U heeft toegetreden tot deze arena. Veel plezier! +JOIN_NOT_ENABLED=MobArena staat niet aan. +JOIN_IN_OTHER_ARENA=U bevindt zich al in een arena! Vertaal die eerst. +JOIN_ARENA_NOT_ENABLED=Deze arena staat niet aan. +JOIN_ARENA_PERMISSION=U heeft geen permissie om deze arena toe te treden. +JOIN_ARENA_NOT_SETUP=Deze arena is nog niet ingesteld. +JOIN_FEE_REQUIRED=Onvoldoende geld. Prijs: % +JOIN_FEE_PAID=Prijs om toe te treden is: % +JOIN_ARENA_IS_RUNNING=Deze arena is al bezig. +JOIN_ALREADY_PLAYING=U bent al aan het spelen! +JOIN_ARG_NEEDED=U moet een arena specificeren. Typ /ma arenas voor een lijst. +JOIN_TOO_FAR=U bent te ver weg van de arena om toe te treden/toe te kijken. +JOIN_EMPTY_INV=U moet uw inventaris eerst legen om toe te treden tot deze arena. +JOIN_PLAYER_LIMIT_REACHED=De speler limiet van deze arena is berijkt. +JOIN_STORE_INV_FAIL=Inventaris opslaan mislukt. Probeer het opnieuw. +LEAVE_PLAYER_LEFT=U verliet de arena. Bedankt voor het spelen! +LEAVE_NOT_PLAYING=U bent niet in de arena. +PLAYER_DIED=% ging dood! +SPEC_PLAYER_SPECTATE=Geniet van de show! +SPEC_NOT_RUNNING=Deze arena is niet actief. +SPEC_ARG_NEEDED=U moet een arena specificeren. Typ /ma arenas voor een lijst. +SPEC_EMPTY_INV=Leeg uw inventaris eerst! +SPEC_ALREADY_PLAYING=Kan niet toekijken als je in de arena zit! +NOT_READY_PLAYERS=Niet klaar: % +FORCE_START_STARTED=Geforceerd arena te starten. +FORCE_START_RUNNING=Arena is al begonnen. +FORCE_START_NOT_READY=Kan arena niet geforceerd starten, geen spelers zijn klaar. +FORCE_END_ENDED=Geforceerd arena te beëindigen. +FORCE_END_EMPTY=Niemand is in deze arena. +FORCE_END_IDLE=U was niet snel genoeg! +REWARDS_GIVE=Hier zijn al uw beloningen! +LOBBY_CLASS_PICKED=U heeft % gekozen als uw klasse! +LOBBY_CLASS_RANDOM=U krijgt een willekeurige klasse als de arena start. +LOBBY_CLASS_PERMISSION=U heeft geen permissie om deze klasse te gebruiken! +LOBBY_NOT_ENOUGH_PLAYERS=Niet genoeg spelers om te beginnen. Op zijn minst % spelers nodig. +LOBBY_PLAYER_READY=U bent gemarkeerd als klaar! +LOBBY_DROP_ITEM=Delen niet toegestaan ​​op dit moment! +LOBBY_PICK_CLASS=U moet eerst een klasse kiezen! +LOBBY_RIGHT_CLICK=Sla het bord. Geen klik met de rechtermuisknop. +WARP_TO_ARENA=Kan niet teleporteren naar een arena tijdens een gevecht! +WARP_FROM_ARENA=Teleporteren niet toegestaan ​​in de arena! +WAVE_DEFAULT=Maak U klaar voor golf #%! +WAVE_SPECIAL=Maak U klaar voor golf #%! [SPECIAAL] +WAVE_REWARD=U heeft zojuist een beloning verdiend: % +MISC_LIST_ARENAS=Beschikbare arenas: % +MISC_LIST_PLAYERS=Live spelers: % +MISC_COMMAND_NOT_ALLOWED=U kan dit commando niet gebruiken in de arena! +MISC_NO_ACCESS=U heeft geen toegang tot dit commando. +MISC_NONE= \ No newline at end of file diff --git a/language files/pirate/announcements.properties b/language files/pirate/announcements.properties new file mode 100644 index 0000000..0afaf05 --- /dev/null +++ b/language files/pirate/announcements.properties @@ -0,0 +1,51 @@ +ARENA_START=Hearts alive, men! +ARENA_END=Land ho! +ARENA_DOES_NOT_EXIST=Thar be no ship by that name! +JOIN_PLAYER_JOINED=Ahoy, matey! +JOIN_NOT_ENABLED=We be swabbing down the decks on all ships, matey. +JOIN_IN_OTHER_ARENA=Ye're already at sea, scallywag! +JOIN_ARENA_NOT_ENABLED=We be swabbing down the decks on this ship, matey. +JOIN_ARENA_NOT_SETUP=Arrrgh! We be missing the top sail! +JOIN_ARENA_PERMISSION=Arrgh! Ye can't board this ship, scurvy dog! +JOIN_FEE_REQUIRED=Ye don't own enough doubloons, matey! Price: % +JOIN_FEE_PAID=Booty ye gave up to join: % +JOIN_ARENA_IS_RUNNING=That ship has sailed, landlubber! +JOIN_ALREADY_PLAYING=Ye be aboard already, ye scurvy dog! +JOIN_ARG_NEEDED=Which ship are ye boarding? Type /ma arenas for a list, arrgh! +JOIN_TOO_FAR=Ye be too far from the docks, landlubber! +JOIN_EMPTY_INV=Throw ye belongings overboard, scallywag! +JOIN_PLAYER_LIMIT_REACHED=Thar be too many men aboard this ship already, landlubber! +JOIN_STORE_INV_FAIL=Ye belongings didn't fit in the storage deck, arrgh! +LEAVE_PLAYER_LEFT=Ye abandonned the ship! Arrrgh! +LEAVE_NOT_PLAYING=Ye're already on land, matey! +PLAYER_DIED=% walked the plank! +SPEC_PLAYER_SPECTATE=Welcome to the Crow's Nest! Grab ye spyglass! +SPEC_NOT_RUNNING=Thar be no one aboard that ship! +SPEC_ARG_NEEDED=Which ship are ye boarding? Type /ma arenas for a list, arrgh! +SPEC_EMPTY_INV=Throw ye belongings overboard, scallywag! +SPEC_ALREADY_PLAYING=Ye don't need ye spyglass aboard this ship, harr! +NOT_READY_PLAYERS=Landlubbers: % +FORCE_START_STARTED=Set sail! +FORCE_START_RUNNING=The ship has already set sail! +FORCE_START_NOT_READY=Thar be no scurvy dogs ready to set sail! +FORCE_END_ENDED=Forced the men to walk the plank! +FORCE_END_EMPTY=Thar be no one aboard the ship! +FORCE_END_IDLE=Arrrgh! Too slow, matey! +REWARDS_GIVE=Here's ye booty, seadog! +LOBBY_CLASS_PICKED=Ye be the %! +LOBBY_CLASS_RANDOM= +LOBBY_CLASS_PERMISSION=Ye can't use that cutlass, scurvy dog! +LOBBY_PLAYER_READY=Ready to sail! Arrrgh! +LOBBY_DROP_ITEM=Don't be sharing ye rum yet, scallywag! +LOBBY_PICK_CLASS=Grab ye cutlass first, matey! +LOBBY_RIGHT_CLICK=Ye can't be right-clickin', landlubber. +WARP_TO_ARENA=Ye can't board this ship, wench! +WARP_FROM_ARENA=Walk the plank if ye want to leave, arrrgh! +WAVE_DEFAULT=Wave %: Blow them all down! +WAVE_SPECIAL=Wave %: Thar she blows! +WAVE_REWARD=Ye earned yerself some booty: % +MISC_LIST_ARENAS=Ships: % +MISC_LIST_PLAYERS=Buccaneers: % +MISC_COMMAND_NOT_ALLOWED=Ye can't be usin' that command on the ship, landlubber! +MISC_NO_ACCESS=Only the captain uses these commands, wench! +MISC_NONE= diff --git a/resources/plugin.yml b/resources/plugin.yml index eab0b29..c4ceb5c 100644 --- a/resources/plugin.yml +++ b/resources/plugin.yml @@ -1,7 +1,8 @@ name: MobArena +author: garbagemule main: com.garbagemule.MobArena.MobArena -version: 0.94.4.4 -softdepend: [Spout,Permissions,MultiVerse,XcraftGate,Towny,Heroes,MagicSpells] +version: 0.94.4.49 +softdepend: [Spout,MultiVerse,XcraftGate,Towny,Heroes,MagicSpells,Vault] commands: ma: description: Base command for MobArena @@ -18,18 +19,20 @@ commands: http://pastebin.com, and then paste the link in the IRC or on the forums. permissions: - mobarena.use: - description: Gives access to usage commands - default: true - children: - mobarena.use.join: true - mobarena.use.spec: true - mobarena.use.leave: true mobarena.use.join: + description: Join command default: true mobarena.use.spec: + description: Spec command default: true mobarena.use.leave: + description: Leave command + default: true + mobarena.use.arenalist: + description: Arena list command + default: true + mobarena.use.playerlist: + description: Arena list command default: true mobarena.admin: @@ -39,9 +42,8 @@ permissions: mobarena.admin.enable: true mobarena.admin.kick: true mobarena.admin.restore: true - mobarena.admin.force.end: true - mobarena.admin.force.start: true - mobarena.admin.config.reload: true + mobarena.admin.force: true + mobarena.admin.teleport: true mobarena.admin.enable: description: Enable and disable MobArena and/or arenas. default: false @@ -51,14 +53,11 @@ permissions: mobarena.admin.restore: description: Restore a player's inventory. default: false - mobarena.admin.force.end: - description: Force end an arena. + mobarena.admin.force: + description: Force start or end an arena. default: false - mobarena.admin.force.start: - description: Force start an arena. - default: false - mobarena.admin.config.reload: - description: Reload the config-file. + mobarena.admin.teleport: + description: Immune to teleport blocking rules. default: false mobarena.setup: @@ -68,7 +67,7 @@ permissions: mobarena.setup.arena: true mobarena.setup.setarena: true mobarena.setup.addarena: true - mobarena.setup.delarena: true + mobarena.setup.removearena: true mobarena.setup.protect: true mobarena.setup.editarena: true mobarena.setup.setregion: true @@ -78,13 +77,11 @@ permissions: mobarena.setup.expandlobbyregion: true mobarena.setup.setwarp: true mobarena.setup.spawnpoints: true - mobarena.setup.addspawn: true - mobarena.setup.delspawn: true mobarena.setup.containers: true - mobarena.setup.addcontainer: true - mobarena.setup.delcontainer: true mobarena.setup.leaderboards: true mobarena.setup.checkdata: true + mobarena.setup.classes: true + mobarena.setup.config: true mobarena.setup.autogenerate: true mobarena.setup.autodegenerate: true mobarena.setup.arena: @@ -96,8 +93,8 @@ permissions: mobarena.setup.addarena: description: Add a new arena node. default: false - mobarena.setup.delarena: - description: Delete an arena. + mobarena.setup.removearena: + description: Remove an arena. default: false mobarena.setup.protect: description: Turn arena protection on/off. @@ -126,27 +123,21 @@ permissions: mobarena.setup.spawnpoints: description: List all spawnpoints. default: false - mobarena.setup.addspawn: - description: Add a new spawnpoint. - default: false - mobarena.setup.delspawn: - description: Delete a spawnpoint. - default: false mobarena.setup.containers: description: List all containers. default: false - mobarena.setup.addcontainer: - description: Add a new container. - default: false - mobarena.setup.delcontainer: - description: Delete a container. - default: false mobarena.setup.leaderboards: description: Set up leaderboards. default: false mobarena.setup.checkdata: description: Check which points need to be set up. default: false + mobarena.setup.classes: + description: Allows setting up classes from in-game + default: false + mobarena.setup.config: + description: Save or reload the config-file + default: false mobarena.setup.autogenerate: description: Auto-generate an arena. default: false diff --git a/resources/res/classes.yml b/resources/res/classes.yml new file mode 100644 index 0000000..d9c6bf4 --- /dev/null +++ b/resources/res/classes.yml @@ -0,0 +1,15 @@ +Knight: + items: diamond_sword, potion:8197:3 + armor: 306,307,308,309 +Tank: + items: iron_sword, potion:8229:2 + armor: 310,311,312,313 +Archer: + items: wood_sword, bow, arrow:256, potion:8197:3, bone + armor: 298,299,300,301 +Chemist: + items: stone_sword, potion:16428:30, potion:16388:8, potion:16389:20, potion:8197:3, potion:8226:1 + armor: 314,315,316,317 +Oddjob: + items: stone_sword, flint_and_steel, netherrack:2, tnt:4, potion:8197:3 + armor: 298,299,300,301 \ No newline at end of file diff --git a/resources/res/config.yml b/resources/res/config.yml index 02c8644..f8ac1a9 100644 --- a/resources/res/config.yml +++ b/resources/res/config.yml @@ -1,113 +1,4 @@ -### MobArena Configuration File -### Please visit the MobArena Wiki here: http://goo.gl/F5TTc for more details -### on how to set up this file. -### Note that you CAN'T use tabs in this file! Always use spaces! - -### GLOBAL SETTINGS -# These settings are arena-independent and count in all arenas. Note that if -# enabled: false in the global settings, MobArena is disabled globally, and -# arena-specific enabled-values do not matter. If true, however, the per-arena -# enabled-values count. -global-settings: - enabled: true - allowed-commands: /list, /pl - update-notification: true - -### CLASS SETUP -# Items can be written as either their data value (numbers) or their Material -# type (names); check the Wiki for links to lists of both. -# The notation is :. If no amount is given, 1 is assumed. Sub- -# types can be created -# Item SUBTYPES (wool/dye colors) are written as ::, -# but here, the amount is REQUIRED, even if it is just 1. -# Note: If you want to specify only one item, but apostrophes around that one -# item, e.g.: items: '278' -# Note: For every bone a class has, one PET WOLF will spawn upon arena start, -# which will assist the player in the arena session. -classes: - Knight: - items: diamond_sword, grilled_pork:2 - armor: 306,307,308,309 - Tank: - items: iron_sword, grilled_pork:3, apple - armor: 310,311,312,313 - Archer: - items: wood_sword, bow, arrow:256, grilled_pork, bone - armor: 298,299,300,301 - Chef: - items: stone_sword, bread:6, grilled_pork:4, mushroom_soup, cake:5 - armor: 314,315,316,317 - Oddjob: - items: stone_sword, flint_and_steel, netherrack:2, tnt:4, pork:3 - armor: 298,299,300,301 - -### ARENA SETUP -# The arena setup is split into 4 different sections per arena: settings, -# waves, rewards and coords (not shown until coords are set up). -# Please refer to the Wiki for descriptions of all these settings. -# Note: The REWARDS use the same notation as the items in the CLASS SETUP -# section. However, only -one- item from the list is picked (at random) when -# the rewards are given. -arenas: - default: - settings: - world: '' - enabled: true - protect: true - entry-fee: '' - logging: true - clear-wave-before-next: false - detonate-creepers: false - detonate-damage: false - lightning: true - auto-equip-armor: true - force-restore: false - soft-restore: false - soft-restore-drops: false - require-empty-inv-join: false - require-empty-inv-spec: false - hellhounds: false - pvp-enabled: false - monster-infight: false - allow-teleporting: false - spectate-on-death: true - share-items-in-arena: true - min-players: 0 - max-players: 0 - max-join-distance: 0 - repair-delay: 5 - first-wave-delay: 5 - wave-interval: 20 - special-modulo: 4 - max-idle-time: 0 - waves: - recurrent: - def1: - type: default - priority: 1 - frequency: 1 - monsters: - zombies: 10 - skeletons: 10 - spiders: 10 - creepers: 10 - wolves: 10 - spec1: - type: special - priority: 2 - frequency: 4 - monsters: - powered-creepers: 10 - zombie-pigmen: 10 - angry-wolves: 10 - humans: 10 - rewards: - waves: - every: - '3': feather, bone, stick - '5': dirt:4, gravel:4, stone:4 - '10': iron_ingot:10, gold_ingot:8 - after: - '7': minecart, storage_minecart, powered_minecart - '13': iron_sword, iron_pickaxe, iron_spade - '16': diamond_sword \ No newline at end of file +# Layout +global-settings: {} +classes: {} +arenas: {} \ No newline at end of file diff --git a/resources/res/global-settings.yml b/resources/res/global-settings.yml new file mode 100644 index 0000000..146e44b --- /dev/null +++ b/resources/res/global-settings.yml @@ -0,0 +1,3 @@ +enabled: true +allowed-commands: /list, /pl +update-notification: true \ No newline at end of file diff --git a/resources/res/magicspells.yml b/resources/res/magicspells.yml index 996d8fb..b33693e 100644 --- a/resources/res/magicspells.yml +++ b/resources/res/magicspells.yml @@ -1,17 +1,16 @@ -# MobArena disabled MagicSpells spells +# MobArena disabled MagicSpells spells # Use this file to disable the MagicSpells spells you don't want your players # to use during arena sessions. Make sure to put a hyphen (-) before every # spell, and try to keep the indentation proper. - # Spells that will be disabled for the entire arena session. disabled-spells: - carpet - blink # Spells that will be disabled only while a boss is present. -disabled-only-on-bosses: +disabled-on-bosses: - purge # Spells that will be disabled only during swarm waves. -disabled-only-on-swarms: +disabled-on-swarms: - purge \ No newline at end of file diff --git a/resources/res/rewards.yml b/resources/res/rewards.yml new file mode 100644 index 0000000..ef24487 --- /dev/null +++ b/resources/res/rewards.yml @@ -0,0 +1,9 @@ +waves: + every: + '3': feather, bone, stick + '5': dirt:4, gravel:4, stone:4 + '10': iron_ingot:10, gold_ingot:8 + after: + '7': minecart, storage_minecart, powered_minecart + '13': iron_sword, iron_pickaxe, iron_spade + '16': diamond_sword \ No newline at end of file diff --git a/resources/res/settings.yml b/resources/res/settings.yml new file mode 100644 index 0000000..a93c5f7 --- /dev/null +++ b/resources/res/settings.yml @@ -0,0 +1,33 @@ +world: '' +enabled: true +protect: true +logging: true +entry-fee: '' +clear-wave-before-next: false +clear-boss-before-next: false +lightning: true +auto-equip-armor: true +soft-restore: false +soft-restore-drops: false +require-empty-inv-join: false +require-empty-inv-spec: false +hellhounds: false +pvp-enabled: false +monster-infight: false +allow-teleporting: false +spectate-on-death: true +auto-respawn: true +share-items-in-arena: true +min-players: 0 +max-players: 0 +max-join-distance: 0 +first-wave-delay: 5 +wave-interval: 15 +final-wave: 0 +monster-limit: 100 +monster-exp: false +keep-exp: true +food-regen: false +lock-food-level: true +spout-class-select: false +player-time-in-arena: world \ No newline at end of file diff --git a/resources/res/waves.yml b/resources/res/waves.yml new file mode 100644 index 0000000..ea946e3 --- /dev/null +++ b/resources/res/waves.yml @@ -0,0 +1,32 @@ +recurrent: + def1: + type: default + priority: 1 + frequency: 1 + monsters: + zombies: 10 + skeletons: 10 + spiders: 10 + creepers: 10 + wolves: 10 + spec1: + type: special + priority: 2 + frequency: 4 + monsters: + powered-creepers: 10 + zombie-pigmen: 10 + angry-wolves: 10 + blazes: 10 +single: + swarm1: + type: swarm + wave: 6 + monster: slime + amount: low + boss1: + type: boss + wave: 10 + monster: cave_spider + health: high + abilities: arrows, root-target, throw-nearby \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/Arena.java b/src/com/garbagemule/MobArena/Arena.java deleted file mode 100644 index d5fcc2e..0000000 --- a/src/com/garbagemule/MobArena/Arena.java +++ /dev/null @@ -1,302 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.*; -import java.util.concurrent.PriorityBlockingQueue; - -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Wolf; -import org.bukkit.inventory.ItemStack; -import org.bukkit.permissions.PermissionAttachment; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.leaderboards.Leaderboard; -import com.garbagemule.MobArena.repairable.Repairable; -import com.garbagemule.MobArena.repairable.RepairableComparator; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.waves.BossWave; -import com.garbagemule.MobArena.waves.Wave; - -public abstract class Arena -{ - private MobArena plugin; - - // Setup fields - protected String name; - protected World world; - protected boolean enabled, protect, running, setup, lobbySetup, autoEquip, forceRestore, softRestore, softRestoreDrops, emptyInvJoin, emptyInvSpec, pvp, monsterInfight, allowWarp; - - protected boolean edit, waveClear, detCreepers, detDamage, lightning, hellhounds, specOnDeath, shareInArena, spoutSelect; - protected Location p1, p2, l1, l2, arenaLoc, lobbyLoc, spectatorLoc; - protected Map spawnpoints, spawnpointsBoss, containers; - protected String logging; - - // Wave/reward/entryfee fields - protected int spawnTaskId, sheepTaskId, waveDelay, waveInterval, specialModulo, spawnMonstersInt, maxIdleTime, finalWave; - protected MASpawnThread spawnThread; - protected Map> everyWaveMap, afterWaveMap; - protected Map classMap; - protected Map> classItems, classArmor; - protected Map> classPerms; - protected Map attachments; - protected List entryFee; - - // Player sets - protected Set arenaPlayers, lobbyPlayers, readyPlayers, specPlayers; - - // Wave stuff - protected TreeSet singleWaves, singleWavesInstance; - protected TreeSet recurrentWaves; - protected BossWave bossWave; - protected Wave currentWave; - - // Arena sets/maps - protected Set hasPaid, rewardedPlayers, notifyPlayers, randoms; - protected Set monsters, explodingSheep, plaguedPigs, madCows; - protected Set blocks; - protected Set pets; - protected Map petMap; - protected LinkedList repairables, containables; - - // Spawn overriding - protected int spawnMonsters; - protected boolean allowMonsters, allowAnimals; - - // Other settings - protected int minPlayers, maxPlayers, joinDistance; - protected Set classes = new HashSet(); - protected Map locations = new HashMap(); - protected Map healthMap = new HashMap(); - protected Map hungerMap = new HashMap(); - protected Map modeMap = new HashMap(); - - // Logging - protected ArenaLog log; - protected Map arenaPlayerMap; - protected Leaderboard leaderboard; - - protected MAListener eventListener; - - protected PriorityBlockingQueue repairQueue; - - public Arena() { - - } - - /** - * Primary constructor. Requires a name and a world. - */ - public Arena(String name, World world) - { - if (world == null) - throw new NullPointerException("[MobArena] ERROR! World for arena '" + name + "' does not exist!"); - - this.name = name; - this.world = world; - plugin = (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena"); - - arenaPlayers = new HashSet(); - lobbyPlayers = new HashSet(); - notifyPlayers = new HashSet(); - readyPlayers = new HashSet(); - specPlayers = new HashSet(); - rewardedPlayers = new HashSet(); - hasPaid = new HashSet(); - monsters = new HashSet(); - explodingSheep = new HashSet(); - plaguedPigs = new HashSet(); - madCows = new HashSet(); - blocks = new HashSet(); - pets = new HashSet(); - petMap = new HashMap(); - classMap = new HashMap(); - randoms = new HashSet(); - repairables = new LinkedList(); - containables = new LinkedList(); - attachments = new HashMap(); - arenaPlayerMap = new HashMap(); - - running = false; - edit = false; - - allowMonsters = world.getAllowMonsters(); - allowAnimals = world.getAllowAnimals(); - spawnMonsters = MAUtils.getSpawnMonsters(world); - - eventListener = new MAListener(this, plugin); - repairQueue = new PriorityBlockingQueue(100, new RepairableComparator()); - } - - public abstract boolean startArena(); - - public abstract boolean endArena(); - - public abstract void forceStart(); - - public abstract void forceEnd(); - - public abstract void playerJoin(Player p, Location loc); - - public abstract void playerReady(Player p); - - public abstract void playerLeave(Player p); - - public abstract void playerDeath(Player p); - - public abstract void playerSpec(Player p, Location loc); - - public abstract void playerKill(Player p); - - public abstract void restoreInvAndGiveRewardsDelayed(final Player p); - - public abstract void restoreInvAndGiveRewards(final Player p); - - public abstract void storePlayerData(Player p, Location loc); - - public abstract void storeContainerContents(); - - public abstract void restoreContainerContents(); - - public abstract void movePlayerToLobby(Player p); - - public abstract void movePlayerToSpec(Player p); - - public abstract void movePlayerToEntry(Player p); - - public abstract void repairBlocks(); - - public abstract void queueRepairable(Repairable r); - - - - /*//////////////////////////////////////////////////////////////////// - // - // Items & Cleanup - // - ////////////////////////////////////////////////////////////////////*/ - - public abstract void assignClass(Player p, String className); - - public abstract void assignRandomClass(Player p); - - public abstract void assignClassPermissions(Player p); - - public abstract void removeClassPermissions(Player p); - - - - /*//////////////////////////////////////////////////////////////////// - // - // Initialization & Checks - // - ////////////////////////////////////////////////////////////////////*/ - - public abstract void load(Config config); - - public abstract void restoreRegion(); - - public abstract void serializeConfig(); - - public abstract void deserializeConfig(); - - public abstract boolean serializeRegion(); - - public abstract boolean deserializeRegion(); - - /** - * Check if a location is inside of the cuboid region - * that p1 and p2 span. - */ - public abstract boolean inRegion(Location loc); - - /** - * Check if a location is inside of the arena region, expanded - * by 'radius' blocks. Used with explosions. - */ - public abstract boolean inRegionRadius(Location loc, int radius); - - - - /*//////////////////////////////////////////////////////////////////// - // - // Getters & Misc - // - ////////////////////////////////////////////////////////////////////*/ - - public abstract boolean inArena(Player p); - - public abstract boolean inLobby(Player p); - - public abstract boolean isEnabled(); - - public abstract boolean isRunning(); - - public abstract boolean isPvpEnabled(); - - public abstract boolean isLightningEnabled(); - - public abstract boolean isBossWave(); - - public abstract String configName(); - - public abstract String arenaName(); - - public abstract MobArena getPlugin(); - - public abstract World getWorld(); - - public abstract Wave getWave(); - - public abstract void setWave(Wave wave); - - public abstract void setBossWave(BossWave bossWave); - - public abstract Set getClasses(); - - public abstract List getAllSpawnpoints(); - - public abstract List getSpawnpoints(); - - public abstract Location getBossSpawnpoint(); - - public abstract int getPlayerCount(); - - public abstract void addBlock(Block b); - - public abstract void addMonster(LivingEntity e); - - public abstract void addExplodingSheep(LivingEntity e); - - public abstract List getAllPlayers(); - - public abstract List getLivingPlayers(); - - public abstract Set getArenaPlayers(); - - public abstract Collection getArenaPlayerSet(); - - public abstract List getArenaPlayerStatistics(Comparator comparator); - - public abstract List getNonreadyPlayers(); - - public abstract Set getMonsters(); - - public abstract Set getPets(); - - public abstract void resetIdleTimer(); - - public abstract void addTrunkAndLeaves(Block b); - - public abstract boolean canAfford(Player p); - - public abstract boolean takeFee(Player p); - - public abstract boolean canJoin(Player p); - - public abstract boolean canSpec(Player p); -} diff --git a/src/com/garbagemule/MobArena/ArenaClass.java b/src/com/garbagemule/MobArena/ArenaClass.java new file mode 100644 index 0000000..0af5dbb --- /dev/null +++ b/src/com/garbagemule/MobArena/ArenaClass.java @@ -0,0 +1,283 @@ +package com.garbagemule.MobArena; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.permissions.PermissionAttachment; + +public class ArenaClass +{ + private String name; + private ItemStack helmet, chestplate, leggings, boots; + private List items, armor; + private Map perms; + private int pets; + + /** + * Create a new, empty arena class with the given name. + * @param name the class name + */ + public ArenaClass(String name) { + this.name = name; + + this.items = new ArrayList(); + this.armor = new ArrayList(4); + this.perms = new HashMap(); + this.pets = 0; + } + + /** + * Get the name of the arena class. + * @return the class name + */ + public String getName() { + return name; + } + + /** + * Get the Material type of the first item in the items list. + * If the items list is empty, the method returns Material.STONE + * @return the type of the first item, or STONE if the list is empty + */ + public Material getLogo() { + if (items.isEmpty()) { + return Material.STONE; + } + return items.get(0).getType(); + } + + /** + * Set the helmet slot for the class. + * @param helmet an item + */ + public void setHelmet(ItemStack helmet) { + this.helmet = helmet; + } + + /** + * Set the chestplate slot for the class. + * @param chestplate an item + */ + public void setChestplate(ItemStack chestplate) { + this.chestplate = chestplate; + } + + /** + * Set the leggings slot for the class. + * @param leggings an item + */ + public void setLeggings(ItemStack leggings) { + this.leggings = leggings; + } + + /** + * Set the boots slot for the class. + * @param boots an item + */ + public void setBoots(ItemStack boots) { + this.boots = boots; + } + + /** + * Add an item to the items list. + * If the item is a weapon-type, its durability will be set to "infinite". + * If the item is a bone, the pets counter will be incremented. + * @param stack an item + */ + public void addItem(ItemStack stack) { + if (stack == null) return; + + if (isWeapon(stack)) { + stack.setDurability(Short.MIN_VALUE); + } + else if (stack.getType() == Material.BONE) { + pets += stack.getAmount(); + } + + items.add(stack); + } + + /** + * Replace the current items list with a new list of all the items in the given list. + * This method uses the addItem() method for each item to ensure consistency. + * @param stacks a list of items + */ + public void setItems(List stacks) { + this.items = new ArrayList(stacks.size()); + for (ItemStack stack : stacks) { + addItem(stack); + } + } + + /** + * Replace the current armor list with the given list. + * @param armor a list of items + */ + public void setArmor(List armor) { + this.armor = armor; + } + + /** + * Grants all of the class items and armor to the given player. + * The normal items will be added to the inventory normally, while the + * armor items will be verified as armor items and placed in their + * appropriate slots. If any specific armor slots are specified, they + * will overwrite any items in the armor list. + * @param p a player + */ + public void grantItems(Player p) { + PlayerInventory inv = p.getInventory(); + + // Fork over the items. + for (ItemStack stack : items) { + inv.addItem(stack); + } + + // Check for legacy armor-node items + if (!armor.isEmpty()) { + for (ItemStack piece : armor) { + ArmorType type = ArmorType.getType(piece); + if (type == null) continue; + + switch (type) { + case HELMET: + inv.setHelmet(piece); + break; + case CHESTPLATE: + inv.setChestplate(piece); + break; + case LEGGINGS: + inv.setLeggings(piece); + break; + case BOOTS: + inv.setBoots(piece); + break; + default: + break; + } + } + } + + // Check type specifics. + if (helmet != null) inv.setHelmet(helmet); + if (chestplate != null) inv.setChestplate(chestplate); + if (leggings != null) inv.setLeggings(leggings); + if (boots != null) inv.setBoots(boots); + } + + /** + * Add a permission value to the class. + * @param perm the permission + * @param value the value + */ + public void addPermission(String perm, boolean value) { + perms.put(perm, value); + } + + /** + * Get an unmodifiable map of permissions and values for the class. + * @return a map of permissions and values + */ + public Map getPermissions() { + return Collections.unmodifiableMap(perms); + } + + /** + * Grant the given player all the permissions of the class. + * All permissions will be attached to a PermissionAttachment object, which + * will be returned to the caller. + * @param plugin a MobArena instance + * @param p a player + * @return the PermissionAttachment with all the permissions + */ + public PermissionAttachment grantPermissions(MobArena plugin, Player p) { + if (perms.isEmpty()) return null; + + PermissionAttachment pa = p.addAttachment(plugin); + + for (Entry entry : perms.entrySet()) { + try { + pa.setPermission(entry.getKey(), entry.getValue()); + } + catch (Exception e) { + String perm = entry.getKey() + ":" + entry.getValue(); + String player = p.getName(); + + plugin.warning("[PERM00] Failed to attach permission '" + perm + "' to player '" + player + " with class " + this.name + + "'.\nPlease verify that your class permissions are well-formed."); + } + } + return pa; + } + + /** + * Get the amount of pets this class is given upon starting the arena. + * @return the number of pets this class has + */ + public int getPetAmount() { + return pets; + } + + /** + * Used by isWeapon() to determine if an ItemStack is a weapon type. + */ + private static int[] weaponTypes = new int[]{256,257,258,267,268,269,270,271,272,273,274,275,276,277,278,279,283,284,285,286,290,291,292,293,294}; + + /** + * Returns true, if the ItemStack appears to be a weapon, in which case + * the addItem() method will set the weapon durability to the absolute + * maximum, as to give them "infinite" durability. + * @param stack an ItemStack + * @return true, if the item is a weapon + */ + private boolean isWeapon(ItemStack stack) { + int id = stack.getTypeId(); + + for (int type : weaponTypes) { + if (id == type) { + return true; + } + } + return false; + } + + /** + * Used by the grantItems() method to determine the armor type of a given + * ItemStack. Armor pieces are auto-equipped. + * Note: This enum is only necessary for backward-compatibility with the + * 'armor'-node. + */ + public enum ArmorType { + HELMET (298,302,306,310,314), + CHESTPLATE (299,303,307,311,315), + LEGGINGS (300,304,308,312,316), + BOOTS (301,305,309,313,317); + + private int[] types; + + private ArmorType(int... types) { + this.types = types; + } + + public static ArmorType getType(ItemStack stack) { + int id = stack.getTypeId(); + + for (ArmorType armorType : ArmorType.values()) { + for (int type : armorType.types) { + if (id == type) { + return armorType; + } + } + } + return null; + } + } +} diff --git a/src/com/garbagemule/MobArena/ArenaImpl.java b/src/com/garbagemule/MobArena/ArenaImpl.java new file mode 100644 index 0000000..f401b40 --- /dev/null +++ b/src/com/garbagemule/MobArena/ArenaImpl.java @@ -0,0 +1,1270 @@ +package com.garbagemule.MobArena; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.PriorityBlockingQueue; + +import org.bukkit.ChatColor; +import org.bukkit.Chunk; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.ContainerBlock; +import org.bukkit.entity.CreatureType; +import org.bukkit.entity.Entity; +import org.bukkit.entity.ExperienceOrb; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.entity.Slime; +import org.bukkit.entity.Vehicle; +import org.bukkit.entity.Wolf; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.potion.PotionEffect; + +import com.garbagemule.MobArena.events.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.leaderboards.Leaderboard; +import com.garbagemule.MobArena.region.ArenaRegion; +import com.garbagemule.MobArena.repairable.*; +import com.garbagemule.MobArena.spout.Spouty; +import com.garbagemule.MobArena.time.Time; +import com.garbagemule.MobArena.time.TimeStrategy; +import com.garbagemule.MobArena.time.TimeStrategyLocked; +import com.garbagemule.MobArena.time.TimeStrategyNull; +import com.garbagemule.MobArena.util.*; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.util.inventory.InventoryManager; +import com.garbagemule.MobArena.util.inventory.InventoryUtils; +import com.garbagemule.MobArena.waves.*; + +public class ArenaImpl implements Arena +{ + // General stuff + private MobArena plugin; + private String name; + private World world; + + // Settings section of the config-file for this arena. + private ConfigSection settings; + + // Run-time settings and critical config settings + private boolean enabled, protect, running, edit; + + // World stuff + private boolean allowMonsters, allowAnimals; + //private Difficulty spawnMonsters; + + // Warps, points and locations + private ArenaRegion region; + private Leaderboard leaderboard; + + // Player stuff + private InventoryManager inventoryManager; + private RewardManager rewardManager; + private Map arenaPlayerMap; + private Map playerData = new HashMap(); + + private Set arenaPlayers, lobbyPlayers, readyPlayers, specPlayers, deadPlayers; + private Set randoms; + + // Classes stuff + private Map classes; + private Map attachments; + + // Blocks and pets + private PriorityBlockingQueue repairQueue; + private Set blocks; + private LinkedList repairables, containables; + + // Monster stuff + private MonsterManager monsterManager; + + // Wave stuff + private WaveManager waveManager; + private Wave currentWave; + private MASpawnThread spawnThread; + private SheepBouncer sheepBouncer; + private Map> everyWaveMap, afterWaveMap; + + // Misc + private ArenaListenerImpl eventListener; + private List entryFee; + //private ArenaLog log; + private TimeStrategy timeStrategy; + + /** + * Primary constructor. Requires a name and a world. + */ + public ArenaImpl(MobArena plugin, Config config, String name, World world) { + if (world == null) + throw new NullPointerException("[MobArena] ERROR! World for arena '" + name + "' does not exist!"); + + this.name = name; + this.world = world; + this.plugin = plugin; + this.settings = new ConfigSection(config, "arenas." + name + ".settings"); + this.region = new ArenaRegion(new ConfigSection(config, "arenas." + name + ".coords"), this); + + this.enabled = settings.getBoolean("enabled", false); + this.protect = settings.getBoolean("protect", true); + this.running = false; + this.edit = false; + + this.inventoryManager = new InventoryManager(this); + this.rewardManager = new RewardManager(this); + + // Warps, points and locations + this.leaderboard = new Leaderboard(plugin, this, region.getLeaderboard()); + + // Player stuff + this.arenaPlayerMap = new HashMap(); + this.arenaPlayers = new HashSet(); + this.lobbyPlayers = new HashSet(); + this.readyPlayers = new HashSet(); + this.specPlayers = new HashSet(); + this.deadPlayers = new HashSet(); + this.randoms = new HashSet(); + + // Classes, items and permissions + this.classes = plugin.getArenaMaster().getClasses(); + this.attachments = new HashMap(); + + // Blocks and pets + this.repairQueue = new PriorityBlockingQueue(100, new RepairableComparator()); + this.blocks = new HashSet(); + this.repairables = new LinkedList(); + this.containables = new LinkedList(); + + // Monster stuff + this.monsterManager = new MonsterManager(); + + // Wave stuff + this.waveManager = new WaveManager(this, config); + this.everyWaveMap = MAUtils.getArenaRewardMap(plugin, config, name, "every"); + this.afterWaveMap = MAUtils.getArenaRewardMap(plugin, config, name, "after"); + + // Misc + this.eventListener = new ArenaListenerImpl(this, plugin); + this.entryFee = ItemParser.parseItems(settings.getString("entry-fee", "")); + this.allowMonsters = world.getAllowMonsters(); + this.allowAnimals = world.getAllowAnimals(); + + String timeString = settings.getString("player-time-in-arena", "world"); + Time time = Enums.getEnumFromString(Time.class, timeString); + this.timeStrategy = (time != null ? new TimeStrategyLocked(time) : new TimeStrategyNull()); + } + + + + /*///////////////////////////////////////////////////////////////////////// + // + // NEW METHODS IN REFACTORING + // + /////////////////////////////////////////////////////////////////////////*/ + + @Override + public ConfigSection getSettings() { + return settings; + } + + @Override + public World getWorld() { + return world; + } + + @Override + public void setWorld(World world) { + this.world = world; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean value) { + enabled = value; + } + + @Override + public boolean isProtected() { + return protect; + } + + @Override + public void setProtected(boolean value) { + protect = value; + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public boolean inEditMode() { + return edit; + } + + @Override + public void setEditMode(boolean value) { + edit = value; + } + + private int getMinPlayers() { + return settings.getInt("min-players"); + } + + private int getMaxPlayers() { + return settings.getInt("max-players"); + } + + private int getJoinDistance() { + return settings.getInt("max-join-distance"); + } + + @Override + public Material getClassLogo(String classname) { + ArenaClass arenaClass = classes.get(classname); + if (arenaClass == null) return Material.STONE; + + return arenaClass.getLogo(); + } + + @Override + public List getEntryFee() { + return entryFee; + } + + @Override + public Set>> getEveryWaveEntrySet() { + return everyWaveMap.entrySet(); + } + + @Override + public List getAfterWaveReward(int wave) { + return afterWaveMap.get(wave); + } + + @Override + public Set getPlayersInArena() { + return Collections.unmodifiableSet(arenaPlayers); + } + + @Override + public Set getPlayersInLobby() { + return Collections.unmodifiableSet(lobbyPlayers); + } + + @Override + public Set getReadyPlayersInLobby() { + return Collections.unmodifiableSet(readyPlayers); + } + + @Override + public Set getSpectators() { + return Collections.unmodifiableSet(specPlayers); + } + + @Override + public MASpawnThread getSpawnThread() { + return spawnThread; + } + + @Override + public WaveManager getWaveManager() { + return waveManager; + } + + /*@Override + public ArenaLog getLog() { + return log; + }*/ + + @Override + public Location getPlayerEntry(Player p) { + PlayerData mp = playerData.get(p); + return (mp != null ? mp.entry() : null); + } + + @Override + public ArenaListenerImpl getEventListener() { + return eventListener; + } + + @Override + public void setLeaderboard(Leaderboard leaderboard) { + this.leaderboard = leaderboard; + } + + @Override + public ArenaPlayer getArenaPlayer(Player p) { + return arenaPlayerMap.get(p); + } + + @Override + public Set getBlocks() { + return blocks; + } + + @Override + public void addBlock(Block b) { + blocks.add(b); + } + + @Override + public boolean removeBlock(Block b) { + return blocks.remove(b); + } + + @Override + public boolean hasPet(Entity e) { + return monsterManager.hasPet(e); + } + + @Override + public void addRepairable(Repairable r) { + repairables.add(r); + } + + @Override + public ArenaRegion getRegion() { + return region; + } + + @Override + public InventoryManager getInventoryManager() { + return inventoryManager; + } + + @Override + public RewardManager getRewardManager() { + return rewardManager; + } + + @Override + public MonsterManager getMonsterManager() { + return monsterManager; + } + + + + + + + + + + + + @Override + public boolean startArena() { + // Sanity-checks + if (running || lobbyPlayers.isEmpty() || !readyPlayers.containsAll(lobbyPlayers)) { + return false; + } + + // Fire the event and check if it's been cancelled. + ArenaStartEvent event = new ArenaStartEvent(this); + plugin.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + + // Store all chest contents. + storeContainerContents(); + + // Populate arenaPlayers and clear the lobby. + arenaPlayers.addAll(lobbyPlayers); + lobbyPlayers.clear(); + readyPlayers.clear(); + + // Assign random classes. + for (Player p : randoms) { + assignRandomClass(p); + } + + // Then check if there are still players left. + if (arenaPlayers.isEmpty()) { + return false; + } + + // Teleport players, give full health, initialize map + for (Player p : arenaPlayers) { + p.teleport(region.getArenaWarp()); + //movePlayerToLocation(p, region.getArenaWarp()); + setHealth(p, 20); + p.setFoodLevel(20); + assignClassPermissions(p); + arenaPlayerMap.get(p).resetStats(); + } + + // Start spawning monsters (must happen before 'running = true;') + startSpawner(); + startBouncingSheep(); + + // Set the boolean. + running = true; + + // Spawn pets (must happen after 'running = true;') + spawnPets(); + + // Start logging + rewardManager.reset(); + //log = new ArenaLog(plugin, this); + //log.start(); + + // Initialize leaderboards and start displaying info. + leaderboard.initialize(); + leaderboard.startTracking(); + + Messenger.tellAll(this, Msg.ARENA_START); + + return true; + } + + @Override + public boolean endArena() { + // Sanity-checks. + if (!running || !arenaPlayers.isEmpty()) { + return false; + } + + // Fire the event and check if it's been cancelled. + ArenaEndEvent event = new ArenaEndEvent(this); + plugin.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + + // Set the running boolean and disable arena if not disabled. + boolean en = enabled; + enabled = false; + running = false; + + // Stop tracking leaderboards + leaderboard.stopTracking(); + leaderboard.update(); + + // Finish logging + //log.end(); + /*if (logging != null) { TODO: FIX LOGGING! + log.saveSessionData(); + log.updateArenaTotals(); + }*/ + //log.clearSessionData(); + + // Stop spawning. + stopSpawner(); + + // Announce and clean arena floor, etc. + Messenger.tellAll(this, Msg.ARENA_END, true); + cleanup(); + + // Restore region. + if (settings.getBoolean("soft-restore", false)) { + restoreRegion(); + } + + // Restore chests + restoreContainerContents(); + + // Restore enabled status. + enabled = en; + + return true; + } + + @Override + public void forceStart() + { + if (running) + return; + + // Set operations. + Set tmp = new HashSet(); + tmp.addAll(lobbyPlayers); + tmp.removeAll(readyPlayers); + + // Force leave. + for (Player p : tmp) { + playerLeave(p); + } + + startArena(); + } + + @Override + public void forceEnd() { + for (Player p : getAllPlayers()) { + playerLeave(p); + } + + cleanup(); + } + + @Override + public boolean playerJoin(Player p, Location loc) + { + // Fire the event and check if it's been cancelled. + ArenaPlayerJoinEvent event = new ArenaPlayerJoinEvent(p, this); + plugin.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + + takeFee(p); + storePlayerData(p, loc); + removePotionEffects(p); + MAUtils.sitPets(p); + setHealth(p, 20); + p.setFoodLevel(20); + p.setGameMode(GameMode.SURVIVAL); + movePlayerToLobby(p); + + arenaPlayerMap.put(p, new ArenaPlayer(p, this, plugin)); + + if (MobArena.hasSpout && settings.getBoolean("spout-class-select")) + Spouty.classSelectionScreen(plugin, this, p); + + Messenger.tellPlayer(p, Msg.JOIN_PLAYER_JOINED); + return true; + } + + @Override + public void playerReady(Player p) + { + readyPlayers.add(p); + + int minPlayers = getMinPlayers(); + if (minPlayers > 0 && lobbyPlayers.size() < minPlayers) + { + Messenger.tellPlayer(p, Msg.LOBBY_NOT_ENOUGH_PLAYERS, "" + minPlayers); + return; + } + + startArena(); + } + + @Override + //@SuppressWarnings("deprecation") + public boolean playerLeave(Player p) + { + // Fire the event and check if it's been cancelled. + ArenaPlayerLeaveEvent event = new ArenaPlayerLeaveEvent(p, this); + plugin.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + + removeClassPermissions(p); + removePotionEffects(p); + + if (inLobby(p) || inArena(p)) { + inventoryManager.clearInventory(p); + inventoryManager.restoreInventory(p); + rewardManager.grantRewards(p); + refund(p); + //p.updateInventory(); + } + else if (inSpec(p)) { + inventoryManager.restoreInventory(p); + //p.updateInventory(); + } + + movePlayerToEntry(p); + discardPlayer(p); + + endArena(); + return true; + } + + @Override + public void playerDeath(Player p) + { + // Fire the event + ArenaPlayerDeathEvent event = new ArenaPlayerDeathEvent(p, this); + plugin.getServer().getPluginManager().callEvent(event); + + arenaPlayers.remove(p); + + if (!settings.getBoolean("auto-respawn", true)) { + deadPlayers.add(p); + endArena(); + return; + } + + p.setHealth(20); + Delays.revivePlayer(plugin, this, p); + endArena(); + } + + @Override + public void playerRespawn(Player p) { + if (settings.getBoolean("auto-respawn", true)) { + return; + } + + deadPlayers.remove(p); + revivePlayer(p); + } + + @Override + @SuppressWarnings("deprecation") + public void revivePlayer(Player p) { + Delays.douse(plugin, p, 1); + removeClassPermissions(p); + removePotionEffects(p); + + if (settings.getBoolean("spectate-on-death", true)) { + movePlayerToSpec(p); + restoreInvAndExp(p); + } else { + restoreInvAndExp(p); + movePlayerToEntry(p); + discardPlayer(p); + } + p.updateInventory(); + } + + @Override + public Location getRespawnLocation(Player p) { + Location l = null; + if (settings.getBoolean("spectate-on-death", true)) { + l = region.getSpecWarp(); + } else { + //l = locations.get(p); + l = playerData.get(p).entry(); + } + return l; + } + + @Override + public void playerSpec(Player p, Location loc) + { + storePlayerData(p, loc); + MAUtils.sitPets(p); + movePlayerToSpec(p); + } + + private void spawnPets() + { + for (Map.Entry entry : arenaPlayerMap.entrySet()) { + ArenaClass arenaClass = entry.getValue().getArenaClass(); + int petAmount = arenaClass.getPetAmount(); + + if (petAmount <= 0) { + continue; + } + + // Remove the bones from the inventory. + Player p = entry.getKey(); + p.getInventory().removeItem(new ItemStack(Material.BONE, petAmount)); + + for (int i = 0; i < petAmount; i++) { + Wolf wolf = (Wolf) world.spawnCreature(p.getLocation(), CreatureType.WOLF); + wolf.setTamed(true); + wolf.setOwner(p); + wolf.setHealth(wolf.getMaxHealth()); + if (settings.getBoolean("hellhounds")) + wolf.setFireTicks(32768); + monsterManager.addPet(wolf); + } + } + } + + private void removePotionEffects(Player p) { + for (PotionEffect effect : p.getActivePotionEffects()) { + p.removePotionEffect(effect.getType()); + } + } + + private void startSpawner() + { + // Set the spawn flags to enable monster spawning. + world.setSpawnFlags(true, true); + //world.setDifficulty(Difficulty.NORMAL); + + // Create a spawner if one doesn't exist, otherwise reset it + if (spawnThread == null) { + spawnThread = new MASpawnThread(plugin, this); + } else { + spawnThread.reset(); + } + + // Schedule it for the initial first wave delay. + scheduleTask(spawnThread, settings.getInt("first-wave-delay", 5) * 20); + } + + /** + * Schedule a Runnable to be executed after the given delay in + * server ticks. The method is used by the MASpawnThread to + * repeatedly spawn new mobs instead of a scheduled repeating + * tasks, as well as the sheep bouncer. + */ + @Override + public void scheduleTask(Runnable r, int delay) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask( + plugin, + r, + delay); + } + + private void stopSpawner() { + world.setSpawnFlags(allowMonsters, allowAnimals); + //world.setDifficulty(spawnMonsters); + } + + private void startBouncingSheep() + { + // Create a new bouncer if necessary. + if (sheepBouncer == null) { + sheepBouncer = new SheepBouncer(this); + } + + // Start bouncing! + scheduleTask(sheepBouncer, settings.getInt("first-wave-delay", 5) * 20); + } + + private void updateChunk(Location loc) + { + if (!arenaPlayers.isEmpty() || !world.getName().equals(loc.getWorld().getName())) + return; + + Chunk chunk = world.getChunkAt(loc); + if (!world.isChunkLoaded(chunk)) + world.loadChunk(chunk); + else + world.refreshChunk(chunk.getX(), chunk.getZ()); + } + + @Override + public void storePlayerData(Player p, Location loc) + { + plugin.getArenaMaster().addPlayer(p, this); + + PlayerData mp = playerData.get(p); + + // If there's no player stored, create a new one! + if (mp == null) { + mp = new PlayerData(p); + playerData.put(p, mp); + } + + // At any rate, update the data. + mp.update(); + + // And update the inventory as well. + inventoryManager.storeInventory(p); + } + + @Override + public void storeContainerContents() + { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, + new Runnable() { + public void run() { + for (Location loc : region.getContainers()) { + BlockState state = world.getBlockAt(loc).getState(); + if (state instanceof ContainerBlock) { + containables.add(new RepairableContainer(state, false)); + } + } + } + }); + } + + @Override + public void restoreContainerContents() + { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, + new Runnable() { + public void run() { + for (Repairable r : containables) { + r.repair(); + } + } + }); + } + + @Override + public void movePlayerToLobby(Player p) + { + updateChunk(region.getLobbyWarp()); + specPlayers.remove(p); // If joining from spec area + lobbyPlayers.add(p); + p.teleport(region.getLobbyWarp()); + timeStrategy.setPlayerTime(p); + //movePlayerToLocation(p, region.getLobbyWarp()); + } + + @Override + public void movePlayerToSpec(Player p) + { + updateChunk(region.getSpecWarp()); + specPlayers.add(p); + p.teleport(region.getSpecWarp()); + timeStrategy.setPlayerTime(p); + //movePlayerToLocation(p, region.getSpecWarp()); + } + + @Override + public void movePlayerToEntry(Player p) + { + Location entry = playerData.get(p).entry(); + if (entry == null || p.isDead()) return; + + updateChunk(entry); + p.teleport(entry); + timeStrategy.resetPlayerTime(p); + //movePlayerToLocation(p, entry); + + p.setGameMode(playerData.get(p).getMode()); + p.addPotionEffects(playerData.get(p).getPotionEffects()); + } + + /*private void movePlayerToLocation(Player p, Location l) { + WorldServer fromWorld = ((CraftWorld) p.getWorld()).getHandle(); + WorldServer toWorld = ((CraftWorld) l.getWorld()).getHandle(); + EntityPlayer player = ((CraftPlayer) p).getHandle(); + + if (fromWorld == toWorld) { + player.netServerHandler.teleport(l); + } + else { + if (player.activeContainer != player.defaultContainer) { + player.closeInventory(); + } + CraftServer cs = ((CraftServer) plugin.getServer()); + cs.getHandle().moveToWorld(player, toWorld.dimension, true, l); + } + }*/ + + private void restoreInvAndExp(Player p) { + inventoryManager.clearInventory(p); + inventoryManager.restoreInventory(p); + rewardManager.grantRewards(p); + + if (!settings.getBoolean("keep-exp", false)) { + playerData.get(p).restoreData(); + } + } + + @Override + public void discardPlayer(Player p) + { + plugin.getArenaMaster().removePlayer(p); + clearPlayer(p); + } + + private void clearPlayer(Player p) + { + // Remove the player data completely. + PlayerData mp = playerData.remove(p); + + // Health must be handled in a certain way because of Heroes + setHealth(p, mp.health()); + + // Put out fire. + Delays.douse(plugin, p, 3); + + // Remove pets. + monsterManager.removePets(p); + + // readyPlayers before lobbyPlayers because of startArena sanity-checks + readyPlayers.remove(p); + specPlayers.remove(p); + arenaPlayers.remove(p); + lobbyPlayers.remove(p); + arenaPlayerMap.remove(p); + } + + private void setHealth(Player p, int health) { + plugin.getHealthStrategy().setHealth(p, health); + } + + @Override + public void repairBlocks() + { + while (!repairQueue.isEmpty()) + repairQueue.poll().repair(); + } + + @Override + public void queueRepairable(Repairable r) + { + repairQueue.add(r); + } + + + + /*//////////////////////////////////////////////////////////////////// + // + // Items & Cleanup + // + ////////////////////////////////////////////////////////////////////*/ + + @Override + public void assignClass(Player p, String className) { + ArenaPlayer arenaPlayer = arenaPlayerMap.get(p); + ArenaClass arenaClass = classes.get(className); + + if (arenaPlayer == null || arenaClass == null) { + return; + } + + inventoryManager.clearInventory(p); + + arenaPlayer.setArenaClass(arenaClass); + arenaClass.grantItems(p); + } + + @Override + public void assignRandomClass(Player p) + { + Random r = new Random(); + List classes = new LinkedList(this.classes.keySet()); + + String className = classes.remove(r.nextInt(classes.size())); + while (!plugin.has(p, "mobarena.classes." + className)) + { + if (classes.isEmpty()) + { + plugin.info("Player '" + p.getName() + "' has no class permissions!"); + playerLeave(p); + return; + } + className = classes.remove(r.nextInt(classes.size())); + } + + assignClass(p, className); + Messenger.tellPlayer(p, Msg.LOBBY_CLASS_PICKED, className); + } + + @Override + public void assignClassPermissions(Player p) + { + PermissionAttachment pa = arenaPlayerMap.get(p).getArenaClass().grantPermissions(plugin, p); + if (pa == null) return; + + attachments.put(p, pa); + p.recalculatePermissions(); + } + + @Override + public void removeClassPermissions(Player p) + { + PermissionAttachment pa = attachments.remove(p); + if (pa == null) return; + + try { + p.removeAttachment(pa); + } + catch (Exception e) { + for (Entry entry : pa.getPermissions().entrySet()) { + String perm = entry.getKey() + ":" + entry.getValue(); + String name = p.getName(); + + plugin.warning("[PERM01] Failed to remove permission attachment '" + perm + "' from player '" + name + + "'.\nThis should not be a big issue, but please verify that the player doesn't have any permissions they shouldn't have."); + } + } + p.recalculatePermissions(); + } + + private void cleanup() { + removeMonsters(); + removeBlocks(); + removeEntities(); + clearPlayers(); + } + + private void removeMonsters() { + monsterManager.clear(); + } + + private void removeBlocks() { + for (Block b : blocks) { + b.setTypeId(0); + } + blocks.clear(); + } + + private void removeEntities() { + List chunks = region.getChunks(); + + for (Chunk c : chunks) { + for (Entity e : c.getEntities()) { + if (!(e instanceof Item || e instanceof Vehicle || e instanceof Slime || e instanceof ExperienceOrb)) { + continue; + } + + if (e != null) { + e.remove(); + } + } + } + } + + private void clearPlayers() { + arenaPlayers.clear(); + arenaPlayerMap.clear(); + lobbyPlayers.clear(); + readyPlayers.clear(); + } + + + + /*//////////////////////////////////////////////////////////////////// + // + // Initialization & Checks + // + ////////////////////////////////////////////////////////////////////*/ + + @Override + public void restoreRegion() + { + Collections.sort(repairables, new RepairableComparator()); + + for (Repairable r : repairables) + r.repair(); + } + + + + /*//////////////////////////////////////////////////////////////////// + // + // Getters & Misc + // + ////////////////////////////////////////////////////////////////////*/ + + @Override + public boolean inArena(Player p) { + return arenaPlayers.contains(p); + } + + @Override + public boolean inLobby(Player p) { + return lobbyPlayers.contains(p); + } + + @Override + public boolean inSpec(Player p) { + return specPlayers.contains(p); + } + + @Override + public boolean isDead(Player p) { + return deadPlayers.contains(p); + } + + @Override + public String configName() + { + return name; + } + + @Override + public String arenaName() + { + return MAUtils.nameConfigToArena(name); + } + + @Override + public MobArena getPlugin() + { + return plugin; + } + + @Override + public Wave getWave() + { + return currentWave; + } + + @Override + public Map getClasses() + { + return classes; + } + + @Override + public int getPlayerCount() + { + return spawnThread.getPlayerCount(); + } + + @Override + public List getAllPlayers() + { + List result = new LinkedList(); + result.addAll(arenaPlayers); + result.addAll(lobbyPlayers); + result.addAll(specPlayers); + + return result; + } + + @Override + public Collection getArenaPlayerSet() + { + return arenaPlayerMap.values(); + } + + /*@Override + public List getArenaPlayerStatistics(Comparator comparator) + { + List list = new ArrayList(); + + for (ArenaPlayer ap : arenaPlayerMap.values()) + list.add(ap.getStats()); + + Collections.sort(list, comparator); + return list; + }*/ + + @Override + public List getNonreadyPlayers() + { + List result = new LinkedList(); + result.addAll(lobbyPlayers); + result.removeAll(readyPlayers); + return result; + } + + @Override + public boolean canAfford(Player p) { + if (entryFee.isEmpty()) return true; + + PlayerInventory inv = p.getInventory(); + for (ItemStack stack : entryFee) { + // Economy money + if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) { + if (!plugin.hasEnough(p, stack.getAmount())) { + return false; + } + } + // Normal stack + else { + if (!inv.contains(stack.getTypeId(), stack.getAmount())) { + return false; + } + } + } + return true; + } + + @Override + public boolean takeFee(Player p) { + if (entryFee.isEmpty()) return true; + + PlayerInventory inv = p.getInventory(); + + // Take some economy money + for (ItemStack stack : InventoryUtils.extractAll(MobArena.ECONOMY_MONEY_ID, entryFee)) { + plugin.takeMoney(p, stack.getAmount()); + } + + // Take any other items + for (ItemStack stack : entryFee) { + inv.removeItem(stack); + } + + Messenger.tellPlayer(p, Msg.JOIN_FEE_PAID.toString(MAUtils.listToString(entryFee, plugin))); + return true; + } + + @Override + public boolean refund(Player p) { + if (entryFee.isEmpty()) return true; + if (!inLobby(p)) return false; + + // Refund economy money + for (ItemStack stack : InventoryUtils.extractAll(MobArena.ECONOMY_MONEY_ID, entryFee)) { + plugin.giveMoney(p, stack.getAmount()); + } + + // Refund other items. + for (ItemStack stack : entryFee) { + if (stack.getTypeId() > 0) { + p.getInventory().addItem(stack); + } + } + return true; + } + + @Override + public boolean canJoin(Player p) { + if (!enabled) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_NOT_ENABLED); + else if (!region.isSetup() || waveManager.getRecurrentWaves().isEmpty()) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_NOT_SETUP); + else if (edit) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_EDIT_MODE); + else if (arenaPlayers.contains(p) || lobbyPlayers.contains(p)) + Messenger.tellPlayer(p, Msg.JOIN_ALREADY_PLAYING); + else if (running) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_IS_RUNNING); + else if (!plugin.has(p, "mobarena.arenas." + configName())) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_PERMISSION); + else if (getMaxPlayers() > 0 && lobbyPlayers.size() >= getMaxPlayers()) + Messenger.tellPlayer(p, Msg.JOIN_PLAYER_LIMIT_REACHED); + else if (getJoinDistance() > 0 && !region.contains(p.getLocation(), getJoinDistance())) + Messenger.tellPlayer(p, Msg.JOIN_TOO_FAR); + else if (settings.getBoolean("require-empty-inv-join", true) && !InventoryManager.hasEmptyInventory(p)) + Messenger.tellPlayer(p, Msg.JOIN_EMPTY_INV); + else if (!canAfford(p)) + Messenger.tellPlayer(p, Msg.JOIN_FEE_REQUIRED, MAUtils.listToString(entryFee, plugin)); + else return true; + + return false; + } + + @Override + public boolean canSpec(Player p) { + if (!enabled) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_NOT_ENABLED); + else if (!region.isSetup()) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_NOT_SETUP); + else if (edit) + Messenger.tellPlayer(p, Msg.JOIN_ARENA_EDIT_MODE); + else if (arenaPlayers.contains(p) || lobbyPlayers.contains(p)) + Messenger.tellPlayer(p, Msg.SPEC_ALREADY_PLAYING); + else if (settings.getBoolean("require-empty-inv-spec", true) && !InventoryManager.hasEmptyInventory(p)) + Messenger.tellPlayer(p, Msg.SPEC_EMPTY_INV); + else if (getJoinDistance() > 0 && !region.contains(p.getLocation(), getJoinDistance())) + Messenger.tellPlayer(p, Msg.JOIN_TOO_FAR); + else return true; + + return false; + } + + /** + * The "perfect equals method" cf. "Object-Oriented Design and Patterns" + * by Cay S. Horstmann. + */ + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null) return false; + if (getClass() != other.getClass()) return false; + + // Arenas must have different names. + if (other instanceof ArenaImpl && ((ArenaImpl)other).name.equals(name)) + return true; + + return false; + } + + @Override + public String toString() { + return ((enabled && region.isSetup()) ? ChatColor.GREEN : ChatColor.GRAY) + configName(); + } +} diff --git a/src/com/garbagemule/MobArena/ArenaListenerImpl.java b/src/com/garbagemule/MobArena/ArenaListenerImpl.java new file mode 100644 index 0000000..e716a5e --- /dev/null +++ b/src/com/garbagemule/MobArena/ArenaListenerImpl.java @@ -0,0 +1,789 @@ +package com.garbagemule.MobArena; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.ContainerBlock; +import org.bukkit.block.Sign; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.entity.Slime; +import org.bukkit.entity.Wolf; +import org.bukkit.event.Event.Result; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockEvent; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; +import org.bukkit.event.entity.EntityTargetEvent.TargetReason; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerAnimationEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.Attachable; +import org.bukkit.material.Bed; +import org.bukkit.material.Door; +import org.bukkit.material.Redstone; + +import com.garbagemule.MobArena.MAUtils; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaListener; +import com.garbagemule.MobArena.leaderboards.Leaderboard; +import com.garbagemule.MobArena.region.ArenaRegion; +import com.garbagemule.MobArena.region.RegionPoint; +import com.garbagemule.MobArena.repairable.*; +import com.garbagemule.MobArena.util.TextUtils; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.waves.MABoss; + +public class ArenaListenerImpl implements ArenaListener +{ + private MobArena plugin; + private Arena arena; + private ArenaRegion region; + private MonsterManager monsters; + + private boolean softRestore, + softRestoreDrops, + protect; + private boolean monsterExp, + monsterInfight, + pvpEnabled, + foodRegen, + lockFoodLevel; + private boolean allowTeleport, + canShare, + allowMonsters; + + private Set banned; + + public ArenaListenerImpl(Arena arena, MobArena plugin) { + this.plugin = plugin; + this.arena = arena; + this.region = arena.getRegion(); + this.monsters = arena.getMonsterManager(); + + /* + * TODO: Figure out if this is really a good idea + It saves needing all + * those methods in Arena.java + It is relatively simple + It would be + * fairly easy to implement an observer pattern - More private fields - + * Uglier code + */ + ConfigSection s = arena.getSettings(); + this.softRestore = s.getBoolean("soft-restore", false); + this.softRestoreDrops = s.getBoolean("soft-restore-drops", false); + this.protect = s.getBoolean("protect", true); + this.monsterExp = s.getBoolean("monster-exp", false); + this.monsterInfight = s.getBoolean("monster-infight", false); + this.pvpEnabled = s.getBoolean("pvp-enabled", false); + this.foodRegen = s.getBoolean("food-regen", false); + this.lockFoodLevel = s.getBoolean("lock-food-level", true); + this.allowTeleport = s.getBoolean("allow-teleporting", false); + this.canShare = s.getBoolean("share-items-in-arena", true); + + this.allowMonsters = arena.getWorld().getAllowMonsters(); + + this.banned = new HashSet(); + } + + public void onBlockBreak(BlockBreakEvent event) { + if (onBlockDestroy(event)) + return; + + event.setCancelled(true); + } + + public void onBlockBurn(BlockBurnEvent event) { + if (onBlockDestroy(event)) + return; + + event.setCancelled(true); + } + + private boolean onBlockDestroy(BlockEvent event) { + if (!arena.getRegion().contains(event.getBlock().getLocation()) || arena.inEditMode() || (!arena.isProtected() && arena.isRunning())) + return true; + + Block b = event.getBlock(); + if (arena.removeBlock(b) || b.getType() == Material.TNT) + return true; + + if (softRestore && arena.isRunning()) { + BlockState state = b.getState(); + + Repairable r = null; + if (state instanceof ContainerBlock) + r = new RepairableContainer(state); + else if (state instanceof Sign) + r = new RepairableSign(state); + else if (state.getData() instanceof Attachable) + r = new RepairableAttachable(state); + else + r = new RepairableBlock(state); + + arena.addRepairable(r); + + if (!softRestoreDrops) + b.setTypeId(0); + + return true; + } + + return false; + } + + public void onBlockPlace(BlockPlaceEvent event) { + Block b = event.getBlock(); + + // If the event didn't happen in the region, or if in edit mode, ignore + if (!arena.getRegion().contains(b.getLocation()) || arena.inEditMode()) { + return; + } + + // If the arena isn't running, or if the player isn't in the arena, + // cancel. + if (!arena.isRunning() || !arena.inArena(event.getPlayer())) { + event.setCancelled(true); + return; + } + + // Otherwise, block was placed during a session. + arena.addBlock(b); + + switch (b.getType()){ + // For doors, add the block just above (so we get both halves) + case WOODEN_DOOR: + case IRON_DOOR_BLOCK: + arena.addBlock(b.getRelative(0, 1, 0)); + break; + } + } + + public void onBlockForm(BlockFormEvent event) { + if (!arena.getRegion().contains(event.getBlock().getLocation())) + return; + + // If a snowman forms some snow on its path, add the block + if (event.getNewState().getType() == Material.SNOW) + arena.addBlock(event.getBlock()); + } + + public void onBlockIgnite(BlockIgniteEvent event) { + if (!arena.getRegion().contains(event.getBlock().getLocation())) + return; + + switch (event.getCause()){ + case LIGHTNING: + case SPREAD: + event.setCancelled(true); + break; + case FLINT_AND_STEEL: + if (arena.isRunning()) + arena.addBlock(event.getBlock().getRelative(BlockFace.UP)); + else + event.setCancelled(true); + break; + default: + break; + } + } + + public void onSignChange(SignChangeEvent event) { + arena.setLeaderboard(new Leaderboard(plugin, arena, event.getBlock().getLocation())); + arena.getRegion().set(RegionPoint.LEADERBOARD, event.getBlock().getLocation()); + + Messenger.tellPlayer(event.getPlayer(), "Leaderboard made. Now set up the stat signs!"); + } + + public void onCreatureSpawn(CreatureSpawnEvent event) { + if (!arena.getRegion().contains(event.getLocation())) { + if (!event.isCancelled()) { + event.setCancelled(!allowMonsters); + } + return; + } + + if (event.getSpawnReason() != SpawnReason.CUSTOM) { + event.setCancelled(true); + return; + } + + LivingEntity entity = (LivingEntity) event.getEntity(); + if (arena.isRunning() && entity instanceof Slime) + monsters.addMonster(entity); + + // If running == true, setCancelled(false), and vice versa. + event.setCancelled(!arena.isRunning()); + } + + public void onEntityExplode(EntityExplodeEvent event) { + if (!monsters.getMonsters().contains(event.getEntity()) && !arena.getRegion().contains(event.getLocation(), 10)) + return; + + monsters.removeMonster(event.getEntity()); + + // Cancel if the arena isn't running + if (!arena.isRunning()) { + event.setCancelled(true); + return; + } + + // Uncancel, just in case. + event.setCancelled(false); + + // If the arena isn't destructible, just clear the blocklist. + if (!softRestore && protect) { + List blocks = new LinkedList(arena.getBlocks()); + event.blockList().retainAll(blocks); + return; + } + + if (!softRestoreDrops) + event.setYield(0); + + // Handle all the blocks in the block list. + for (Block b : event.blockList()) { + BlockState state = b.getState(); + + if (state.getData() instanceof Door && ((Door) state.getData()).isTopHalf()) { + state = b.getRelative(BlockFace.DOWN).getState(); + } + else if (state.getData() instanceof Bed && ((Bed) state.getData()).isHeadOfBed()) { + state = b.getRelative(((Bed) state.getData()).getFacing().getOppositeFace()).getState(); + } + + // Create a Repairable from the block. + Repairable r = null; + if (state instanceof ContainerBlock) + r = new RepairableContainer(state); + else if (state instanceof Sign) + r = new RepairableSign(state); + else if (state.getData() instanceof Bed) + r = new RepairableBed(state); + else if (state.getData() instanceof Door) + r = new RepairableDoor(state); + else if (state.getData() instanceof Attachable || state.getData() instanceof Redstone) + r = new RepairableAttachable(state); + else + r = new RepairableBlock(state); + + // Cakes and liquids should just get removed. If player-placed block, drop as item. + Material mat = state.getType(); + if (mat == Material.CAKE_BLOCK || mat == Material.WATER || mat == Material.LAVA) + arena.removeBlock(b); + else if (arena.removeBlock(b)) + arena.getWorld().dropItemNaturally(b.getLocation(), new ItemStack(state.getTypeId(), 1)); + else if (softRestore) + arena.addRepairable(r); + else + arena.queueRepairable(r); + } + } + + /****************************************************** + * + * DEATH LISTENERS + * + ******************************************************/ + + public void onEntityDeath(EntityDeathEvent event) { + if (event instanceof PlayerDeathEvent) { + onPlayerDeath((PlayerDeathEvent) event, (Player) event.getEntity()); + } + else if (monsters.removeMonster(event.getEntity())) { + onMonsterDeath(event); + } + } + + private void onPlayerDeath(PlayerDeathEvent event, Player player) { + if (arena.inArena(player) || arena.inLobby(player)) { + event.getDrops().clear(); + event.setDroppedExp(0); + event.setKeepLevel(true); + arena.playerDeath(player); + } + } + + public boolean onPlayerRespawn(PlayerRespawnEvent event) { + Player p = event.getPlayer(); + if (!arena.isDead(p)) { + return false; + } + + Location loc = arena.getRespawnLocation(p); + event.setRespawnLocation(loc); + + arena.playerRespawn(p); + return true; + } + + private void onMonsterDeath(EntityDeathEvent event) { + EntityDamageEvent e1 = event.getEntity().getLastDamageCause(); + EntityDamageByEntityEvent e2 = (e1 instanceof EntityDamageByEntityEvent) ? (EntityDamageByEntityEvent) e1 : null; + Entity damager = (e2 != null) ? e2.getDamager() : null; + + // Make sure to grab the owner of a projectile/pet + if (damager instanceof Projectile) { + damager = ((Projectile) damager).getShooter(); + } + else if (damager instanceof Wolf && arena.hasPet(damager)) { + damager = (Player) ((Wolf) damager).getOwner(); + } + + // If the damager was a player, add to kills. + if (damager instanceof Player) { + ArenaPlayer ap = arena.getArenaPlayer((Player) damager); + if (ap != null) { + ArenaPlayerStatistics stats = ap.getStats(); + if (stats != null) { + ap.getStats().inc("kills"); + } + } + } + + if (!monsterExp) + event.setDroppedExp(0); + + event.getDrops().clear(); + + List loot = monsters.getLoot(event.getEntity()); + if (loot != null && !loot.isEmpty()) { + event.getDrops().add(getRandomItem(loot)); + } + + return; + } + + private ItemStack getRandomItem(List stacks) { + return stacks.get((new Random()).nextInt(stacks.size())); + } + + /****************************************************** + * + * DAMAGE LISTENERS + * + ******************************************************/ + + public void onEntityDamage(EntityDamageEvent event) { + Entity damagee = event.getEntity(); + if (!arena.isRunning() || !arena.getRegion().contains(damagee.getLocation())) { + return; + } + + EntityDamageByEntityEvent edbe = (event instanceof EntityDamageByEntityEvent) ? (EntityDamageByEntityEvent) event : null; + Entity damager = null; + + if (edbe != null) { + damager = edbe.getDamager(); + + if (damager instanceof Projectile) { + damager = ((Projectile) damager).getShooter(); + } + else if (damager instanceof Wolf && arena.hasPet(damager)) { + damager = (Player) ((Wolf) damager).getOwner(); + } + } + + // Pet wolf + if (damagee instanceof Wolf && arena.hasPet(damagee)) { + onPetDamage(event, (Wolf) damagee, damager); + } + // Player + else if (damagee instanceof Player) { + onPlayerDamage(event, (Player) damagee, damager); + } + // Boss + else if (monsters.getBossMonsters().contains(damagee)) { + onBossDamage(event, (LivingEntity) damagee, damager); + } + // Regular monster + else if (monsters.getMonsters().contains(damagee)) { + onMonsterDamage(event, damagee, damager); + } + } + + private void onPlayerDamage(EntityDamageEvent event, Player player, Entity damager) { + // Cancel all damage in the lobby and spec area + if (arena.inLobby(player) || arena.inSpec(player)) { + event.setCancelled(true); + return; + } + // If PvP is disabled and damager is a player, cancel damage + else if (arena.inArena(player)) { + if (!pvpEnabled && damager instanceof Player) { + event.setCancelled(true); + return; + } + event.setCancelled(false); + arena.getArenaPlayer(player).getStats().add("dmgTaken", event.getDamage()); + } + } + + private void onPetDamage(EntityDamageEvent event, Wolf pet, Entity damager) { + event.setCancelled(true); + } + + private void onMonsterDamage(EntityDamageEvent event, Entity monster, Entity damager) { + if (damager instanceof Player) { + Player p = (Player) damager; + if (!arena.inArena(p)) { + event.setCancelled(true); + return; + } + + ArenaPlayerStatistics aps = arena.getArenaPlayer(p).getStats(); + aps.add("dmgDone", event.getDamage()); + aps.inc("hits"); + } + else if (damager instanceof Wolf && arena.hasPet(damager)) { + event.setDamage(1); + Player p = (Player) ((Wolf) damager).getOwner(); + ArenaPlayerStatistics aps = arena.getArenaPlayer(p).getStats(); + aps.add("dmgDone", event.getDamage()); + // arena.getArenaPlayer(p).getStats().dmgDone += event.getDamage(); + } + else if (damager instanceof LivingEntity) { + if (!monsterInfight) + event.setCancelled(true); + } + } + + private void onBossDamage(EntityDamageEvent event, LivingEntity monster, Entity damager) { + // Health the boss back up. + monster.setHealth(monster.getMaxHealth()); + + // Damage the underlying MABoss. + MABoss boss = monsters.getBoss(monster); + boss.damage(event.getDamage()); + + // If it died, remove it from the arena. + if (boss.isDead()) { + monsters.removeBoss(monster); + monster.damage(10000); + } + + // And "cancel out" the damage. + event.setDamage(1); + } + + public void onEntityCombust(EntityCombustEvent event) { + if (monsters.getMonsters().contains(event.getEntity())) + event.setCancelled(true); + } + + public void onEntityTarget(EntityTargetEvent event) { + if (!arena.isRunning() || event.isCancelled()) + return; + + if (arena.hasPet(event.getEntity())) { + if (event.getReason() != TargetReason.TARGET_ATTACKED_OWNER && event.getReason() != TargetReason.OWNER_ATTACKED_TARGET) + return; + + if (!(event.getTarget() instanceof Player)) + return; + + // If the target is a player, cancel. + event.setCancelled(true); + } + + else if (monsters.getMonsters().contains(event.getEntity())) { + if (event.getReason() == TargetReason.FORGOT_TARGET) + event.setTarget(MAUtils.getClosestPlayer(plugin, event.getEntity(), arena)); + + else if (event.getReason() == TargetReason.TARGET_DIED) + event.setTarget(MAUtils.getClosestPlayer(plugin, event.getEntity(), arena)); + + else if (event.getReason() == TargetReason.TARGET_ATTACKED_ENTITY) + if (arena.hasPet(event.getTarget())) + event.setCancelled(true); + + else if (event.getReason() == TargetReason.CLOSEST_PLAYER) + if (!arena.inArena((Player) event.getTarget())) + event.setCancelled(true); + } + } + + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + if (arena.getRegion().contains(event.getBlock().getLocation())) + event.setCancelled(true); + } + + public void onEntityRegainHealth(EntityRegainHealthEvent event) { + if (!arena.isRunning()) + return; + + if (!(event.getEntity() instanceof Player) || !arena.inArena((Player) event.getEntity())) + return; + + if (!foodRegen && event.getRegainReason() == RegainReason.SATIATED) { + event.setCancelled(true); + } + } + + public void onFoodLevelChange(FoodLevelChangeEvent event) { + if (!arena.isRunning()) + return; + + if (!(event.getEntity() instanceof Player) || !arena.inArena((Player) event.getEntity())) + return; + + // If the food level is locked, cancel all changes. + if (lockFoodLevel) + event.setCancelled(true); + } + + public void onPlayerAnimation(PlayerAnimationEvent event) { + if (!arena.isRunning() || !arena.inArena(event.getPlayer())) + return; + + arena.getArenaPlayer(event.getPlayer()).getStats().inc("swings"); + } + + public void onPlayerDropItem(PlayerDropItemEvent event) { + Player p = event.getPlayer(); + + // If the player is active in the arena, only cancel if sharing is not + // allowed + if (arena.inArena(p)) { + if (!canShare) { + Messenger.tellPlayer(p, Msg.LOBBY_DROP_ITEM); + event.setCancelled(true); + } + } + + // Else, if the player is in the lobby or a spectator, just cancel + else if (arena.inLobby(p) || arena.inSpec(p)) { + Messenger.tellPlayer(p, Msg.LOBBY_DROP_ITEM); + event.setCancelled(true); + } + + /* + * If the player is not in the arena in any way (as arena player, lobby + * player or a spectator), but they -are- in the region, it must mean + * they are trying to drop items when not allowed + */ + else if (region.contains(p.getLocation())) { + Messenger.tellPlayer(p, Msg.LOBBY_DROP_ITEM); + event.setCancelled(true); + } + + /* + * If the player is in the banned set, it means they got kicked or + * disconnected during a session, meaning they are more than likely + * trying to steal items, if a PlayerDropItemEvent is fired. + */ + else if (banned.contains(p)) { + plugin.warning("Player " + p.getName() + " tried to steal class items!"); + event.setCancelled(true); + } + } + + public void onPlayerBucketEmpty(PlayerBucketEmptyEvent event) { + if (!arena.getReadyPlayersInLobby().contains(event.getPlayer()) && !arena.inArena(event.getPlayer())) + return; + + if (!arena.isRunning()) { + event.getBlockClicked().getRelative(event.getBlockFace()).setTypeId(0); + event.setCancelled(true); + return; + } + + Block liquid = event.getBlockClicked().getRelative(event.getBlockFace()); + arena.addBlock(liquid); + } + + public void onPlayerInteract(PlayerInteractEvent event) { + Player p = event.getPlayer(); + if (arena.inArena(p) || !arena.inLobby(p)) + return; + + // Player is in the lobby, so disallow using items. + Action a = event.getAction(); + if (a == Action.RIGHT_CLICK_AIR || a == Action.RIGHT_CLICK_BLOCK) { + event.setUseItemInHand(Result.DENY); + event.setCancelled(true); + } + + // If there's no block involved, just return. + if (!event.hasBlock()) + return; + + // Iron block + if (event.getClickedBlock().getTypeId() == 42) { + handleReadyBlock(p); + } + // Sign + else if (event.getClickedBlock().getState() instanceof Sign) { + Sign sign = (Sign) event.getClickedBlock().getState(); + handleSign(sign, p); + } + } + + private void handleReadyBlock(Player p) { + if (arena.getArenaPlayer(p).getArenaClass() != null) { + Messenger.tellPlayer(p, Msg.LOBBY_PLAYER_READY); + arena.playerReady(p); + } + else { + Messenger.tellPlayer(p, Msg.LOBBY_PICK_CLASS); + } + } + + private void handleSign(Sign sign, Player p) { + // Check if the first line is a class name. + String className = ChatColor.stripColor(sign.getLine(0)).toLowerCase(); + + if (!arena.getClasses().containsKey(className) && !className.equals("random")) + return; + + // Check for permission. + if (!plugin.has(p, "mobarena.classes." + className) && !className.equals("random")) { + Messenger.tellPlayer(p, Msg.LOBBY_CLASS_PERMISSION); + return; + } + + // Delay the inventory stuff to ensure that right-clicking works. + delayAssignClass(p, className); + } + + private void delayAssignClass(final Player p, final String className) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin,new Runnable() { + public void run() { + arena.assignClass(p, className); + + if (!className.equalsIgnoreCase("random")) + Messenger.tellPlayer(p, Msg.LOBBY_CLASS_PICKED, TextUtils.camelCase(className), arena.getClassLogo(className)); + else + Messenger.tellPlayer(p, Msg.LOBBY_CLASS_RANDOM); + } + }); + } + + public void onPlayerQuit(PlayerQuitEvent event) { + Player p = event.getPlayer(); + if (!arena.isEnabled() || (!arena.inArena(p) && !arena.inLobby(p))) + return; + + arena.playerLeave(p); + banned.add(p); + scheduleUnban(p, 20); + } + + public void onPlayerKick(PlayerKickEvent event) { + Player p = event.getPlayer(); + if (!arena.isEnabled() || (!arena.inArena(p) && !arena.inLobby(p))) { + return; + } + + arena.playerLeave(p); + banned.add(p); + scheduleUnban(p, 20); + } + + private void scheduleUnban(final Player p, int ticks) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + public void run() { + banned.remove(p); + } + }, ticks); + } + + public TeleportResponse onPlayerTeleport(PlayerTeleportEvent event) { + if (!arena.isEnabled() || !region.isSetup() || arena.inEditMode() || allowTeleport) { + return TeleportResponse.IDGAF; + } + + Location to = event.getTo(); + Location from = event.getFrom(); + Player p = event.getPlayer(); + + if (region.contains(from)) { + // Players not in the arena are free to warp out. + if (!arena.inArena(p) && !arena.inLobby(p) && !arena.inSpec(p)) { + return TeleportResponse.ALLOW; + } + + // Covers the case in which both locations are in the arena. + if (region.contains(to) || region.isWarp(to) || to.equals(arena.getPlayerEntry(p))) { + return TeleportResponse.ALLOW; + } + + Messenger.tellPlayer(p, Msg.WARP_FROM_ARENA); + return TeleportResponse.REJECT; + } + else if (region.contains(to)) { + if (region.isWarp(from) || region.isWarp(to) || to.equals(arena.getPlayerEntry(p))) { + return TeleportResponse.ALLOW; + } + + Messenger.tellPlayer(p, Msg.WARP_TO_ARENA); + return TeleportResponse.REJECT; + } + + return TeleportResponse.IDGAF; + } + + public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + Player p = event.getPlayer(); + + if (event.isCancelled() || (!arena.inArena(p) && !arena.inLobby(p))) { + return; + } + + // This is safe, because commands will always have at least one element. + String base = event.getMessage().split(" ")[0]; + + // Check if the entire base command is allowed. + if (plugin.getArenaMaster().isAllowed(base)) { + return; + } + + // If not, check if the specific command is allowed. + String noslash = event.getMessage().substring(1); + if (plugin.getArenaMaster().isAllowed(noslash)) { + return; + } + + // This is dirty, but it ensures that commands are indeed blocked. + event.setMessage("/"); + + // Cancel the event regardless. + event.setCancelled(true); + Messenger.tellPlayer(p, Msg.MISC_COMMAND_NOT_ALLOWED); + } +} diff --git a/src/com/garbagemule/MobArena/ArenaLog.java b/src/com/garbagemule/MobArena/ArenaLog.java deleted file mode 100644 index 0c78f57..0000000 --- a/src/com/garbagemule/MobArena/ArenaLog.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.garbagemule.MobArena; - -import java.sql.Timestamp; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.util.data.PlainText; -import com.garbagemule.MobArena.util.data.Totals; -import com.garbagemule.MobArena.util.data.XML; -import com.garbagemule.MobArena.util.data.YAML; - -public class ArenaLog -{ - public MobArena plugin; - public Arena arena; - public Timestamp startTime, endTime; - public int lastWave; - - public Map players; - public Map distribution; - - /** - * Create a new ArenaLog. - * @param plugin MobArena instance - * @param arena The arena - */ - public ArenaLog(MobArena plugin, Arena arena) - { - this.plugin = plugin; - this.arena = arena; - this.players = new HashMap(arena.arenaPlayers.size()); - this.distribution = new HashMap(arena.classes.size()); - } - - /** - * Start logging by creating ArenaPlayer objects and recording - * the class distributions and the start time. - */ - public void start() - { - // Populate the data maps - populatePlayerMap(); - populateDistributionMap(); - - // Grab the current timestamp - startTime = new Timestamp((new Date()).getTime()); - } - - /** - * End logging by recording the last wave and the end time. - */ - public void end() - { - lastWave = arena.spawnThread.getWave() - 1; - endTime = new Timestamp((new Date()).getTime()); - } - - /** - * Map players to ArenaPlayer objects. - */ - private void populatePlayerMap() - { - for (Player p : arena.arenaPlayers) - players.put(p, new ArenaPlayer(p, arena, plugin)); - } - - /** - * Map classes to amounts of players playing as the classes. - */ - private void populateDistributionMap() - { - // Initialize the map - for (String c : arena.classes) - distribution.put(c,0); - - // Count occurrences - for (String c : arena.classMap.values()) - distribution.put(c,distribution.get(c) + 1); - } - - /** - * Save the data of the current session according to the logging type. - */ - public void saveSessionData() - { - if (arena.logging.equals("xml")) - XML.saveSessionData(this); - else if (arena.logging.equals("yml") || arena.logging.equals("yaml")) - YAML.saveSessionData(this); - else - PlainText.saveSessionData(this); - //CSV.saveSessionData(this, plugin); - //YAML.saveSessionData(this); - } - - /** - * Update the totals-file - */ - public void updateArenaTotals() - { - Totals.updateArenaTotals(this); - - if (arena.logging.equals("xml")) - XML.updateArenaTotals(this); - //PlainText.updateArenaTotals(this); - } - - /** - * Clear maps. - */ - public void clearSessionData() - { - players.clear(); - distribution.clear(); - } - - public Arena getArena() { return arena; } - public Timestamp getStartTime() { return startTime; } - public Timestamp getEndTime() { return endTime; } - public String getDuration() { return MAUtils.getDuration(endTime.getTime() - startTime.getTime()); } - public long getDurationLong() { return endTime.getTime() - startTime.getTime(); } - public int getLastWave() { return lastWave; } - - public void playerKill(Player p) - { - players.get(p).getStats().kills++; - } - - public void playerDamager(Player p, int damage) - { - players.get(p).getStats().dmgDone += damage; - } - - public void playerDamagee(Player p, int damage) - { - players.get(p).getStats().dmgTaken += damage; - } -} diff --git a/src/com/garbagemule/MobArena/ArenaMaster.java b/src/com/garbagemule/MobArena/ArenaMaster.java deleted file mode 100644 index 537fa4b..0000000 --- a/src/com/garbagemule/MobArena/ArenaMaster.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -//import org.bukkit.util.config.Configuration; - -//import com.garbagemule.ArenaPlugin.Master; - -public abstract class ArenaMaster //implements Master -{ - protected Arena selectedArena; - - // Settings - protected boolean enabled, updateNotify; - - // Classes - protected Set classes; - protected Map> classItems, classArmor; - protected Map> classPerms; - //protected Map>> classBonuses; - protected Map arenaMap; - - // Location map - protected Map locations = new HashMap(); - - // Arena list - protected List arenas; - - // Listeners - protected Set listeners = new HashSet(); - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Arena getters - // - /////////////////////////////////////////////////////////////////////////*/ - - - public abstract List getEnabledArenas(); - - public abstract List getPermittedArenas(Player p); - - public abstract List getEnabledAndPermittedArenas(Player p); - - public abstract Arena getArenaAtLocation(Location loc); - - public abstract List getArenasInWorld(World world); - - public abstract List getAllPlayers(); - - public abstract List getAllPlayersInArena(String arenaName); - - public abstract List getAllLivingPlayers(); - - public abstract List getLivingPlayersInArena(String arenaName); - - public abstract Arena getArenaWithPlayer(Player p); - - public abstract Arena getArenaWithPlayer(String playerName); - - public abstract Arena getArenaWithSpectator(Player p); - - public abstract Arena getArenaWithMonster(Entity e); - - public abstract Arena getArenaWithPet(Entity e); - - public abstract Arena getArenaWithName(String configName); - - public abstract Arena getArenaWithName(Collection arenas, String configName); - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Initialization - // - /////////////////////////////////////////////////////////////////////////*/ - - public abstract void initialize(); - - /** - * Load the global settings. - */ - public abstract void loadSettings(); - - /** - * Load all class-related stuff. - */ - public abstract void loadClasses(); - - /** - * Load all arena-related stuff. - */ - public abstract void loadArenas(); - - public abstract Arena createArenaNode(String configName, World world); - - public abstract void removeArenaNode(String configName); - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Update and serialization methods - // - /////////////////////////////////////////////////////////////////////////*/ - - /** - * Update one, two or all three of global settings, classes - * and arenas (arenas with deserialization). - */ - public abstract void update(boolean settings, boolean classes, boolean arenalist); - - /** - * Serialize the global settings. - */ - public abstract void serializeSettings(); - - /** - * Serialize all arena configs. - */ - public abstract void serializeArenas(); - - /** - * Deserialize all arena configs. Updates the arena list to - * include only the current arenas (not ones added in the - * actual file) that are also in the config-file. - */ - public abstract void deserializeArenas(); - - public abstract void updateSettings(); - public abstract void updateClasses(); - public abstract void updateArenas(); - public abstract void updateAll(); -} diff --git a/src/com/garbagemule/MobArena/ArenaMasterImpl.java b/src/com/garbagemule/MobArena/ArenaMasterImpl.java new file mode 100644 index 0000000..7a61223 --- /dev/null +++ b/src/com/garbagemule/MobArena/ArenaMasterImpl.java @@ -0,0 +1,591 @@ +package com.garbagemule.MobArena; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; + +import com.garbagemule.MobArena.ArenaImpl; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.ArenaClass.ArmorType; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.ItemParser; +import com.garbagemule.MobArena.util.TextUtils; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.util.config.ConfigUtils; + +public class ArenaMasterImpl implements ArenaMaster +{ + private MobArena plugin; + private Config config; + + private List arenas; + private Map arenaMap; + private Arena selectedArena; + + private Map classes; + + private Set allowedCommands; + + /** + * Default constructor. + */ + public ArenaMasterImpl(MobArena plugin) { + this.plugin = plugin; + this.config = plugin.getMAConfig(); + + this.arenas = new LinkedList(); + this.arenaMap = new HashMap(); + + this.classes = new HashMap(); + + this.allowedCommands = new HashSet(); + } + + /* + * ///////////////////////////////////////////////////////////////////////// + * // // NEW METHODS IN REFACTORING // + * ///////////////////////////////////////////////////////////////////////// + */ + + public MobArena getPlugin() { + return plugin; + } + + public boolean isEnabled() { + return config.getBoolean("global-settings.enabled", true); + } + + public void setEnabled(boolean value) { + config.set("global-settings.enabled", value); + } + + public boolean notifyOnUpdates() { + return config.getBoolean("global-settings.update-notification", false); + } + + public Arena getSelectedArena() { + return selectedArena; + } + + public void setSelectedArena(Arena arena) { + selectedArena = arena; + } + + public List getArenas() { + return arenas; + } + + public Map getClasses() { + return classes; + } + + public void addPlayer(Player p, Arena arena) { + arenaMap.put(p, arena); + } + + public Arena removePlayer(Player p) { + return arenaMap.remove(p); + } + + public void resetArenaMap() { + arenaMap.clear(); + } + + public boolean isAllowed(String command) { + return allowedCommands.contains(command); + } + + /* + * ///////////////////////////////////////////////////////////////////////// + * // // Arena getters // + * ///////////////////////////////////////////////////////////////////////// + */ + + public List getEnabledArenas() { + List result = new LinkedList(); + for (Arena arena : arenas) + if (arena.isEnabled()) + result.add(arena); + return result; + } + + public List getPermittedArenas(Player p) { + List result = new LinkedList(); + for (Arena arena : arenas) + if (plugin.has(p, "mobarena.arenas." + arena.configName())) + result.add(arena); + return result; + } + + public List getEnabledAndPermittedArenas(Player p) { + List result = new LinkedList(); + for (Arena arena : arenas) + if (arena.isEnabled() && plugin.has(p, "mobarena.arenas." + arena.configName())) + result.add(arena); + return result; + } + + public Arena getArenaAtLocation(Location loc) { + for (Arena arena : arenas) + if (arena.getRegion().contains(loc)) + return arena; + return null; + } + + public List getArenasInWorld(World world) { + List result = new LinkedList(); + for (Arena arena : arenas) + if (arena.getWorld().equals(world)) + result.add(arena); + return result; + } + + public List getAllPlayers() { + List result = new LinkedList(); + for (Arena arena : arenas) + result.addAll(arena.getAllPlayers()); + return result; + } + + public List getAllPlayersInArena(String arenaName) { + Arena arena = getArenaWithName(arenaName); + return (arena != null) ? new LinkedList(arena.getPlayersInArena()) : new LinkedList(); + } + + public List getAllLivingPlayers() { + List result = new LinkedList(); + for (Arena arena : arenas) + result.addAll(arena.getPlayersInArena()); + return result; + } + + public List getLivingPlayersInArena(String arenaName) { + Arena arena = getArenaWithName(arenaName); + return (arena != null) ? new LinkedList(arena.getPlayersInArena()) : new LinkedList(); + } + + public Arena getArenaWithPlayer(Player p) { + return arenaMap.get(p); + } + + public Arena getArenaWithPlayer(String playerName) { + return arenaMap.get(plugin.getServer().getPlayer(playerName)); + } + + public Arena getArenaWithSpectator(Player p) { + for (Arena arena : arenas) { + if (arena.getSpectators().contains(p)) + return arena; + } + return null; + } + + public Arena getArenaWithMonster(Entity e) { + for (Arena arena : arenas) + if (arena.getMonsterManager().getMonsters().contains(e)) + return arena; + return null; + } + + public Arena getArenaWithPet(Entity e) { + for (Arena arena : arenas) + if (arena.hasPet(e)) + return arena; + return null; + } + + public Arena getArenaWithName(String configName) { + return getArenaWithName(this.arenas, configName); + } + + public Arena getArenaWithName(Collection arenas, String configName) { + for (Arena arena : arenas) + if (arena.configName().equals(configName)) + return arena; + return null; + } + + /* + * ///////////////////////////////////////////////////////////////////////// + * // // Initialization // + * ///////////////////////////////////////////////////////////////////////// + */ + + public void initialize() { + config.load(); + loadSettings(); + loadClasses(); + loadArenas(); + config.save(); + } + + /** + * Load the global settings. + */ + public void loadSettings() { + ConfigUtils.replaceAllNodes(plugin, config, "global-settings", "global-settings.yml"); + ConfigSection section = config.getConfigSection("global-settings"); + + // Grab the commands string + String cmds = section.getString("allowed-commands", ""); + + // Split by commas + String[] parts = cmds.split(","); + + // Add in the /ma command. + allowedCommands.add("/ma"); + + // Add in each command + for (String part : parts) { + allowedCommands.add(part.trim().toLowerCase()); + } + } + + /** + * Load all class-related stuff. + */ + public void loadClasses() { + Set classNames = config.getKeys("classes"); + + // If no classes were found, load the defaults. + if (classNames == null || classNames.isEmpty()) { + loadDefaultClasses(); + classNames = config.getKeys("classes"); + } + + // Establish the map. + classes = new HashMap(); + + // Load each individual class. + for (String className : classNames) { + loadClass(className.toLowerCase()); + } + } + + /** + * Loads the classes in res/classes.yml into the config-file. + */ + public void loadDefaultClasses() { + ConfigUtils.addMissingNodes(plugin, config, "classes", "classes.yml"); + } + + /** + * Helper method for loading a single class. + */ + private ArenaClass loadClass(String className) { + // Grab the class section. + ConfigSection section = config.getConfigSection("classes." + TextUtils.camelCase(className)); + + // If the section doesn't exist, the class doesn't either. + if (section == null) { + plugin.error("Failed to load class '" + TextUtils.camelCase(className) + "'."); + return null; + } + + ArenaClass arenaClass = new ArenaClass(className); + + // Parse the items-node + String items = section.getString("items", ""); + if (!items.equals("")) { + List stacks = ItemParser.parseItems(items); + arenaClass.setItems(stacks); + } + + // And the legacy armor-node + String armor = section.getString("armor", ""); + if (!armor.equals("")) { + List stacks = ItemParser.parseItems(armor); + arenaClass.setArmor(stacks); + } + + // Get armor strings + String head = section.getString("helmet", null); + String chest = section.getString("chestplate", null); + String legs = section.getString("leggings", null); + String feet = section.getString("boots", null); + + // Parse to ItemStacks + ItemStack helmet = ItemParser.parseItem(head); + ItemStack chestplate = ItemParser.parseItem(chest); + ItemStack leggings = ItemParser.parseItem(legs); + ItemStack boots = ItemParser.parseItem(feet); + + // Set in ArenaClass + arenaClass.setHelmet(helmet); + arenaClass.setChestplate(chestplate); + arenaClass.setLeggings(leggings); + arenaClass.setBoots(boots); + + // Per-class permissions + loadClassPermissions(arenaClass, section); + + // Register the permission. + registerPermission("mobarena.classes." + className, PermissionDefault.TRUE).addParent("mobarena.classes", true); + + // Finally add the class to the classes map. + classes.put(className, arenaClass); + return arenaClass; + } + + private void loadClassPermissions(ArenaClass arenaClass, ConfigSection section) { + List perms = section.getStringList("permissions", null); + if (perms.isEmpty()) + return; + + for (String perm : perms) { + // If the permission starts with - or ^, it must be revoked. + boolean value = true; + if (perm.startsWith("-") || perm.startsWith("^")) { + perm = perm.substring(1).trim(); + value = false; + } + arenaClass.addPermission(perm, value); + } + } + + public ArenaClass createClassNode(String className, PlayerInventory inv, boolean safe) { + String path = "classes." + TextUtils.camelCase(className); + if (safe && config.getConfigSection(path) != null) { + return null; + } + + // Create the node. + config.set(path, ""); + + // Grab the section. + ConfigSection section = config.getConfigSection(path); + + // Take the current items and armor. + section.set("items", ItemParser.parseString(inv.getContents())); + section.set("armor", ItemParser.parseString(inv.getArmorContents())); + + // If the helmet isn't a real helmet, set it explicitly. + ItemStack helmet = inv.getHelmet(); + if (helmet.getTypeId() != 0 && ArmorType.getType(helmet) != ArmorType.HELMET) { + section.set("helmet", ItemParser.parseString(helmet)); + } + + // Save changes. + config.save(); + + // Load the class + return loadClass(className.toLowerCase()); + } + + public void removeClassNode(String className) { + if (!classes.containsKey(className.toLowerCase())) + throw new IllegalArgumentException("Class does not exist!"); + + // Remove the class from the config-file and save it. + config.remove("classes." + className); + config.save(); + + // Remove the class from the map. + classes.remove(className.toLowerCase()); + + unregisterPermission("mobarena.arenas." + className.toLowerCase()); + } + + public boolean addClassPermission(String className, String perm) { + return addRemoveClassPermission(className, perm, true); + } + + public boolean removeClassPermission(String className, String perm) { + return addRemoveClassPermission(className, perm, false); + } + + private boolean addRemoveClassPermission(String className, String perm, boolean add) { + String path = "classes." + TextUtils.camelCase(className); + if (config.getConfigSection(path) == null) + return false; + + // Grab the class section + ConfigSection section = config.getConfigSection(path); + + // Get any previous nodes + List nodes = section.getStringList("permissions", null); + if (nodes.contains(perm)) + return false; + + // Add or remove. + if (add) { + removeContradictions(nodes, perm); + nodes.add(perm); + } + else { + nodes.remove(perm); + } + + // Replace the set. + section.set("permissions", nodes); + config.save(); + + // Reload the class. + loadClass(className); + return true; + } + + /** + * Removes any nodes that would contradict the permission, e.g. if the node + * 'mobarena.use' is in the set, and the perm node is '-mobarena.use', the + * '-mobarena.use' node is removed as to not contradict the new + * 'mobarena.use' node. + */ + private void removeContradictions(List nodes, String perm) { + if (perm.startsWith("^") || perm.startsWith("-")) { + nodes.remove(perm.substring(1).trim()); + } + else { + nodes.remove("^" + perm); + nodes.remove("-" + perm); + } + } + + /** + * Load all arena-related stuff. + */ + public void loadArenas() { + Set arenaNames = config.getKeys("arenas"); + + // If no arenas were found, create a default node. + if (arenaNames == null || arenaNames.isEmpty()) { + createArenaNode("default", plugin.getServer().getWorlds().get(0)); + arenaNames = config.getKeys("arenas"); + } + + // Establish the list. + arenas = new LinkedList(); + + for (String arenaName : arenaNames) { + loadArena(arenaName); + } + + selectedArena = arenas.get(0); + } + + private Arena loadArena(String arenaName) { + String path = "arenas." + arenaName; + String worldName = config.getString(path + ".settings.world", ""); + World world = null; + + // If a string was found, try to fetch the world from the server. + if (!worldName.equals("")) { + world = plugin.getServer().getWorld(worldName); + + if (world == null) { + plugin.error("The world '" + worldName + "' for arena '" + arenaName + "' does not exist!"); + return null; + } + } + // Otherwise, use the default world. + else { + world = plugin.getServer().getWorlds().get(0); + plugin.warning("Could not find the world for arena '" + arenaName + "'. Using default world ('" + world.getName() + "')! Check the config-file!"); + } + + // Assert all settings nodes. + ConfigUtils.replaceAllNodes(plugin, config, path + ".settings", "settings.yml"); + + // Create an Arena with the name and world. + Arena arena = new ArenaImpl(plugin, config, arenaName, world); + + // Register the permission + registerPermission("mobarena.arenas." + arenaName, PermissionDefault.TRUE); + + // Finally, add it to the arena list. + arenas.add(arena); + return arena; + } + + public Arena createArenaNode(String arenaName, World world) { + String path = "arenas." + arenaName; + if (config.getConfigSection(path) != null) + throw new IllegalArgumentException("Arena already exists!"); + + // Extract the default settings and update the world-node. + ConfigUtils.replaceAllNodes(plugin, config, path + ".settings", "settings.yml"); + config.set(path + ".settings.world", world.getName()); + + // Extract the default waves. + ConfigUtils.replaceAllNodes(plugin, config, path + ".waves", "waves.yml"); + + // Extract the default rewards. + ConfigUtils.replaceAllNodes(plugin, config, path + ".rewards", "rewards.yml"); + + // Save the changes. + config.save(); + + // Load the arena + return loadArena(arenaName); + } + + public void removeArenaNode(Arena arena) { + // Remove the arena from the config-file and save it. + config.remove("arenas." + arena.configName()); + config.save(); + + // Remove the arena from the list. + arenas.remove(arena); + + unregisterPermission("mobarena.arenas." + arena.configName()); + } + + public void reloadConfig() { + boolean wasEnabled = isEnabled(); + + // If MobArena was enabled, disable it before updating. + if (wasEnabled) { + setEnabled(false); + } + + for (Arena a : arenas) { + a.forceEnd(); + } + + config.load(); + loadSettings(); + loadClasses(); + loadArenas(); + + // If MobArena was enabled, re-enable it after updating. + if (wasEnabled) { + setEnabled(true); + } + } + + public void saveConfig() { + config.save(); + } + + private Permission registerPermission(String permString, PermissionDefault value) { + PluginManager pm = plugin.getServer().getPluginManager(); + + Permission perm = pm.getPermission(permString); + if (perm == null) { + perm = new Permission(permString); + perm.setDefault(value); + pm.addPermission(perm); + } + return perm; + } + + private void unregisterPermission(String s) { + plugin.getServer().getPluginManager().removePermission(s); + } +} diff --git a/src/com/garbagemule/MobArena/ArenaMasterStandard.java b/src/com/garbagemule/MobArena/ArenaMasterStandard.java deleted file mode 100644 index e89fff6..0000000 --- a/src/com/garbagemule/MobArena/ArenaMasterStandard.java +++ /dev/null @@ -1,388 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.util.Config; - -public class ArenaMasterStandard extends ArenaMaster -{ - private MobArena plugin; - private Config config; - - - - /** - * Default constructor. - */ - public ArenaMasterStandard(MobArenaPlugin instance) - { - plugin = (MobArena) instance; - config = plugin.getMAConfig(); - arenas = new LinkedList(); - arenaMap = new HashMap(); - } - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Arena getters - // - /////////////////////////////////////////////////////////////////////////*/ - - public List getEnabledArenas() - { - List result = new LinkedList(); - for (Arena arena : arenas) - if (arena.enabled) - result.add(arena); - return result; - } - - public List getPermittedArenas(Player p) - { - List result = new LinkedList(); - for (Arena arena : arenas) - if (plugin.has(p, "mobarena.arenas." + arena.configName())) - result.add(arena); - return result; - } - - public List getEnabledAndPermittedArenas(Player p) - { - List result = new LinkedList(); - for (Arena arena : arenas) - if (arena.enabled && plugin.has(p, "mobarena.arenas." + arena.configName())) - result.add(arena); - return result; - } - - public Arena getArenaAtLocation(Location loc) - { - for (Arena arena : arenas) - if (arena.inRegion(loc)) - return arena; - return null; - } - - public List getArenasInWorld(World world) - { - List result = new LinkedList(); - for (Arena arena : arenas) - if (arena.world.equals(world)) - result.add(arena); - return result; - } - - public List getAllPlayers() - { - List result = new LinkedList(); - for (Arena arena : arenas) - result.addAll(arena.getAllPlayers()); - return result; - } - - public List getAllPlayersInArena(String arenaName) - { - Arena arena = getArenaWithName(arenaName); - return (arena != null) ? arena.getLivingPlayers() : new LinkedList(); - } - - public List getAllLivingPlayers() - { - List result = new LinkedList(); - for (Arena arena : arenas) - result.addAll(arena.getLivingPlayers()); - return result; - } - - public List getLivingPlayersInArena(String arenaName) - { - Arena arena = getArenaWithName(arenaName); - return (arena != null) ? arena.getLivingPlayers() : new LinkedList(); - } - - public Arena getArenaWithPlayer(Player p) - { - return arenaMap.get(p); - } - - public Arena getArenaWithPlayer(String playerName) - { - return arenaMap.get(Bukkit.getServer().getPlayer(playerName)); - } - - public Arena getArenaWithSpectator(Player p) - { - for (Arena arena : arenas) - { - if (arena.specPlayers.contains(p)) - return arena; - } - return null; - } - - public Arena getArenaWithMonster(Entity e) - { - for (Arena arena : arenas) - if (arena.monsters.contains(e)) - return arena; - return null; - } - - public Arena getArenaWithPet(Entity e) - { - for (Arena arena : arenas) - if (arena.pets.contains(e)) - return arena; - return null; - } - - public Arena getArenaWithName(String configName) - { - return getArenaWithName(this.arenas, configName); - } - - public Arena getArenaWithName(Collection arenas, String configName) - { - for (Arena arena : arenas) - if (arena.configName().equals(configName)) - return arena; - return null; - } - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Initialization - // - /////////////////////////////////////////////////////////////////////////*/ - - public void initialize() - { - config.load(); - loadSettings(); - loadClasses(); - loadArenas(); - config.save(); - } - - /** - * Load the global settings. - */ - public void loadSettings() - { - if (config.getKeys("global-settings") == null) - { - config.setProperty("global-settings.enabled", true); - config.setProperty("global-settings.update-notification", true); - } - - enabled = config.getBoolean("global-settings.enabled", true); - updateNotify = config.getBoolean("global-settings.update-notification", true); - } - - /** - * Load all class-related stuff. - */ - public void loadClasses() - { - classes = config.getKeys("classes"); - if (classes == null) - { - config.setProperty("classes.Archer.items", "wood_sword, bow, arrow:128, grilled_pork"); - config.setProperty("classes.Archer.armor", "298,299,300,301"); - config.setProperty("classes.Knight.items", "diamond_sword, grilled_pork:2"); - config.setProperty("classes.Knight.armor", "306,307,308,309"); - config.setProperty("classes.Tank.items", "iron_sword, grilled_pork:3, apple"); - config.setProperty("classes.Tank.armor", "310,311,312,313"); - config.setProperty("classes.Oddjob.items", "stone_sword, flint_and_steel, netherrack:2, wood_pickaxe, tnt:4, fishing_rod, apple, grilled_pork:3"); - config.setProperty("classes.Oddjob.armor", "298,299,300,301"); - config.setProperty("classes.Chef.items", "stone_sword, bread:6, grilled_pork:4, mushroom_soup, cake:3, cookie:12"); - config.setProperty("classes.Chef.armor", "314,315,316,317"); - classes = config.getKeys("classes"); - } - classItems = MAUtils.getClassItems(config, "items"); - classArmor = MAUtils.getClassItems(config, "armor"); - classPerms = MAUtils.getClassPerms(config); - } - - /** - * Load all arena-related stuff. - */ - public void loadArenas() - { - arenas = new LinkedList(); - - if (config.getKeys("arenas") == null) - createArenaNode("default", Bukkit.getServer().getWorlds().get(0)); - - for (String configName : config.getKeys("arenas")) - { - String arenaPath = "arenas." + configName + "."; - String worldName = config.getString(arenaPath + "settings.world", null); - World world; - if (worldName == null || worldName.equals("")) - { - MobArena.warning("Could not find the world for arena '" + configName + "'. Using default world! Check the config-file!"); - world = Bukkit.getServer().getWorlds().get(0); - } - else world = Bukkit.getServer().getWorld(worldName); - - Arena arena = new ArenaStandard(MAUtils.nameConfigToArena(configName), world); - arena.load(config); - arenas.add(arena); - } - - selectedArena = arenas.get(0); - } - - public Arena createArenaNode(String configName, World world) - { - config.setProperty("arenas." + configName + ".settings.world", world.getName()); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.enabled", true); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.protect", true); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.entry-fee", ""); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.logging", false); - config.setProperty("arenas." + configName + ".settings.clear-wave-before-next", false); - config.setProperty("arenas." + configName + ".settings.detonate-creepers", false); - config.setProperty("arenas." + configName + ".settings.detonate-damage", false); - config.setProperty("arenas." + configName + ".settings.lightning", true); - config.setProperty("arenas." + configName + ".settings.auto-equip-armor", true); - config.setProperty("arenas." + configName + ".settings.force-restore", false); - config.setProperty("arenas." + configName + ".settings.soft-restore", false); - config.setProperty("arenas." + configName + ".settings.soft-restore-drops", false); - config.setProperty("arenas." + configName + ".settings.require-empty-inv-join", true); - config.setProperty("arenas." + configName + ".settings.require-empty-inv-spec", true); - config.setProperty("arenas." + configName + ".settings.hellhounds", false); - config.setProperty("arenas." + configName + ".settings.pvp-enabled", false); - config.setProperty("arenas." + configName + ".settings.monster-infight", false); - config.setProperty("arenas." + configName + ".settings.allow-teleporting", false); - config.setProperty("arenas." + configName + ".settings.spectate-on-death", true); - config.setProperty("arenas." + configName + ".settings.share-items-in-arena", true); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.player-limit", 0); - config.setProperty("arenas." + configName + ".settings.max-join-distance", 0); - config.save(); - config.load(); - config.setProperty("arenas." + configName + ".settings.repair-delay", 5); - config.setProperty("arenas." + configName + ".settings.first-wave-delay", 5); - config.setProperty("arenas." + configName + ".settings.wave-interval", 20); - config.setProperty("arenas." + configName + ".settings.special-modulo", 4); - config.setProperty("arenas." + configName + ".settings.max-idle-time", 0); - config.save(); - config.load(); - - Arena arena = new ArenaStandard(MAUtils.nameConfigToArena(configName), world); - arena.load(config); - return arena; - } - - public void removeArenaNode(String configName) - { - config.removeProperty("arenas." + configName); - config.save(); - } - - - - /*///////////////////////////////////////////////////////////////////////// - // - // Update and serialization methods - // - /////////////////////////////////////////////////////////////////////////*/ - - /** - * Update one, two or all three of global settings, classes - * and arenas (arenas with deserialization). - */ - public void update(boolean settings, boolean classes, boolean arenalist) - { - boolean tmp = enabled; - enabled = false; - - for (Arena arena : arenas) - arena.forceEnd(); - - config.load(); - if (settings) loadSettings(); - if (classes) loadClasses(); - if (arenalist) deserializeArenas(); - config.save(); - - enabled = tmp; - } - - /** - * Serialize the global settings. - */ - public void serializeSettings() - { - String settings = "global-settings."; - config.setProperty(settings + "enabled", enabled); - config.save(); - } - - /** - * Serialize all arena configs. - */ - public void serializeArenas() - { - for (Arena arena : arenas) - arena.serializeConfig(); - } - - /** - * Deserialize all arena configs. Updates the arena list to - * include only the current arenas (not ones added in the - * actual file) that are also in the config-file. - */ - public void deserializeArenas() - { - // Get only the arenas in the config. - Set strings = config.getKeys("arenas"); - if (strings == null) - return; - - // Get their Arena objects. - List configArenas = new LinkedList(); - for (String s : strings) - if (getArenaWithName(s) != null) - configArenas.add(getArenaWithName(s)); - - // Remove all Arenas no longer in the config. - arenas.retainAll(configArenas); - - for (Arena arena : arenas) - arena.deserializeConfig(); - - // Make sure to update the selected arena to a valid one. - if (!arenas.contains(selectedArena) && arenas.size() >= 1) - selectedArena = arenas.get(0); - } - - public void updateSettings() { update(true, false, false); } - public void updateClasses() { update(false, true, false); } - public void updateArenas() { update(false, false, true); } - public void updateAll() { update(true, true, true); } -} diff --git a/src/com/garbagemule/MobArena/ArenaPlayer.java b/src/com/garbagemule/MobArena/ArenaPlayer.java index 359eae8..9d84cb2 100644 --- a/src/com/garbagemule/MobArena/ArenaPlayer.java +++ b/src/com/garbagemule/MobArena/ArenaPlayer.java @@ -1,59 +1,63 @@ package com.garbagemule.MobArena; -import java.util.LinkedList; -import java.util.List; - -import org.bukkit.block.Block; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.ArenaPlayerStatistics; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; public class ArenaPlayer { - public MobArena plugin; - public Player player; - public String className; - public Arena arena; - public List rewards; - public List blocks; - + private Player player; + + private ArenaClass arenaClass; private ArenaPlayerStatistics stats; - - protected boolean isDead, inArena, inLobby, inSpec, isReady; - - public ArenaPlayer(Player player, Arena arena, MobArena plugin) - { + private boolean isDead; + + //private List rewards; + //private List blocks; + + public ArenaPlayer(Player player, Arena arena, MobArena plugin) { this.player = player; - this.arena = arena; - this.plugin = plugin; - - className = arena.classMap.get(player); - rewards = new LinkedList(); - blocks = new LinkedList(); - - stats = new ArenaPlayerStatistics(this); } - - public Player getPlayer() { return player; } - public Arena getArena() { return arena; } - public String getClassName() { return className; } - + + public Player getPlayer() { + return player; + } + + public ArenaClass getArenaClass() { + return arenaClass; + } + + public void setArenaClass(ArenaClass arenaClass) { + this.arenaClass = arenaClass; + } + /** * Check if the player is "dead", i.e. died or not. * @return true, if the player is either a spectator or played and died, false otherwise */ - public boolean isDead() - { + public boolean isDead() { return isDead; } - + /** * Set the player's death status. * @param value true, if the player is dead, false otherwise */ - public void setDead(boolean value) - { + public void setDead(boolean value) { isDead = value; } - - public ArenaPlayerStatistics getStats() { return stats; } + + public void resetStats() { + if (stats != null) { + stats.reset(); + return; + } + stats = new ArenaPlayerStatistics(this); + } + + public ArenaPlayerStatistics getStats() { + return stats; + } } diff --git a/src/com/garbagemule/MobArena/ArenaPlayerStatistics.java b/src/com/garbagemule/MobArena/ArenaPlayerStatistics.java index 8b0289e..30792cf 100644 --- a/src/com/garbagemule/MobArena/ArenaPlayerStatistics.java +++ b/src/com/garbagemule/MobArena/ArenaPlayerStatistics.java @@ -1,91 +1,130 @@ package com.garbagemule.MobArena; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; import org.bukkit.entity.Player; +import com.garbagemule.MobArena.ArenaPlayer; +import com.garbagemule.MobArena.ArenaPlayerStatistics; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.util.MutableInt; + public class ArenaPlayerStatistics { - private String playerName, className; private ArenaPlayer player; - public int kills, dmgDone, dmgTaken, swings, hits, lastWave; - - public ArenaPlayerStatistics(ArenaPlayer player) - { - this.player = player; + private String playerName, className; + private Map ints; + + public ArenaPlayerStatistics(ArenaPlayer player) { + this.player = player; this.playerName = player.getPlayer().getName(); - this.className = player.getClassName(); + this.className = player.getArenaClass().getName(); + reset(); } - - public ArenaPlayerStatistics(Player p, Arena arena, MobArena plugin) - { + + public void reset() { + if (ints == null) { + ints = new HashMap(); + } + + ints.clear(); + + ints.put("kills", new MutableInt()); + ints.put("dmgDone", new MutableInt()); + ints.put("dmgTaken", new MutableInt()); + ints.put("swings", new MutableInt()); + ints.put("hits", new MutableInt()); + ints.put("lastWave", new MutableInt()); + } + + public ArenaPlayerStatistics(Player p, Arena arena, MobArena plugin) { this(new ArenaPlayer(p, arena, plugin)); } - - public ArenaPlayer getArenaPlayer() - { + + public ArenaPlayer getArenaPlayer() { return player; } - - public static Comparator killComparator() - { - return new Comparator() - { - public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) - { - if (s1.kills > s2.kills) - return -1; - else if (s1.kills < s2.kills) - return 1; + + public String getPlayerName() { + return playerName; + } + + public String getClassName() { + return className; + } + + public int getInt(String s) { + return ints.get(s).value(); + } + + public void inc(String s) { + ints.get(s).inc(); + } + + public void add(String s, int amount) { + ints.get(s).add(amount); + } + + public static Comparator killComparator() { + return new Comparator() { + public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) { + int s1kills = s1.getInt("kills"); + int s2kills = s2.getInt("kills"); + + if (s1kills == s2kills) return 0; - } - }; + + return (s1kills > s2kills ? -1 : 1); + } + }; } - - private static int compareKills(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) - { - if (s1.kills > s2.kills) - return -1; - else if (s1.kills < s2.kills) - return 1; - return 0; + + public static Comparator waveComparator() { + return new Comparator() { + public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) { + int result = compareWaves(s1, s2); + if (result != 0) + return result; + + return compareKills(s1, s2); + } + }; } - - public static Comparator waveComparator() - { - return new Comparator() - { - public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) - { - int result = compareWaves(s1, s2); - if (result != 0) return result; - - return compareKills(s1, s2); - } - }; - } - - private static int compareWaves(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) - { - if (s1.lastWave > s2.lastWave) - return -1; - else if (s1.lastWave < s2.lastWave) - return 1; - return 0; - } - - public static Comparator dmgDoneComparator() - { - return new Comparator() - { - public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) - { - if (s1.dmgDone > s2.dmgDone) - return -1; - else if (s1.dmgDone < s2.dmgDone) - return 1; + + public static Comparator dmgDoneComparator() { + return new Comparator() { + public int compare(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) { + int s1dmgDone = s1.getInt("dmgDone"); + int s2dmgDone = s2.getInt("dmgDone"); + + if (s1dmgDone == s2dmgDone) return 0; - } - }; + + return (s1dmgDone > s2dmgDone ? -1 : 1); + } + }; + } + + private static int compareKills(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) { + int s1kills = s1.getInt("kills"); + int s2kills = s2.getInt("kills"); + + if (s1kills == s2kills) + return 0; + + return (s1kills > s2kills ? -1 : 1); + } + + private static int compareWaves(ArenaPlayerStatistics s1, ArenaPlayerStatistics s2) { + int s1wave = s1.getInt("lastWave"); + int s2wave = s2.getInt("lastWave"); + + if (s1wave == s2wave) + return 0; + + return (s1wave > s2wave ? -1 : 1); } } diff --git a/src/com/garbagemule/MobArena/ArenaStandard.java b/src/com/garbagemule/MobArena/ArenaStandard.java deleted file mode 100644 index f40e854..0000000 --- a/src/com/garbagemule/MobArena/ArenaStandard.java +++ /dev/null @@ -1,1413 +0,0 @@ -package com.garbagemule.MobArena; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.PriorityBlockingQueue; - -import net.minecraft.server.WorldServer; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Chunk; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.block.ContainerBlock; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.entity.CraftEntity; -import org.bukkit.entity.Creature; -import org.bukkit.entity.CreatureType; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Item; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Slime; -import org.bukkit.entity.Wolf; -import org.bukkit.event.entity.EntityRegainHealthEvent; -import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.permissions.PermissionAttachment; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.leaderboards.Leaderboard; -import com.garbagemule.MobArena.repairable.Repairable; -import com.garbagemule.MobArena.repairable.RepairableComparator; -import com.garbagemule.MobArena.repairable.RepairableContainer; -import com.garbagemule.MobArena.spout.Spouty; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.Delays; -import com.garbagemule.MobArena.util.InventoryItem; -import com.garbagemule.MobArena.util.WaveUtils; -import com.garbagemule.MobArena.waves.BossWave; -import com.garbagemule.MobArena.waves.Wave; -import com.garbagemule.MobArena.waves.Wave.WaveBranch; - -public class ArenaStandard extends Arena -{ - private MobArena plugin; - - /** - * Primary constructor. Requires a name and a world. - */ - public ArenaStandard(String name, World world) - { - if (world == null) - throw new NullPointerException("[MobArena] ERROR! World for arena '" + name + "' does not exist!"); - - this.name = name; - this.world = world; - plugin = (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena"); - - arenaPlayers = new HashSet(); - lobbyPlayers = new HashSet(); - notifyPlayers = new HashSet(); - readyPlayers = new HashSet(); - specPlayers = new HashSet(); - rewardedPlayers = new HashSet(); - hasPaid = new HashSet(); - monsters = new HashSet(); - explodingSheep = new HashSet(); - plaguedPigs = new HashSet(); - madCows = new HashSet(); - blocks = new HashSet(); - pets = new HashSet(); - petMap = new HashMap(); - classMap = new HashMap(); - randoms = new HashSet(); - repairables = new LinkedList(); - containables = new LinkedList(); - attachments = new HashMap(); - arenaPlayerMap = new HashMap(); - - running = false; - edit = false; - - allowMonsters = world.getAllowMonsters(); - allowAnimals = world.getAllowAnimals(); - spawnMonsters = MAUtils.getSpawnMonsters(world); - - eventListener = new MAListener(this, plugin); - repairQueue = new PriorityBlockingQueue(100, new RepairableComparator()); - } - - public boolean startArena() - { - // Sanity-checks - if (running || lobbyPlayers.isEmpty() || !readyPlayers.containsAll(lobbyPlayers)) - return false; - if (!softRestore && forceRestore && !serializeRegion()) - return false; - - // Store all chest contents. - storeContainerContents(); - - // Populate arenaPlayers and clear the lobby. - arenaPlayers.addAll(lobbyPlayers); - lobbyPlayers.clear(); - readyPlayers.clear(); - - // Assign random classes. - for (Player p : randoms) - assignRandomClass(p); - if (arenaPlayers.isEmpty()) - return false; - - // Teleport players, give full health, initialize maps - for (Player p : arenaPlayers) - { - p.teleport(arenaLoc); - setHealth(p, 20); - p.setFoodLevel(10); - assignClassPermissions(p); - arenaPlayerMap.put(p, new ArenaPlayer(p, this, plugin)); - } - - // Copy the singleWaves Set for polling. - singleWavesInstance = new TreeSet(singleWaves); - - // Start spawning monsters (must happen before 'running = true;') - startSpawning(); - startBouncingSheep(); - - // Set the boolean. - running = true; - - // Spawn pets (must happen after 'running = true;') - spawnPets(); - - // Start logging - log = new ArenaLog(plugin, this); - log.start(); - - // Initialize leaderboards and start displaying info. - leaderboard.initialize(); - leaderboard.startTracking(); - - // Announce and notify. - MAUtils.tellAll(this, Msg.ARENA_START); - - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onArenaStart(this); - - return true; - } - - public boolean endArena() - { - // Sanity-checks. - if (!running || !arenaPlayers.isEmpty()) - { - //System.out.println("Arena was not ended, playercount: " + arenaPlayers.size()); - return false; - } - //else System.out.println("Arena ending..."); - - // Set the boolean. - running = false; - - // Stop tracking leaderboards - leaderboard.stopTracking(); - leaderboard.update(); - - // Finish logging - log.end(); - if (logging != null) - { - log.saveSessionData(); - log.updateArenaTotals(); - } - log.clearSessionData(); - - // Stop spawning. - stopSpawning(); - - // Clean arena floor. - cleanup(); - - // Restore region. - if (softRestore) - restoreRegion(); - else if (forceRestore) - deserializeRegion(); - - // Restore chests - restoreContainerContents(); - - // Announce and clear sets. - MAUtils.tellAll(this, Msg.ARENA_END, true); - arenaPlayers.clear(); - arenaPlayerMap.clear(); - lobbyPlayers.clear(); - readyPlayers.clear(); - notifyPlayers.clear(); - rewardedPlayers.clear(); - classMap.clear(); - - // Notify listeners. - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onArenaEnd(this); - - return true; - } - - public void forceStart() - { - if (running) - return; - - // Set operations. - Set tmp = new HashSet(); - tmp.addAll(lobbyPlayers); - tmp.removeAll(readyPlayers); - - // Force leave. - for (Player p : tmp) - { - plugin.getAM().arenaMap.remove(p); - playerLeave(p); - } - } - - public void forceEnd() - { - Bukkit.getServer().getScheduler().cancelTask(spawnTaskId); - Bukkit.getServer().getScheduler().cancelTask(sheepTaskId); - - for (Player p : getAllPlayers()) - playerLeave(p); - - for (Entity e : monsters) - if (e != null) e.remove(); - - if (bossWave != null) - bossWave.clear(); - - arenaPlayers.clear(); - lobbyPlayers.clear(); - readyPlayers.clear(); - - cleanup(); - - spawnTaskId = -1; - sheepTaskId = -1; - } - - public void playerJoin(Player p, Location loc) - { - storePlayerData(p, loc); - MAUtils.sitPets(p); - setHealth(p, 20); - p.setFoodLevel(10); - p.setGameMode(GameMode.SURVIVAL); - movePlayerToLobby(p); - - // Notify listeners. - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onPlayerJoin(this, p); - - if (MobArena.hasSpout && spoutSelect) - Spouty.classSelectionScreen(plugin, this, p); - } - - public void playerReady(Player p) - { - readyPlayers.add(p); - - if (minPlayers > 0 && lobbyPlayers.size() < minPlayers) - { - MAUtils.tellPlayer(p, Msg.LOBBY_NOT_ENOUGH_PLAYERS, "" + minPlayers); - return; - } - - startArena(); - } - - public void playerLeave(Player p) - { - finishArenaPlayer(p, false); - movePlayerToEntry(p); - discardPlayer(p); - - // Notify listeners. - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onPlayerLeave(this, p); - - // End arena if possible. - endArena(); - } - - public void playerDeath(Player p) - { - finishArenaPlayer(p, true); - - if (specOnDeath) - { - clearPlayer(p); - movePlayerToSpec(p); - } - else - { - movePlayerToEntry(p); - discardPlayer(p); - } - - if (running && spawnThread != null) - spawnThread.updateTargets(); - - MAUtils.tellAll(this, Msg.PLAYER_DIED, p.getName()); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - endArena(); - } - }); - - // Notify listeners. - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onPlayerDeath(this, p); - } - - public void playerSpec(Player p, Location loc) - { - storePlayerData(p, loc); - MAUtils.sitPets(p); - movePlayerToSpec(p); - } - - private void spawnPets() - { - for (Map.Entry entry : petMap.entrySet()) - { - // Remove the bones from the inventory. - Player p = entry.getKey(); - p.getInventory().removeItem(new ItemStack(Material.BONE, entry.getValue())); - - // Spawn wolves, set owner and health, and add to pet set. - for (int i = 0; i < entry.getValue(); i++) - { - Wolf wolf = (Wolf) world.spawnCreature(p.getLocation(), CreatureType.WOLF); - wolf.setTamed(true); - wolf.setOwner(p); - wolf.setHealth(20); - if (hellhounds) - wolf.setFireTicks(32768); - pets.add(wolf); - } - } - } - - private void startSpawning() - { - // Set the spawn flags to enable monster spawning. - MAUtils.setSpawnFlags(plugin, world, 1, allowMonsters, allowAnimals); - - // Start the spawnThread. - spawnThread = new MASpawnThread(plugin, this); - spawnTaskId = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, spawnThread, waveDelay, (!waveClear) ? waveInterval : 60); - } - - private void stopSpawning() - { - // Stop the spawn thread. - if (spawnThread != null) - { - Bukkit.getServer().getScheduler().cancelTask(spawnThread.getTaskId()); - Bukkit.getServer().getScheduler().cancelTask(spawnTaskId); - Bukkit.getServer().getScheduler().cancelTask(sheepTaskId); - spawnTaskId = -1; - sheepTaskId = -1; - spawnThread = null; - currentWave = null; - } - else System.out.println("--------- THE SPAWNTHREAD IS NULL! ----------"); - - // Restore spawn flags. - MAUtils.setSpawnFlags(plugin, world, spawnMonsters, allowMonsters, allowAnimals); - } - - private void startBouncingSheep() - { - sheepTaskId = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, - new Runnable() - { - public void run() - { - if (explodingSheep.isEmpty()) return; - - for (LivingEntity e : new LinkedList(explodingSheep)) - { - if (e == null) continue; - - Creature c = (Creature) e; - LivingEntity target = c.getTarget(); - if (target != null && target instanceof Player && MAUtils.distanceSquared((Player) target, e.getLocation()) < 8D) - { - CraftEntity ce = (CraftEntity) e; - CraftWorld cw = (CraftWorld) e.getWorld(); - WorldServer ws = cw.getHandle(); - ws.createExplosion(ce.getHandle(), e.getLocation().getX(), e.getLocation().getY() + 1, e.getLocation().getZ(), 2f, false); - e.remove(); - } - - if (e.isDead()) - { - explodingSheep.remove(e); - continue; - } - - if (Math.abs(e.getVelocity().getY()) < 1) - e.setVelocity(e.getVelocity().setY(0.5)); - } - } - }, waveDelay, 20); - } - - private void updateChunk(Location loc) - { - if (!arenaPlayers.isEmpty() || !world.getName().equals(loc.getWorld().getName())) - return; - - Chunk chunk = world.getChunkAt(loc); - if (!world.isChunkLoaded(chunk)) - world.loadChunk(chunk); - else - world.refreshChunk(chunk.getX(), chunk.getZ()); - } - - public void playerKill(Player p) - { - if (p == null || log.players.get(p) == null) - return; - - arenaPlayerMap.get(p).getStats().kills++; - } - - public void restoreInvAndGiveRewardsDelayed(final Player p) - { - final List rewards = log != null && log.players.get(p) != null ? log.players.get(p).rewards : new LinkedList(); - final boolean hadRewards = rewardedPlayers.contains(p); - - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - if (!emptyInvJoin) - MAUtils.restoreInventory(p); - - if (hadRewards) - return; - - MAUtils.giveRewards(p, rewards, plugin); - if (running) - rewardedPlayers.add(p); - } - }); - } - - public void restoreInvAndGiveRewards(final Player p) - { - if (!emptyInvJoin) - MAUtils.restoreInventory(p); - - if (rewardedPlayers.contains(p)) - return; - - final List rewards = (log != null && log.players.get(p) != null) ? - log.players.get(p).rewards : - new LinkedList(); - - MAUtils.giveRewards(p, rewards, plugin); - if (running) - rewardedPlayers.add(p); - } - - public void storePlayerData(Player p, Location loc) - { - plugin.getAM().arenaMap.put(p, this); - - if (!locations.containsKey(p)) - locations.put(p, loc); - - if (!healthMap.containsKey(p)) - healthMap.put(p, p.getHealth()); - - if (!hungerMap.containsKey(p)) - hungerMap.put(p, p.getFoodLevel()); - - if (!modeMap.containsKey(p)) - modeMap.put(p, p.getGameMode()); - } - - public void storeContainerContents() - { - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - for (Location loc : containers.values()) - { - BlockState state = world.getBlockAt(loc).getState(); - if (state instanceof ContainerBlock) - containables.add(new RepairableContainer(state, false)); - } - } - }); - } - - public void restoreContainerContents() - { - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - for (Repairable r : containables) - r.repair(); - } - }); - } - - public void movePlayerToLobby(Player p) - { - updateChunk(lobbyLoc); - lobbyPlayers.add(p); - p.teleport(lobbyLoc); - } - - public void movePlayerToSpec(Player p) - { - updateChunk(spectatorLoc); - specPlayers.add(p); - p.teleport(spectatorLoc); - } - - public void movePlayerToEntry(Player p) - { - Location entry = locations.get(p); - if (entry == null || p.isDead()) return; - - updateChunk(entry); - p.teleport(entry); - } - - private void clearPlayer(Player p) - { - if (healthMap.containsKey(p)) - { - int health = healthMap.remove(p); - setHealth(p, health); - } - - if (hungerMap.containsKey(p)) - p.setFoodLevel(hungerMap.remove(p)); - - if (modeMap.containsKey(p)) - p.setGameMode(modeMap.remove(p)); - - // Put out fire. - Delays.douse(plugin, p, 3); - - // Remove pets. - removePets(p); - - // readyPlayers before lobbyPlayers because of startArena sanity-checks - readyPlayers.remove(p); - specPlayers.remove(p); - arenaPlayers.remove(p); - lobbyPlayers.remove(p); - classMap.remove(p); - arenaPlayerMap.remove(p); - } - - /** - * Completely remove a player from the arena collections. - * @param p A player - */ - private void discardPlayer(Player p) - { - locations.remove(p); - plugin.getAM().arenaMap.remove(p); - clearPlayer(p); - } - - /** - * Give the player back his inventory and record his last wave. - * Called when a player dies or leaves prematurely. - * @param p A player - * @param dead If the player died or not - */ - private void finishArenaPlayer(Player p, boolean dead) - { - if (!arenaPlayers.contains(p) && !lobbyPlayers.contains(p)) - return; - - removeClassPermissions(p); - MAUtils.clearInventory(p); - - if (dead) restoreInvAndGiveRewardsDelayed(p); - else restoreInvAndGiveRewards(p); - - ArenaPlayer ap = arenaPlayerMap.get(p); - if (ap != null) - ap.setDead(true); - } - - private void setHealth(Player p, int health) - { - // Grab the current health - int current = p.getHealth(); - - // If the health is 20, just leave it at that no matter what. - int regain = health == 20 ? 20 : health - current; - - // Set the health, and fire off the event. - p.setHealth(health); - EntityRegainHealthEvent event = new EntityRegainHealthEvent(p, regain, RegainReason.CUSTOM); - Bukkit.getPluginManager().callEvent(event); - } - - public void repairBlocks() - { - while (!repairQueue.isEmpty()) - repairQueue.poll().repair(); - } - - public void queueRepairable(Repairable r) - { - repairQueue.add(r); - } - - - - /*//////////////////////////////////////////////////////////////////// - // - // Items & Cleanup - // - ////////////////////////////////////////////////////////////////////*/ - - public void assignClass(Player p, String className) - { - petMap.remove(p); - randoms.remove(p); - classMap.put(p, className); - - MAUtils.clearInventory(p); - - // If random, don't give any items yet. - if (className.equalsIgnoreCase("random")) - { - randoms.add(p); - return; - } - - MAUtils.giveItems(p, classItems.get(className), autoEquip, plugin); - MAUtils.giveItems(p, classArmor.get(className), autoEquip, plugin); - - int pets = MAUtils.getPetAmount(p); - if (pets > 0) petMap.put(p, pets); - } - - public void assignRandomClass(Player p) - { - Random r = new Random(); - List classes = new LinkedList(plugin.getAM().classes); - - String className = classes.remove(r.nextInt(classes.size())); - while (!plugin.has(p, "mobarena.classes." + className)) - { - if (classes.isEmpty()) - { - MobArena.info("Player '" + p.getName() + "' has no class permissions!"); - playerLeave(p); - return; - } - className = classes.remove(r.nextInt(classes.size())); - } - - assignClass(p, className); - MAUtils.tellPlayer(p, Msg.LOBBY_CLASS_PICKED, className); - } - - public void assignClassPermissions(Player p) - { - Map perms = classPerms.get(classMap.get(p)); - if (perms == null || perms.isEmpty()) return; - - PermissionAttachment pa = p.addAttachment(plugin); - attachments.put(p,pa); - for (Map.Entry entry : perms.entrySet()) - pa.setPermission(entry.getKey(), entry.getValue()); - - p.recalculatePermissions(); - } - - public void removeClassPermissions(Player p) - { - if (attachments.get(p) == null) return; - - for (PermissionAttachment pa : attachments.values()) - if (pa != null) pa.remove(); - - p.recalculatePermissions(); - } - - private void cleanup() - { - removeMonsters(); - removeBlocks(); - removePets(); - removeEntities(); - monsters.clear(); - explodingSheep.clear(); - plaguedPigs.clear(); - madCows.clear(); - blocks.clear(); - pets.clear(); - } - - private void removeMonsters() - { - if (bossWave != null) - bossWave.clear(); - for (LivingEntity e : monsters) - if (e != null) e.remove(); - for (LivingEntity e : explodingSheep) - if (e != null) e.remove(); - for (LivingEntity e : plaguedPigs) - if (e != null) e.remove(); - for (LivingEntity e : madCows) - if (e != null) e.remove(); - } - - private void removeBlocks() - { - for (Block b : blocks) - b.setTypeId(0); - } - - private void removePets() - { - for (Wolf w : pets) - { - if (w == null) continue; - - w.setOwner(null); - w.remove(); - } - } - - private void removePets(Player p) - { - for (Wolf w : pets) - { - if (w == null || !(w.getOwner() instanceof Player) || !((Player) w.getOwner()).getName().equals(p.getName())) - continue; - - w.setOwner(null); - w.remove(); - } - petMap.remove(p); - } - - private void removeEntities() - { - if (p1 == null || p2 == null) return; - Chunk c1 = world.getChunkAt(p1); - Chunk c2 = world.getChunkAt(p2); - - for (int i = c1.getX(); i <= c2.getX(); i++) - for (int j = c1.getZ(); j <= c2.getZ(); j++) - for (Entity e : world.getChunkAt(i,j).getEntities()) - if ((e instanceof Item || e instanceof Slime) && inRegion(e.getLocation())) - if (e != null) e.remove(); - } - - - - /*//////////////////////////////////////////////////////////////////// - // - // Initialization & Checks - // - ////////////////////////////////////////////////////////////////////*/ - - public void load(Config config) - { - config.load(); - - String arenaPath = "arenas." + MAUtils.nameArenaToConfig(name) + ".settings."; - String configName = MAUtils.nameArenaToConfig(name); - - enabled = config.getBoolean(arenaPath + "enabled", true); - protect = config.getBoolean(arenaPath + "protect", true); - logging = config.getString(arenaPath + "logging", null); - autoEquip = config.getBoolean(arenaPath + "auto-equip-armor", true); - waveClear = config.getBoolean(arenaPath + "clear-wave-before-next", false); - detCreepers = config.getBoolean(arenaPath + "detonate-creepers", false); - detDamage = config.getBoolean(arenaPath + "detonate-damage", false); - lightning = config.getBoolean(arenaPath + "lightning", true); - forceRestore = config.getBoolean(arenaPath + "force-restore", false); - softRestore = config.getBoolean(arenaPath + "soft-restore", false); - softRestoreDrops = config.getBoolean(arenaPath + "soft-restore-drops", false); - emptyInvJoin = config.getBoolean(arenaPath + "require-empty-inv-join", true); - emptyInvSpec = config.getBoolean(arenaPath + "require-empty-inv-spec", true); - hellhounds = config.getBoolean(arenaPath + "hellhounds", false); - pvp = config.getBoolean(arenaPath + "pvp-enabled", false); - monsterInfight = config.getBoolean(arenaPath + "monster-infight", false); - allowWarp = config.getBoolean(arenaPath + "allow-teleporting", false); - specOnDeath = config.getBoolean(arenaPath + "spectate-on-death", true); - shareInArena = config.getBoolean(arenaPath + "share-items-in-arena", true); - spoutSelect = config.getBoolean(arenaPath + "spout-class-select", true); - joinDistance = config.getInt(arenaPath + "max-join-distance", 0); - minPlayers = config.getInt(arenaPath + "min-players", 0); - maxPlayers = config.getInt(arenaPath + "max-players", 0); - waveDelay = config.getInt(arenaPath + "first-wave-delay", 5) * 20; - waveInterval = config.getInt(arenaPath + "wave-interval", 20) * 20; - specialModulo = config.getInt(arenaPath + "special-modulo", 4); - maxIdleTime = config.getInt(arenaPath + "max-idle-time", 0) * 20; - finalWave = config.getInt(arenaPath + "final-wave", 0); - - everyWaveMap = MAUtils.getArenaRewardMap(config, configName, "every"); - afterWaveMap = MAUtils.getArenaRewardMap(config, configName, "after"); - entryFee = MAUtils.getEntryFee(config, configName); - - p1 = MAUtils.getArenaCoord(config, world, configName, "p1"); - p2 = MAUtils.getArenaCoord(config, world, configName, "p2"); - l1 = MAUtils.getArenaCoord(config, world, configName, "l1"); - l2 = MAUtils.getArenaCoord(config, world, configName, "l2"); - arenaLoc = MAUtils.getArenaCoord(config, world, configName, "arena"); - lobbyLoc = MAUtils.getArenaCoord(config, world, configName, "lobby"); - spectatorLoc = MAUtils.getArenaCoord(config, world, configName, "spectator"); - spawnpoints = MAUtils.getArenaSpawnpoints(config, world, configName); - spawnpointsBoss = MAUtils.getArenaBossSpawnpoints(config, world, configName); - containers = MAUtils.getArenaContainers(config, world, configName); - leaderboard = new Leaderboard(plugin, this, config); - - // NEW WAVES - singleWaves = WaveUtils.getWaves(this, config, WaveBranch.SINGLE); - recurrentWaves = WaveUtils.getWaves(this, config, WaveBranch.RECURRENT); - - classes = plugin.getAM().classes; - classItems = plugin.getAM().classItems; - classArmor = plugin.getAM().classArmor; - classPerms = plugin.getAM().classPerms; - - // Determine if the arena is properly set up. Then add the to arena list. - setup = MAUtils.verifyData(this); - lobbySetup = MAUtils.verifyLobby(this); - } - - public void restoreRegion() - { - Collections.sort(repairables, new RepairableComparator()); - - for (Repairable r : repairables) - r.repair(); - } - - public void serializeConfig() - { - String coords = "arenas." + configName() + ".coords."; - Config config = plugin.getMAConfig(); - - config.setProperty("arenas." + configName() + ".settings.enabled", enabled); - config.setProperty("arenas." + configName() + ".settings.protect", protect); - if (p1 != null) config.setProperty(coords + "p1", MAUtils.makeCoord(p1)); - if (p2 != null) config.setProperty(coords + "p2", MAUtils.makeCoord(p2)); - if (l1 != null) config.setProperty(coords + "l1", MAUtils.makeCoord(l1)); - if (l2 != null) config.setProperty(coords + "l2", MAUtils.makeCoord(l2)); - if (arenaLoc != null) config.setProperty(coords + "arena", MAUtils.makeCoord(arenaLoc)); - if (lobbyLoc != null) config.setProperty(coords + "lobby", MAUtils.makeCoord(lobbyLoc)); - if (spectatorLoc != null) config.setProperty(coords + "spectator", MAUtils.makeCoord(spectatorLoc)); - for (Map.Entry entry : spawnpoints.entrySet()) - config.setProperty(coords + "spawnpoints." + entry.getKey(), MAUtils.makeCoord(entry.getValue())); - for (Map.Entry entry : spawnpointsBoss.entrySet()) - config.setProperty(coords + "spawnpoints." + entry.getKey(), MAUtils.makeCoord(entry.getValue())); - - config.save(); - } - - public void deserializeConfig() - { - Config config = plugin.getMAConfig(); - config.load(); - load(config); - } - - public boolean serializeRegion() - { - int x1 = (int) p1.getX(); - int y1 = (int) p1.getY(); - int z1 = (int) p1.getZ(); - int x2 = (int) p2.getX(); - int y2 = (int) p2.getY(); - int z2 = (int) p2.getZ(); - - HashSet set = new HashSet(); - int[] buffer; - for (int i = x1; i <= x2; i++) - { - for (int j = y1; j <= y2; j++) - { - for (int k = z1; k <= z2; k++) - { - buffer = new int[4]; - buffer[0] = i; - buffer[1] = j; - buffer[2] = k; - buffer[3] = world.getBlockAt(i,j,k).getTypeId(); - set.add(buffer); - } - } - } - - try - { - new File(plugin.getDataFolder() + File.separator + "regions").mkdir(); - File regionFile = new File(plugin.getDataFolder() + File.separator + "regions" + File.separator + configName() + ".tmp"); - if (regionFile.exists()) - regionFile.createNewFile(); - - FileOutputStream fos = new FileOutputStream(regionFile); - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(set); - oos.close(); - } - catch (Exception e) - { - MobArena.warning("Could not create region file. The arena will not be started!"); - e.printStackTrace(); - return false; - } - return true; - } - - @SuppressWarnings("unchecked") - public boolean deserializeRegion() - { - HashSet set = new HashSet(); - try - { - File regionFile = new File(plugin.getDataFolder() + File.separator + "regions" + File.separator + configName() + ".tmp"); - if (!regionFile.exists()) - return false; - - FileInputStream fis = new FileInputStream(regionFile); - ObjectInputStream ois = new ObjectInputStream(fis); - set = (HashSet) ois.readObject(); - ois.close(); - } - catch (Exception e) - { - MobArena.warning("Could not find region file. The arena cannot be restored!"); - e.printStackTrace(); - return false; - } - - for (int[] buffer : set) - world.getBlockAt(buffer[0], buffer[1], buffer[2]).setTypeId(buffer[3]); - - return true; - } - - /** - * Check if a location is inside of the cuboid region - * that p1 and p2 span. - */ - public boolean inRegion(Location loc) - { - if (!loc.getWorld().getName().equals(world.getName()) || !setup) - return false; - - int x = loc.getBlockX(); - int y = loc.getBlockY(); - int z = loc.getBlockZ(); - - // Check the lobby first. - if (lobbySetup) - { - if ((x >= l1.getBlockX() && x <= l2.getBlockX()) && - (z >= l1.getBlockZ() && z <= l2.getBlockZ()) && - (y >= l1.getBlockY() && y <= l2.getBlockY())) - return true; - } - - // Returns false if the location is outside of the region. - return ((x >= p1.getBlockX() && x <= p2.getBlockX()) && - (z >= p1.getBlockZ() && z <= p2.getBlockZ()) && - (y >= p1.getBlockY() && y <= p2.getBlockY())); - } - - /** - * Check if a location is inside of the arena region, expanded - * by 'radius' blocks. Used with explosions. - */ - public boolean inRegionRadius(Location loc, int radius) - { - if (!loc.getWorld().getName().equals(world.getName()) || !setup) - return false; - - int x = loc.getBlockX(); - int y = loc.getBlockY(); - int z = loc.getBlockZ(); - - // Check the lobby first. - if (lobbySetup) - { - if ((x + radius >= l1.getBlockX() && x - radius <= l2.getBlockX()) && - (z + radius >= l1.getBlockZ() && z - radius <= l2.getBlockZ()) && - (y + radius >= l1.getBlockY() && y - radius <= l2.getBlockY())) - return true; - } - - return ((x + radius >= p1.getBlockX() && x - radius <= p2.getBlockX()) && - (z + radius >= p1.getBlockZ() && z - radius <= p2.getBlockZ()) && - (y + radius >= p1.getBlockY() && y - radius <= p2.getBlockY())); - } - - - - /*//////////////////////////////////////////////////////////////////// - // - // Getters & Misc - // - ////////////////////////////////////////////////////////////////////*/ - - public boolean inArena(Player p) - { - return arenaPlayers.contains(p); - } - - public boolean inLobby(Player p) - { - return lobbyPlayers.contains(p); - } - - public boolean isEnabled() { - return enabled; - } - - public boolean isRunning() - { - return running; - } - - public boolean isPvpEnabled() - { - return pvp; - } - - public boolean isLightningEnabled() - { - return lightning; - } - - public boolean isBossWave() - { - return bossWave != null; - } - - public String configName() - { - return MAUtils.nameArenaToConfig(name); - } - - public String arenaName() - { - return name; - } - - public MobArena getPlugin() - { - return plugin; - } - - public World getWorld() - { - return world; - } - - public Wave getWave() - { - return currentWave; - } - - public void setWave(Wave wave) - { - currentWave = wave; - } - - public void setBossWave(BossWave bossWave) - { - this.bossWave = bossWave; - } - - public Set getClasses() - { - return classes; - } - - public List getAllSpawnpoints() - { - ArrayList result = new ArrayList(spawnpoints.size() + spawnpointsBoss.size()); - result.addAll(spawnpoints.values()); - result.addAll(spawnpointsBoss.values()); - return result; - } - - public List getSpawnpoints() - { - return new ArrayList(spawnpoints.values()); - } - - public Location getBossSpawnpoint() - { - if (spawnpointsBoss.isEmpty()) - { - List points = WaveUtils.getValidSpawnpoints(this, getLivingPlayers()); - return points.get(MobArena.random.nextInt(points.size())); - } - - return new ArrayList(spawnpointsBoss.values()).get(MobArena.random.nextInt(spawnpointsBoss.size())); - } - - public int getPlayerCount() - { - return spawnThread.getPlayerCount(); - } - - public void addBlock(Block b) - { - blocks.add(b); - } - - public void addMonster(LivingEntity e) - { - monsters.add(e); - } - - public void addExplodingSheep(LivingEntity e) - { - explodingSheep.add(e); - } - - public List getAllPlayers() - { - List result = new LinkedList(); - result.addAll(arenaPlayers); - result.addAll(lobbyPlayers); - result.addAll(specPlayers); - return result; - } - - public List getLivingPlayers() - { - List result = new LinkedList(); - result.addAll(arenaPlayers); - return result; - } - - public Set getArenaPlayers() - { - return arenaPlayers; - } - - public Collection getArenaPlayerSet() - { - return arenaPlayerMap.values(); - } - - public List getArenaPlayerStatistics(Comparator comparator) - { - List list = new ArrayList(); - - for (ArenaPlayer ap : arenaPlayerMap.values()) - list.add(ap.getStats()); - - Collections.sort(list, comparator); - return list; - } - - public List getNonreadyPlayers() - { - List result = new LinkedList(); - result.addAll(lobbyPlayers); - result.removeAll(readyPlayers); - return result; - } - - public Set getMonsters() - { - Set tmp = new HashSet(monsters); - tmp.addAll(explodingSheep); - return tmp; - } - - public Set getPets() - { - return pets; - } - - public void resetIdleTimer() - { - if (maxIdleTime <= 0 || !running) - return; - - // Reset the previousSize, cancel the previous timer, and start the new timer. - spawnThread.setPreviousSize(getMonsters().size()); - Bukkit.getServer().getScheduler().cancelTask(spawnThread.getTaskId()); - int id = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - // Make sure to remove any dead/removed entities first. - List ms = new LinkedList(monsters); - for (Entity e : ms) - if (e.isDead()) - monsters.remove(e); - - // Compare the current size with the previous size. - if (monsters.size() < spawnThread.getPreviousSize() || spawnThread.getPreviousSize() == 0 || bossWave != null) - { - resetIdleTimer(); - return; - } - - // Clear all player inventories, and "kill" all players. - List ps = new LinkedList(arenaPlayers); - for (Player p : ps) - { - MAUtils.clearInventory(p); - MAUtils.tellPlayer(p, Msg.FORCE_END_IDLE); - playerDeath(p); - } - } - }, maxIdleTime); - spawnThread.setTaskId(id); - } - - public void addTrunkAndLeaves(Block b) - { - final int x = b.getX(); - final int y = b.getY(); - final int z = b.getZ(); - - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - List result = new LinkedList(); - for (int i = x-2; i <= x+2; i++) - for (int j = y; j <= y+10; j++) - for (int k = z-2; k <= z+2; k++) - if (world.getBlockAt(i,j,k).getType() == Material.LOG || world.getBlockAt(i,j,k).getType() == Material.LEAVES) - result.add(world.getBlockAt(i,j,k)); - - if (running) blocks.addAll(result); - } - }, 10); - } - - public boolean canAfford(Player p) - { - if (entryFee.isEmpty()) - return true; - - PlayerInventory inv = p.getInventory(); - for (ItemStack stack : entryFee) - { - // Economy money - if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) - { - //if (plugin.Methods.hasMethod() && !plugin.Method.getAccount(p.getName()).hasEnough(stack.getAmount())) - if (plugin.Method != null && !(plugin.Method.getAccount(p.getName()).balance() >= stack.getAmount())) - return false; - } - // Normal stack - else - { - if (!inv.contains(stack.getTypeId(), stack.getAmount())) - return false; - } - } - - return true; - } - - public boolean takeFee(Player p) - { - if (entryFee.isEmpty()) - return true; - - PlayerInventory inv = p.getInventory(); - InventoryItem[] items = InventoryItem.parseItemStacks(inv.getContents()); - InventoryItem[] fee = InventoryItem.parseItemStacks(entryFee); - - // Take some economy money - for (InventoryItem item : InventoryItem.extractAllFromArray(MobArena.ECONOMY_MONEY_ID, fee)) - if (plugin.Method != null) - plugin.Method.getAccount(p.getName()).subtract(item.getAmount()); - - // Take any other items - for (InventoryItem item : fee) - InventoryItem.removeItemFromArray(item, items); - - // Turn everything back into ItemStacks - for (int i = 0; i < items.length; i++) - inv.setItem(i, items[i].toItemStack()); - - return true; - } - - public boolean canJoin(Player p) - { - if (!enabled) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_NOT_ENABLED); - else if (!setup || recurrentWaves.isEmpty()) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_NOT_SETUP); - else if (edit) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_EDIT_MODE); - else if (running && (notifyPlayers.contains(p) || notifyPlayers.add(p))) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_IS_RUNNING); - else if (arenaPlayers.contains(p) || lobbyPlayers.contains(p)) - MAUtils.tellPlayer(p, Msg.JOIN_ALREADY_PLAYING); - else if (!plugin.has(p, "mobarena.arenas." + configName())) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_PERMISSION); - else if (maxPlayers > 0 && lobbyPlayers.size() >= maxPlayers) - MAUtils.tellPlayer(p, Msg.JOIN_PLAYER_LIMIT_REACHED); - else if (joinDistance > 0 && !inRegionRadius(p.getLocation(), joinDistance)) - MAUtils.tellPlayer(p, Msg.JOIN_TOO_FAR); - else if (emptyInvJoin && !MAUtils.hasEmptyInventory(p)) - MAUtils.tellPlayer(p, Msg.JOIN_EMPTY_INV); - else if (!canAfford(p))// || !takeFee(p)) - MAUtils.tellPlayer(p, Msg.JOIN_FEE_REQUIRED, MAUtils.listToString(entryFee, plugin)); - else return true; - - return false; - } - - public boolean canSpec(Player p) - { - if (!enabled) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_NOT_ENABLED); - else if (!setup) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_NOT_SETUP); - else if (edit) - MAUtils.tellPlayer(p, Msg.JOIN_ARENA_EDIT_MODE); - else if (arenaPlayers.contains(p) || lobbyPlayers.contains(p)) - MAUtils.tellPlayer(p, Msg.SPEC_ALREADY_PLAYING); - else if (emptyInvSpec && !MAUtils.hasEmptyInventory(p)) - MAUtils.tellPlayer(p, Msg.SPEC_EMPTY_INV); - else if (joinDistance > 0 && !inRegionRadius(p.getLocation(), joinDistance)) - MAUtils.tellPlayer(p, Msg.JOIN_TOO_FAR); - else return true; - - return false; - } - - /** - * The "perfect equals method" cf. "Object-Oriented Design and Patterns" - * by Cay S. Horstmann. - */ - public boolean equals(Object other) - { - if (this == other) return true; - if (other == null) return false; - if (getClass() != other.getClass()) return false; - - // Arenas must have different names. - if (other instanceof ArenaStandard && ((ArenaStandard)other).name.equals(name)) - return true; - - return false; - } - - public String toString() - { - return ((enabled && setup) ? ChatColor.GREEN : ChatColor.GRAY) + configName(); - } -} diff --git a/src/com/garbagemule/MobArena/MABlockListener.java b/src/com/garbagemule/MobArena/MABlockListener.java deleted file mode 100644 index 47c9c8c..0000000 --- a/src/com/garbagemule/MobArena/MABlockListener.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.garbagemule.MobArena; - -import org.bukkit.ChatColor; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockListener; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.block.SignChangeEvent; - -import com.garbagemule.MobArena.leaderboards.Stats; - -public class MABlockListener extends BlockListener -{ - private ArenaMaster am; - - public MABlockListener(ArenaMaster am) - { - this.am = am; - } - - public void onBlockBreak(BlockBreakEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onBlockBreak(event); - } - - public void onBlockBurn(BlockBurnEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onBlockBurn(event); - } - - public void onBlockPlace(BlockPlaceEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onBlockPlace(event); - } - - public void onBlockIgnite(BlockIgniteEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onBlockIgnite(event); - } - - public void onSignChange(SignChangeEvent event) - { - if (!event.getPlayer().hasPermission("mobarena.setup.leaderboards")) - return; - - if (event.getLine(0).startsWith("[MA]")) - { - String text = event.getLine(0).substring((4)); - Arena arena; - Stats stat; - if ((arena = am.getArenaWithName(text)) != null) - { - arena.eventListener.onSignChange(event); - setSignLines(event, ChatColor.GREEN + "MobArena", ChatColor.YELLOW + arena.arenaName(), ChatColor.AQUA + "Players", "---------------"); - } - else if ((stat = Stats.fromString(text)) != null) - { - setSignLines(event, ChatColor.GREEN + "", "", ChatColor.AQUA + stat.getFullName(), "---------------"); - MAUtils.tellPlayer(event.getPlayer(), "Stat sign created."); - } - } - } - - private void setSignLines(SignChangeEvent event, String s1, String s2, String s3, String s4) - { - event.setLine(0, s1); - event.setLine(1, s2); - event.setLine(2, s3); - event.setLine(3, s4); - } - - /* - public void onBlockPhysics(BlockPhysicsEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onBlockPhysics(event); - } - */ -} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/MAClass.java b/src/com/garbagemule/MobArena/MAClass.java deleted file mode 100644 index 3eb6e55..0000000 --- a/src/com/garbagemule/MobArena/MAClass.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; - -public class MAClass -{ - private String name; - private List items, armor; - private Map perms; - - public MAClass(String name) - { - this.name = name; - this.items = new LinkedList(); - this.armor = new LinkedList(); - this.perms = new HashMap(); - } - - public MAClass(String name, List items, List armor) - { - this.name = name; - this.items = items; - this.armor = armor; - this.perms = new HashMap(); - sortArmor(); - immunizeWeapons(); - } - - public String getName() - { - return name; - } - - public List getItems() - { - return items; - } - - public void setItems(List items) - { - this.items = items; - immunizeWeapons(); - } - - public void setArmor(List armor) - { - this.armor = armor; - sortArmor(); - } - - public void setPerms(Map perms) - { - this.perms = perms; - } - - public void giveItems(Player p) - { - PlayerInventory inv = p.getInventory(); - - for (ItemStack stack : items) - { - - inv.addItem(stack.clone()); - } - } - - public void giveArmor(Player p) - { - PlayerInventory inv = p.getInventory(); - - inv.setHelmet (armor.get(0)); - inv.setChestplate(armor.get(1)); - inv.setLeggings (armor.get(2)); - inv.setBoots (armor.get(3)); - } - - private void sortArmor() - { - ItemStack s0 = null, s1 = null, s2 = null, s3 = null; - for (ItemStack stack : armor) - { - if (isHelmet(stack)) - s0 = stack; - else if (isChestplate(stack)) - s1 = stack; - else if (isLeggings(stack)) - s2 = stack; - else if (isBoots(stack)) - s3 = stack; - } - - armor.set(0, s0); - armor.set(1, s1); - armor.set(2, s2); - armor.set(3, s3); - } - - private void immunizeWeapons() - { - for (ItemStack stack : items) - { - } - } - - private boolean isHelmet(ItemStack stack) - { - return (!isChestplate(stack) && !isLeggings(stack) && !isBoots(stack)); - } - - private boolean isChestplate(ItemStack stack) - { - return matches(stack, 299, 303, 307, 311, 315); - } - - private boolean isLeggings(ItemStack stack) - { - return matches(stack, 300, 304, 308, 312, 316); - } - - private boolean isBoots(ItemStack stack) - { - return matches(stack, 301, 305, 309, 313, 317); - } - - private boolean isWeapon(ItemStack stack) - { - return matches(stack, 256, 257, 258, 267, 268, 269, 270, 271, 272, 273, - 274, 275, 276, 277, 278, 279, 283, 284, 285, 286, - 290, 291, 292, 293, 294, 346); - } - - private boolean matches(ItemStack stack, int... ids) - { - int id = stack.getTypeId(); - - for (int i : ids) - if (id == i) - return true; - - return false; - } -} diff --git a/src/com/garbagemule/MobArena/MACommands.java b/src/com/garbagemule/MobArena/MACommands.java deleted file mode 100644 index f5d2992..0000000 --- a/src/com/garbagemule/MobArena/MACommands.java +++ /dev/null @@ -1,1309 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.List; -import java.util.LinkedList; -import java.util.Set; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.DyeColor; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.block.ContainerBlock; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.ConsoleCommandSender; - -public class MACommands implements CommandExecutor -{ - public static List ALLOWED_COMMANDS = new LinkedList(); - public static final List COMMANDS = new LinkedList(); - static - { - COMMANDS.add("j"); // Join - COMMANDS.add("join"); // Join - COMMANDS.add("l"); // Leave - COMMANDS.add("leave"); // Leave - COMMANDS.add("notready"); // List of players who aren't ready - COMMANDS.add("spec"); // Watch arena - COMMANDS.add("spectate"); // Watch arena - COMMANDS.add("arenas"); // List of arenas - COMMANDS.add("list"); // List of players - COMMANDS.add("players"); // List of players - COMMANDS.add("info"); // Info/help - COMMANDS.add("help"); // Info/help - COMMANDS.add("restore"); // Restore inventory - COMMANDS.add("enable"); // Enabling - COMMANDS.add("disable"); // Disabling - COMMANDS.add("protect"); // Protection on/off - COMMANDS.add("force"); // Force start/end - COMMANDS.add("config"); // Reload config - COMMANDS.add("arena"); // Current arena - COMMANDS.add("setarena"); // Set current arena - COMMANDS.add("addarena"); // Add a new arena - COMMANDS.add("delarena"); // Delete current aren - COMMANDS.add("editarena"); // Editing - COMMANDS.add("setregion"); // Set a region point - COMMANDS.add("expandregion"); // Expand the region - COMMANDS.add("showregion"); // Show the region - COMMANDS.add("setlobbyregion"); // Set a region point - COMMANDS.add("expandlobbyregion"); // Expand the region - COMMANDS.add("setwarp"); // Set arena/lobby/spec - COMMANDS.add("spawnpoints"); // List spawnpoints - COMMANDS.add("addspawn"); // Add a spawnpoint - COMMANDS.add("delspawn"); // Delete a spawnpoint - COMMANDS.add("containers"); // List containers - COMMANDS.add("addcontainer"); // Add a container block - COMMANDS.add("delcontainer"); // Delete a container block - COMMANDS.add("reset"); // Reset arena coordinates - COMMANDS.add("addclass"); // Add a new class - COMMANDS.add("delclass"); // Delete a class - COMMANDS.add("checkdata"); // Check arena well formedness - COMMANDS.add("auto-generate"); // Auto-generate arena - COMMANDS.add("auto-degenerate"); // Restore cuboid - } - private boolean meanAdmins, showingRegion; - private Server server; - private MobArena plugin; - private ArenaMaster am; - - public MACommands() - { - - } - - public MACommands(MobArena plugin, ArenaMaster am) - { - this.plugin = plugin; - this.am = am; - server = Bukkit.getServer(); - meanAdmins = (server.getPluginManager().getPlugin("Mean Admins") != null); - ALLOWED_COMMANDS = MAUtils.getAllowedCommands(plugin.getMAConfig()); - } - - /** - * Handles all command parsing. - * Unrecognized commands return false, giving the sender a list of - * valid commands (from plugin.yml). - */ - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) - { - // Play nice with Mean Admins. - if (meanAdmins && !COMMANDS.contains(args[0].toLowerCase())) - { - server.getPluginManager().getPlugin("Mean Admins").onCommand(sender, command, label, args); - return true; - } - - // Determine if the sender is a player (and an op), or the console. - boolean player = (sender instanceof Player); - boolean op = player && ((Player) sender).isOp(); - boolean console = (sender instanceof ConsoleCommandSender); - - // Cast the sender to Player if possible. - Player p = (player) ? (Player) sender : null; - - if (args.length == 0) - { - List arenas = am.getEnabledArenas(); - if (arenas == null || arenas.isEmpty()) - MAUtils.tellPlayer(p, "There are no enabled arenas to join!"); - else - MAUtils.tellPlayer(p, "Use /ma join to join an arena."); - return true; - } - - // Grab the command base and any arguments. - String base = args[0].toLowerCase(); - String arg1 = (args.length > 1) ? args[1].toLowerCase() : ""; - String arg2 = (args.length > 2) ? args[2].toLowerCase() : ""; - String arg3 = (args.length > 3) ? args[3].toLowerCase() : ""; - - - - /*//////////////////////////////////////////////////////////////// - // - // Basics - // - ////////////////////////////////////////////////////////////////*/ - - if (base.equals("spawn")) - { - p.getWorld().setSpawnLocation(p.getLocation().getBlockX(), p.getLocation().getBlockY() + 1, p.getLocation().getBlockZ()); - } - - /* - * Player join - */ - if (base.equals("join") || base.equals("j")) - { - if (!player || !plugin.has(p, "mobarena.use.join")) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - List arenas = am.getEnabledArenas(); - if (!am.enabled || arenas.size() < 1) - { - MAUtils.tellPlayer(p, Msg.JOIN_NOT_ENABLED); - return true; - } - - // Grab the arena to join - Arena arena = arenas.size() == 1 ? arenas.get(0) : am.getArenaWithName(arg1); - - // Run a couple of basic sanity checks - if (!sanityChecks(p, arena, arg1, arenas)) - return true; - - // Run a bunch of per-arena sanity checks - if (!arena.canJoin(p)) - return true; - - // If player is in a boat/minecart, eject! - if (p.isInsideVehicle()) - p.leaveVehicle(); - - // Take entry fee and store inventory - arena.takeFee(p); - if (!arena.emptyInvJoin) MAUtils.storeInventory(p); - - // If player is in a bed, unbed! - if (p.isSleeping()) - { - p.kickPlayer("Banned for life... Nah, just don't join from a bed ;)"); - return true; - } - - // Join the arena! - arena.playerJoin(p, p.getLocation()); - - MAUtils.tellPlayer(p, Msg.JOIN_PLAYER_JOINED); - if (!arena.entryFee.isEmpty()) - MAUtils.tellPlayer(p, Msg.JOIN_FEE_PAID.toString(MAUtils.listToString(arena.entryFee, plugin))); - if (arena.hasPaid.contains(p)) - arena.hasPaid.remove(p); - - return true; - } - - /* - * Player leave - */ - if (base.equals("leave") || base.equals("l")) - { - if (!player || !plugin.has(p, "mobarena.use.leave")) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!am.arenaMap.containsKey(p)) - { - Arena arena = am.getArenaWithSpectator(p); - if (arena != null) - { - arena.playerLeave(p); - MAUtils.tellPlayer(p, Msg.LEAVE_PLAYER_LEFT); - return true; - } - - MAUtils.tellPlayer(p, Msg.LEAVE_NOT_PLAYING); - return true; - } - - Arena arena = am.arenaMap.get(p); - arena.playerLeave(p); - MAUtils.tellPlayer(p, Msg.LEAVE_PLAYER_LEFT); - return true; - } - - /* - * Player spectate - */ - if (base.equals("spectate") || base.equals("spec")) - { - if (!player || !plugin.has(p, "mobarena.use.spectate")) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - List arenas = am.getEnabledArenas(); - if (!am.enabled || arenas.size() < 1) - { - MAUtils.tellPlayer(p, Msg.JOIN_NOT_ENABLED); - return true; - } - - // Grab the arena to join - Arena arena = arenas.size() == 1 ? arenas.get(0) : am.getArenaWithName(arg1); - - // Run a couple of basic sanity checks - if (!sanityChecks(p, arena, arg1, arenas)) - return true; - - // Run a bunch of arena-specific sanity-checks - if (!arena.canSpec(p)) - return true; - - // If player is in a boat/minecart, eject! - if (p.isInsideVehicle()) - p.leaveVehicle(); - - // If player is in a bed, unbed! - if (p.isSleeping()) - { - p.kickPlayer("Banned for life... Nah, just don't join from a bed ;)"); - return true; - } - - // Spectate the arena! - arena.playerSpec(p, p.getLocation()); - - MAUtils.tellPlayer(p, Msg.SPEC_PLAYER_SPECTATE); - return true; - } - - /* - * Prints a list of all arenas. - */ - if (base.equals("arenas")) - { - String list = MAUtils.listToString(player ? am.getPermittedArenas(p) : am.arenas, plugin); - MAUtils.tellPlayer(sender, Msg.MISC_LIST_ARENAS.toString(list)); - return true; - } - - /* - * Prints a list of all live players in all arenas, or live players in a specific arena. - */ - if (base.equals("players") || base.equals("list")) - { - if (!arg1.isEmpty()) - { - Arena arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - String list = MAUtils.listToString(arena.getLivingPlayers(), plugin); - MAUtils.tellPlayer(sender, Msg.MISC_LIST_PLAYERS.toString(list)); - } - else - { - StringBuffer buffy = new StringBuffer(); - List players = new LinkedList(); - for (Arena arena : am.arenas) - players.addAll(arena.getLivingPlayers()); - buffy.append(MAUtils.listToString(players, plugin)); - MAUtils.tellPlayer(sender, Msg.MISC_LIST_PLAYERS.toString(buffy.toString())); - } - return true; - } - - /* - * Prints a list of all non-ready players in current arena, or non-ready players in a specific arena. - */ - if (base.equals("notready")) - { - Arena arena; - if (!arg1.isEmpty()) - { - arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - } - else if (player) - { - arena = am.getArenaWithPlayer(p); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.LEAVE_NOT_PLAYING); - return true; - } - } - else - { - MAUtils.tellPlayer(sender, "Usage: /ma notready "); - return true; - } - - String list = MAUtils.listToString(arena.getNonreadyPlayers(), plugin); - MAUtils.tellPlayer(sender, Msg.MISC_LIST_PLAYERS.toString(list)); - return true; - } - - - - /*//////////////////////////////////////////////////////////////// - // - // Setup & Reload - // - ////////////////////////////////////////////////////////////////*/ - - /* - * Enable or disable arena(s) - */ - if ((base.equals("enable") || base.equals("disable"))) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.enable")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!arg1.isEmpty()) - { - Arena arena = am.getArenaWithName(arg1); - if (arena != null) - { - arena.enabled = base.equals("enable"); - arena.serializeConfig(); - MAUtils.tellPlayer(sender, "Arena '" + arena.configName() + "' " + ((arena.enabled) ? ChatColor.GREEN : ChatColor.RED) + base + "d"); - } - else - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - } - } - else - { - am.enabled = base.equals("enable"); - am.serializeSettings(); - MAUtils.tellPlayer(sender, "All arenas " + ((am.enabled) ? ChatColor.GREEN : ChatColor.RED) + base + "d"); - } - return true; - } - - /* - * Kick player from whichever arena they are in. - */ - if (base.equals("kick")) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.kick")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (arg1.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma kick "); - return true; - } - - Arena arena = am.getArenaWithPlayer(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "That player is not in an arena."); - return true; - } - else - { - Player pl = server.getPlayer(arg1); - am.arenaMap.remove(pl); - arena.playerLeave(pl); - MAUtils.tellPlayer(sender, "Player '" + arg1 + "' was kicked from arena '" + arena.configName() + "'."); - MAUtils.tellPlayer(pl, "You were kicked by " + ((player) ? p.getName() : "the server.")); - return true; - } - } - - /* - * Restore a player's inventory. - */ - if (base.equals("restore")) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.restore")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!arg1.isEmpty()) - { - if (am.getArenaWithPlayer(arg1) != null) - { - MAUtils.tellPlayer(sender, "Player is currently in an arena."); - return true; - } - - if (MAUtils.restoreInventory(Bukkit.getServer().getPlayer(arg1))); - MAUtils.tellPlayer(sender, "Restored " + arg1 + "'s inventory!"); - return true; - } - } - - /* - * Force start/end arenas. - */ - if (base.equals("force")) - { - if (arg1.equals("end")) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.force.end")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (arg2.isEmpty()) - { - for (Arena arena : am.arenas) - arena.forceEnd(); - - am.arenaMap.clear(); - return true; - } - - Arena arena = am.getArenaWithName(arg2); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - if (arena.getAllPlayers().isEmpty()) - { - MAUtils.tellPlayer(sender, Msg.FORCE_END_EMPTY); - return true; - } - - arena.forceEnd(); - MAUtils.tellPlayer(sender, Msg.FORCE_END_ENDED); - return true; - } - else if (arg1.equals("start")) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.force.start")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (arg2.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma force start "); - return true; - } - - Arena arena = am.getArenaWithName(arg2); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - if (arena.running) - { - MAUtils.tellPlayer(sender, Msg.FORCE_START_RUNNING); - return true; - } - if (arena.readyPlayers.isEmpty()) - { - MAUtils.tellPlayer(sender, Msg.FORCE_START_NOT_READY); - return true; - } - - arena.forceStart(); - MAUtils.tellPlayer(sender, Msg.FORCE_START_STARTED); - return true; - } - else - { - MAUtils.tellPlayer(sender, "Usage: /ma force [start|end] ()"); - return true; - } - } - - /* - * Reload the config-file. - */ - if (base.equals("config")) - { - if (!console && !(player && plugin.has(p, "mobarena.admin.config.reload")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!arg1.equals("reload")) - { - MAUtils.tellPlayer(sender, "Usage: /ma config reload"); - return true; - } - - am.updateAll(); - MAUtils.tellPlayer(sender, "Config reloaded."); - return true; - } - - /* - * Get the current arena, and list all other arenas. - */ - if (base.equals("arena")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.arena")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - MAUtils.tellPlayer(sender, "Currently selected arena: " + ChatColor.GREEN + am.selectedArena.configName()); - - StringBuffer buffy = new StringBuffer(); - if (am.arenas.size() > 1) - { - for (Arena arena : am.arenas) - if (!arena.equals(am.selectedArena)) - buffy.append(arena.configName() + " "); - } - else buffy.append(Msg.MISC_NONE); - - MAUtils.tellPlayer(sender, "Other arenas: " + buffy.toString()); - return true; - } - - /* - * Set the current arena - */ - if (base.equals("setarena")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.setarena")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma setarena "); - return true; - } - - Arena arena = am.getArenaWithName(arg1); - if (arena != null) - { - am.selectedArena = arena; - MAUtils.tellPlayer(sender, "Currently selected arena: " + arena.configName()); - } - else - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - } - return true; - } - - /* - * Create a new arena, and set the current arena to this new arena. - */ - if (base.equals("addarena")) - { - if (!(player && plugin.has(p, "mobarena.setup.addarena")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma addarena "); - return true; - } - - Arena arena = am.getArenaWithName(arg1); - if (arena != null) - { - MAUtils.tellPlayer(sender, "An arena with that name already exists."); - return true; - } - - arena = am.createArenaNode(arg1, p.getWorld()); - am.arenas.add(arena); - am.selectedArena = arena; - - MAUtils.tellPlayer(sender, "New arena with name '" + arg1 + "' created!"); - return true; - } - - if (base.equals("delarena")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.delarena")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma delarena "); - return true; - } - if (am.arenas.size() < 2) - { - MAUtils.tellPlayer(sender, "At least one arena must exist."); - return true; - } - - Arena arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "There is no arena with that name."); - return true; - } - - am.removeArenaNode(arg1); - am.arenas.remove(arena); - am.selectedArena = (am.selectedArena.equals(arena)) ? am.arenas.get(0) : am.selectedArena; - - MAUtils.tellPlayer(sender, "Arena '" + arena.configName() + "' deleted."); - return true; - } - - /* - * Enable or disable protection - */ - if (base.equals("protect")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.protect")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - Arena arena; - - // No arguments - if (arg1.isEmpty()) - { - arena = am.selectedArena; - arena.protect = !arena.protect; - } - - // One argument - else if (arg2.isEmpty()) - { - // true/false - if (arg1.equals("true") || arg1.equals("false")) - { - arena = am.selectedArena; - arena.protect = arg1.equals("true"); - } - // Arena name - else - { - arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "There is no arena with that name."); - MAUtils.tellPlayer(sender, "Usage: /ma protect ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma protect ([true|false])"); - return true; - } - arena.protect = !arena.protect; - } - } - - // Two arguments - else - { - if (!(arg2.equals("true") || arg2.equals("false"))) - { - MAUtils.tellPlayer(sender, "Usage: /ma protect ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma protect ([true|false])"); - return true; - } - arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "There is no arena with that name."); - MAUtils.tellPlayer(sender, "Usage: /ma protect ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma protect ([true|false])"); - return true; - } - arena.protect = arg2.equals("true"); - } - - arena.serializeConfig(); - MAUtils.tellPlayer(sender, "Protection for arena '" + arena.configName() + "': " + ((arena.protect) ? ChatColor.GREEN + "on" : ChatColor.RED + "off")); - return true; - } - - if (base.equals("editarena")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.editarena")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - Arena arena; - - // No arguments. - if (arg1.isEmpty()) - { - arena = am.selectedArena; - arena.edit = !arena.edit; - } - - // One argument. - else if (arg2.isEmpty()) - { - // Argument is [true|false] - if (arg1.equals("true") || arg1.equals("false")) - { - arena = am.selectedArena; - arena.edit = arg1.equals("true"); - } - // Argument is - else - { - arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "There is no arena with that name."); - MAUtils.tellPlayer(sender, "Usage: /ma editarena ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma editarena ([true|false])"); - return true; - } - arena.edit = !arena.edit; - } - } - - // Two arguments - else - { - if (!(arg2.equals("true") || arg2.equals("false"))) - { - MAUtils.tellPlayer(sender, "Usage: /ma editarena ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma editarena ([true|false])"); - return true; - } - arena = am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, "There is no arena with that name."); - MAUtils.tellPlayer(sender, "Usage: /ma editarena ([true|false])"); - MAUtils.tellPlayer(sender, " or /ma editarena ([true|false])"); - return true; - } - arena.edit = arg2.equals("true"); - } - - MAUtils.tellPlayer(sender, "Edit mode for arena '" + arena.configName() + "': " + ((arena.edit) ? ChatColor.GREEN + "true" : ChatColor.RED + "false")); - if (arena.edit) MAUtils.tellPlayer(sender, "Remember to turn it back off after editing!"); - return true; - } - - /* - * Set region points [p1|p2] for the current arena. - */ - if (base.equals("setregion")) - { - if (!(player && plugin.has(p, "mobarena.setup.setregion")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!(arg1.equals("p1") || arg1.equals("p2"))) - { - MAUtils.tellPlayer(sender, "Usage: /ma setregion [p1|p2]"); - return true; - } - - MAUtils.setArenaCoord(plugin.getMAConfig(), am.selectedArena, arg1, p.getLocation()); - MAUtils.tellPlayer(sender, "Set region point " + arg1 + " for arena '" + am.selectedArena.configName() + "'"); - return true; - } - - /* - * Expand the region (arg1) [up|down|out] - */ - if (base.equals("expandregion")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.expandregion")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (args.length != 3 || !arg1.matches("(-)?[0-9]+")) - { - MAUtils.tellPlayer(sender, "Usage: /ma expandregion [up|down|out]"); - return true; - } - if (am.selectedArena.p1 == null || am.selectedArena.p2 == null) - { - MAUtils.tellPlayer(sender, "You must first define p1 and p2"); - return true; - } - - if (arg2.equals("up")) - { - am.selectedArena.p2.setY(Math.min(127, am.selectedArena.p2.getY() + Integer.parseInt(arg1))); - } - else if (arg2.equals("down")) - { - am.selectedArena.p1.setY(Math.max(0, am.selectedArena.p1.getY() - Integer.parseInt(arg1))); - } - else if (arg2.equals("out")) - { - am.selectedArena.p1.setX(am.selectedArena.p1.getX() - Integer.parseInt(arg1)); - am.selectedArena.p1.setZ(am.selectedArena.p1.getZ() - Integer.parseInt(arg1)); - am.selectedArena.p2.setX(am.selectedArena.p2.getX() + Integer.parseInt(arg1)); - am.selectedArena.p2.setZ(am.selectedArena.p2.getZ() + Integer.parseInt(arg1)); - } - else - { - MAUtils.tellPlayer(sender, "Usage: /ma expandregion [up|down|out]"); - return true; - } - - // In case of a "negative" region, fix it! - MAUtils.fixRegion(plugin.getMAConfig(), am.selectedArena.world, am.selectedArena); - - MAUtils.tellPlayer(sender, "Region for '" + am.selectedArena.configName() + "' expanded " + arg2 + " by " + arg1 + " blocks."); - am.selectedArena.serializeConfig(); - am.selectedArena.load(plugin.getMAConfig()); - return true; - } - - if (base.equals("showregion")) - { - if (!(player && plugin.has(p, "mobarena.setup.showregion")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - Arena arena = am.getArenaAtLocation(p.getLocation()); - if (arena == null) - arena = am.selectedArena; - - if (arena.p1 == null || arena.p2 == null) - { - MAUtils.tellPlayer(sender, "The region is not defined for the selected arena."); - return true; - } - if (showingRegion) - { - MAUtils.tellPlayer(sender, "Already showing region."); - return true; - } - - Material mat = Material.WOOL; - byte color = (byte)0; - - if (arg1.equals("glowstone")) - mat = Material.GLOWSTONE; - else if (arg1.equals("white") || arg1.equals("red") || arg1.equals("blue")) - color = DyeColor.valueOf(arg1.toUpperCase()).getData(); - else if (arg1.equals("green")) // Dark green sucks - color = 0x5; - else - mat = Material.GLASS; - - // Set the variable so we don't overwrite the region. - showingRegion = true; - - // Show the frame. - final World world = arena.world; - final Set blocks = MAUtils.showRegion(p, world, arena.p1, arena.p2, mat.getId(), color); - final Player fp = p; - - // And hide the frame. - plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - for (int[] buffer : blocks) - fp.sendBlockChange(new Location(world, buffer[0], buffer[1], buffer[2]), buffer[3], (byte) 0); - //world.getBlockAt(buffer[0], buffer[1], buffer[2]).setTypeIdAndData(buffer[3], (byte) 0, false); - showingRegion = false; - } - }, 2*20); - - return true; - } - - if (base.equals("setlobbyregion")) - { - if (!(player && plugin.has(p, "mobarena.setup.setlobbyregion")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - if (!(arg1.equals("l1") || arg1.equals("l2"))) - { - MAUtils.tellPlayer(sender, "Usage: /ma setlobbyregion [l1|l2]"); - return true; - } - - MAUtils.setArenaCoord(plugin.getMAConfig(), am.selectedArena, arg1, p.getLocation()); - MAUtils.tellPlayer(sender, "Set lobby point " + arg1 + " for arena '" + am.selectedArena.configName() + "'"); - return true; - } - - if (base.equals("expandlobbyregion")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.expandlobbyregion")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (args.length != 3 || !arg1.matches("[0-9]+")) - { - MAUtils.tellPlayer(sender, "Usage: /ma expandlobbyregion [up|down|out]"); - return true; - } - if (am.selectedArena.l1 == null || am.selectedArena.l2 == null) - { - MAUtils.tellPlayer(sender, "You must first define l1 and l2"); - return true; - } - - if (arg2.equals("up")) - { - am.selectedArena.l2.setY(Math.min(127, am.selectedArena.l2.getY() + Integer.parseInt(arg1))); - } - else if (arg2.equals("down")) - { - am.selectedArena.l1.setY(Math.max(0, am.selectedArena.l1.getY() - Integer.parseInt(arg1))); - } - else if (arg2.equals("out")) - { - am.selectedArena.l1.setX(am.selectedArena.l1.getX() - Integer.parseInt(arg1)); - am.selectedArena.l1.setZ(am.selectedArena.l1.getZ() - Integer.parseInt(arg1)); - am.selectedArena.l2.setX(am.selectedArena.l2.getX() + Integer.parseInt(arg1)); - am.selectedArena.l2.setZ(am.selectedArena.l2.getZ() + Integer.parseInt(arg1)); - } - else - { - MAUtils.tellPlayer(sender, "Usage: /ma expandlobbyregion [up|down|out]"); - return true; - } - - MAUtils.tellPlayer(sender, "Lobby region for '" + am.selectedArena.configName() + "' expanded " + arg2 + " by " + arg1 + " blocks."); - am.selectedArena.serializeConfig(); - am.selectedArena.load(plugin.getMAConfig()); - return true; - } - - /* - * Set warp points [arena|lobby|spectator] for the current arena. - */ - if (base.equals("setwarp")) - { - if (!(player && plugin.has(p, "mobarena.setup.setwarp")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (!(arg1.equals("arena") || arg1.equals("lobby") || arg1.equals("spectator"))) - { - MAUtils.tellPlayer(sender, "Usage: /ma setwarp [arena|lobby|spectator]"); - return true; - } - - MAUtils.setArenaCoord(plugin.getMAConfig(), am.selectedArena, arg1, p.getLocation()); - MAUtils.tellPlayer(sender, "Warp point " + arg1 + " was set for arena '" + am.selectedArena.configName() + "'"); - MAUtils.tellPlayer(sender, "Type /ma checkdata to see if you're missing anything..."); - return true; - } - - /* - * List all the current spawnpoints for the current arena. - */ - if (base.equals("spawnpoints")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.spawnpoints")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - StringBuffer buffy = new StringBuffer(); - Set spawnpoints = plugin.getMAConfig().getKeys("arenas." + am.selectedArena.configName() + ".coords.spawnpoints"); - - if (spawnpoints != null) - { - for (String s : spawnpoints) - { - buffy.append(s); - buffy.append(" "); - } - } - else - { - buffy.append(Msg.MISC_NONE); - } - - MAUtils.tellPlayer(sender, "Spawnpoints for arena '" + am.selectedArena.configName() + "': " + buffy.toString()); - return true; - } - - /* - * Add a spawnpoint for the current arena. - */ - if (base.equals("addspawn")) - { - if (!(player && plugin.has(p, "mobarena.setup.addspawn")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1 == null || !arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) - { - MAUtils.tellPlayer(sender, "Usage: /ma addspawn "); - return true; - } - - MAUtils.setArenaCoord(plugin.getMAConfig(), am.selectedArena, "spawnpoints." + arg1, p.getLocation()); - MAUtils.tellPlayer(sender, "Spawnpoint " + arg1 + " added for arena \"" + am.selectedArena.configName() + "\""); - return true; - } - - /* - * Delete a spawnpoint for the current arena. - */ - if (base.equals("delspawn")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.delspawn")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1 == null || !arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) - { - MAUtils.tellPlayer(sender, "Usage: /ma delspawn "); - return true; - } - - if (MAUtils.delArenaCoord(plugin.getMAConfig(), am.selectedArena, "spawnpoints." + arg1)) - MAUtils.tellPlayer(sender, "Spawnpoint " + arg1 + " deleted for arena '" + am.selectedArena.configName() + "'"); - else - MAUtils.tellPlayer(sender, "Could not find the spawnpoint " + arg1 + "for the arena '" + am.selectedArena.configName() + "'"); - return true; - } - - if (base.equals("containers")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.containers")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - StringBuffer buffy = new StringBuffer(); - Set containers = plugin.getMAConfig().getKeys("arenas." + am.selectedArena.configName() + ".coords.containers"); - - if (containers != null) - { - for (String s : containers) - { - buffy.append(s); - buffy.append(" "); - } - } - else - { - buffy.append(Msg.MISC_NONE); - } - - MAUtils.tellPlayer(sender, "Containers for arena '" + am.selectedArena.configName() + "': " + buffy.toString()); - return true; - } - - if (base.equals("addcontainer")) - { - if (!(player && plugin.has(p, "mobarena.setup.addcontainer")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1 == null || !arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) - { - MAUtils.tellPlayer(sender, "Usage: /ma addcontainer "); - return true; - } - if (!(p.getTargetBlock(null, 50).getState() instanceof ContainerBlock)) - { - MAUtils.tellPlayer(sender, "You must look at container."); - return true; - } - - MAUtils.setArenaCoord(plugin.getMAConfig(), am.selectedArena, "containers." + arg1, p.getTargetBlock(null, 50).getLocation()); - MAUtils.tellPlayer(sender, "Container '" + arg1 + "' added for arena \"" + am.selectedArena.configName() + "\""); - return true; - } - - if (base.equals("delcontainer")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.delcontainer")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1 == null || !arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) - { - MAUtils.tellPlayer(sender, "Usage: /ma delcontainer "); - return true; - } - - if (MAUtils.delArenaCoord(plugin.getMAConfig(), am.selectedArena, "containers." + arg1)) - MAUtils.tellPlayer(sender, "Container '" + arg1 + "' deleted for arena '" + am.selectedArena.configName() + "'"); - else - MAUtils.tellPlayer(sender, "Could not find the container '" + arg1 + "' for arena '" + am.selectedArena.configName() + "'"); - return true; - } - - if (base.equals("checkdata")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.checkdata")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - Arena arena = arg1.isEmpty() ? am.selectedArena : am.getArenaWithName(arg1); - if (arena == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - MAUtils.checkData(arena, sender); - return true; - } - - if (base.equals("auto-generate") || base.equals("autogenerate")) - { - if (!(player && plugin.has(p, "mobarena.setup.autogenerate")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1 == null || !arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) - { - MAUtils.tellPlayer(sender, "Usage: /ma auto-generate "); - return true; - } - if (am.getArenaWithName(arg1) != null) - { - MAUtils.tellPlayer(sender, "An arena with that name already exists."); - return true; - } - - if (MAUtils.doooooItHippieMonster(p.getLocation(), 13, arg1, plugin)) - MAUtils.tellPlayer(sender, "Arena with name '" + arg1 + "' generated."); - else - MAUtils.tellPlayer(sender, "Could not auto-generate arena."); - return true; - } - - if (base.equals("auto-degenerate") || base.equals("autodegenerate")) - { - if (!console && !(player && plugin.has(p, "mobarena.setup.autodegenerate")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (arg1.isEmpty()) - { - MAUtils.tellPlayer(sender, "Usage: /ma auto-degenerate "); - return true; - } - if (am.arenas.size() < 2) - { - MAUtils.tellPlayer(sender, "At least one arena must exist!"); - return true; - } - if (am.getArenaWithName(arg1) == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - if (MAUtils.undoItHippieMonster(arg1, plugin, true)) - MAUtils.tellPlayer(sender, "Arena with name '" + arg1 + "' degenerated."); - else - MAUtils.tellPlayer(sender, "Could not degenerate arena."); - return true; - } - - if (base.equals("dooooo") && arg1.equals("it") && arg2.equals("hippie") && arg3.equals("monster")) - { - if (!(player && plugin.has(p, "mobarena.setup.autogenerate")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - - String name = "a0"; - do name = name.substring(0,1) + (Integer.parseInt(name.substring(1,2)) + 1); - while (am.getArenaWithName(name) != null); - - if (MAUtils.doooooItHippieMonster(p.getLocation(), 13, name, plugin)) - MAUtils.tellPlayer(sender, "Arena with name '" + name + "' generated."); - else - MAUtils.tellPlayer(sender, "Could not auto-generate arena."); - return true; - } - - if (base.equals("undo") && arg1.equals("it") && arg2.equals("hippie") && arg3.equals("monster")) - { - if (!(player && plugin.has(p, "mobarena.setup.autodegenerate")) && !op) - { - MAUtils.tellPlayer(sender, Msg.MISC_NO_ACCESS); - return true; - } - if (am.arenas.size() < 2) - { - MAUtils.tellPlayer(sender, "At least one arena must exist!"); - return true; - } - if (am.getArenaWithName("a1") == null) - { - MAUtils.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); - return true; - } - - String name = "a1"; - while (am.getArenaWithName(name) != null) - name = name.substring(0,1) + (Integer.parseInt(name.substring(1,2)) + 1); - name = name.substring(0,1) + (Integer.parseInt(name.substring(1,2)) - 1); - - if (MAUtils.undoItHippieMonster(name, plugin, true)) - MAUtils.tellPlayer(sender, "Arena with name '" + name + "' degenerated."); - else - MAUtils.tellPlayer(sender, "Could not degenerate arena."); - return true; - } - - MAUtils.tellPlayer(sender, "Command not found."); - return true; - } - - private boolean sanityChecks(Player p, Arena arena, String arg1, List arenas) - { - if (arenas.size() > 1 && arg1.isEmpty()) - MAUtils.tellPlayer(p, Msg.JOIN_ARG_NEEDED); - else if (arena == null) - MAUtils.tellPlayer(p, Msg.ARENA_DOES_NOT_EXIST); - else if (am.arenaMap.containsKey(p) && (am.arenaMap.get(p).arenaPlayers.contains(p) || am.arenaMap.get(p).lobbyPlayers.contains(p))) - MAUtils.tellPlayer(p, Msg.JOIN_IN_OTHER_ARENA); - else - return true; - - return false; - } -} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/MAEntityListener.java b/src/com/garbagemule/MobArena/MAEntityListener.java deleted file mode 100644 index 7c853d4..0000000 --- a/src/com/garbagemule/MobArena/MAEntityListener.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.garbagemule.MobArena; - -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EndermanPickupEvent; -import org.bukkit.event.entity.EndermanPlaceEvent; -import org.bukkit.event.entity.EntityCombustEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.entity.EntityListener; -import org.bukkit.event.entity.EntityRegainHealthEvent; -import org.bukkit.event.entity.EntityTargetEvent; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.ArenaMaster; - -public class MAEntityListener extends EntityListener -{ - private ArenaMaster am; - - public MAEntityListener(ArenaMaster am) - { - this.am = am; - } - - public void onEntityRegainHealth(EntityRegainHealthEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityRegainHealth(event); - } - - public void onEntityDeath(EntityDeathEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityDeath(event); - } - - public void onEntityDamage(EntityDamageEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityDamage(event); - } - - public void onCreatureSpawn(CreatureSpawnEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onCreatureSpawn(event); - } - - public void onEntityExplode(EntityExplodeEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityExplode(event); - } - - public void onEntityCombust(EntityCombustEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityCombust(event); - } - - public void onEntityTarget(EntityTargetEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEntityTarget(event); - } - - public void onEndermanPickup(EndermanPickupEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEndermanPickup(event); - } - - public void onEndermanPickup(EndermanPlaceEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onEndermanPlace(event); - } -} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/MAListener.java b/src/com/garbagemule/MobArena/MAListener.java deleted file mode 100644 index 07e90c2..0000000 --- a/src/com/garbagemule/MobArena/MAListener.java +++ /dev/null @@ -1,724 +0,0 @@ -package com.garbagemule.MobArena; - -import java.util.LinkedList; -import java.util.List; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.ContainerBlock; -import org.bukkit.block.Sign; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Projectile; -import org.bukkit.entity.Slime; -import org.bukkit.entity.Wolf; -import org.bukkit.event.Event.Result; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.block.SignChangeEvent; -import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.event.entity.EndermanPickupEvent; -import org.bukkit.event.entity.EndermanPlaceEvent; -import org.bukkit.event.entity.EntityCombustEvent; -import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.entity.EntityRegainHealthEvent; -import org.bukkit.event.entity.EntityTargetEvent; -import org.bukkit.event.entity.EntityDamageEvent.DamageCause; -import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; -import org.bukkit.event.entity.EntityTargetEvent.TargetReason; -import org.bukkit.event.player.PlayerAnimationEvent; -import org.bukkit.event.player.PlayerBucketEmptyEvent; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.ItemStack; -import org.bukkit.material.Attachable; -import org.bukkit.material.Bed; -import org.bukkit.material.Door; -import org.bukkit.material.Redstone; - -import com.garbagemule.MobArena.leaderboards.Leaderboard; -import com.garbagemule.MobArena.repairable.*; - -public class MAListener implements ArenaListener -{ - private MobArena plugin; - private Arena arena; - - public MAListener(Arena arena, MobArena plugin) - { - this.arena = arena; - this.plugin = plugin; - } - - public void onBlockBreak(BlockBreakEvent event) - { - if (onBlockDestroy(event)) - return; - - event.setCancelled(true); - } - - public void onBlockBurn(BlockBurnEvent event) - { - if (onBlockDestroy(event)) - return; - - event.setCancelled(true); - } - - private boolean onBlockDestroy(BlockEvent event) - { - if (!arena.inRegion(event.getBlock().getLocation()) || arena.edit || (!arena.protect && arena.running)) - return true; - - Block b = event.getBlock(); - if (arena.blocks.remove(b) || b.getType() == Material.TNT) - return true; - - if (arena.softRestore && arena.running) - { - BlockState state = b.getState(); - - Repairable r = null; - if (state instanceof ContainerBlock) - r = new RepairableContainer(state); - else if (state instanceof Sign) - r = new RepairableSign(state); - else if (state.getData() instanceof Attachable) - r = new RepairableAttachable(state); - else - r = new RepairableBlock(state); - - arena.repairables.add(r); - - if (!arena.softRestoreDrops) - b.setTypeId(0); - - return true; - } - - return false; - } - - public void onBlockPlace(BlockPlaceEvent event) - { - Block b = event.getBlock(); - - if (!arena.inRegion(b.getLocation()) || arena.edit) - return; - - if (arena.running && arena.inArena(event.getPlayer())) - { - arena.blocks.add(b); - Material mat = b.getType(); - - if (mat == Material.WOODEN_DOOR || mat == Material.IRON_DOOR_BLOCK) - arena.blocks.add(b.getRelative(0,1,0)); - - return; - } - - // If the arena isn't running, or if the player isn't in the arena, cancel. - event.setCancelled(true); - } - - public void onBlockIgnite(BlockIgniteEvent event) - { - if (!arena.inRegion(event.getBlock().getLocation())) - return; - - switch (event.getCause()) - { - case LIGHTNING: - event.setCancelled(true); - break; - case SPREAD: - case FLINT_AND_STEEL: - if (arena.running) - arena.blocks.add(event.getBlock()); - else - event.setCancelled(true); - break; - default: - break; - } - } - - public void onSignChange(SignChangeEvent event) - { - arena.leaderboard = new Leaderboard(plugin, arena, event.getBlock().getLocation()); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "leaderboard", event.getBlock().getLocation()); - MAUtils.tellPlayer(event.getPlayer(), "Leaderboard made. Now set up the stat signs!"); - } - - public void onCreatureSpawn(CreatureSpawnEvent event) - { - if (!arena.inRegion(event.getLocation())) // || event.getSpawnReason() == SpawnReason.CUSTOM) - return; - - LivingEntity entity = (LivingEntity) event.getEntity(); - if (arena.running && entity instanceof Slime) - arena.monsters.add(entity); - - // If running == true, setCancelled(false), and vice versa. - event.setCancelled(!arena.running); - } - - public void onEntityExplode(EntityExplodeEvent event) - { - if (!arena.monsters.contains(event.getEntity()) && !arena.inRegionRadius(event.getLocation(), 10)) - return; - - arena.monsters.remove(event.getEntity()); - - // Cancel if the arena isn't running - if (!arena.running) - { - event.setCancelled(true); - return; - } - - // Uncancel, just in case. - event.setCancelled(false); - - // If the arena isn't destructible, just clear the blocklist. - if (!arena.softRestore && arena.protect) - { - List blocks = new LinkedList(arena.blocks); - event.blockList().retainAll(blocks); - return; - } - - if (!arena.softRestoreDrops) - event.setYield(0); - - // Handle all the blocks in the block list. - for (Block b : event.blockList()) - { - BlockState state = b.getState(); - - if (state.getData() instanceof Door && ((Door) state.getData()).isTopHalf()) - state = b.getRelative(BlockFace.DOWN).getState(); - else if (state.getData() instanceof Bed && ((Bed) state.getData()).isHeadOfBed()) - state = b.getRelative(((Bed) state.getData()).getFacing().getOppositeFace()).getState(); - - // Create a Repairable from the block. - Repairable r = null; - if (state instanceof ContainerBlock) - r = new RepairableContainer(state); - else if (state instanceof Sign) - r = new RepairableSign(state); - else if (state.getData() instanceof Bed) - r = new RepairableBed(state); - else if (state.getData() instanceof Door) - r = new RepairableDoor(state); - else if (state.getData() instanceof Attachable || state.getData() instanceof Redstone) - r = new RepairableAttachable(state); - else - r = new RepairableBlock(state); - - // Cakes and liquids should just get removed. If player-placed block, drop as item. - Material mat = state.getType(); - if (mat == Material.CAKE_BLOCK || mat == Material.WATER || mat == Material.LAVA) - arena.blocks.remove(b); - else if (arena.blocks.remove(b)) - arena.world.dropItemNaturally(b.getLocation(), new ItemStack(state.getTypeId(), 1)); - else if (arena.softRestore) - arena.repairables.add(r); - else - arena.queueRepairable(r); - } - } - - - /****************************************************** - * - * DEATH LISTENERS - * - ******************************************************/ - - public void onEntityDeath(EntityDeathEvent event) - { - if (event.getEntity() instanceof Player) - onPlayerDeath(event, (Player) event.getEntity()); - - else if (arena.monsters.remove(event.getEntity())) - onMonsterDeath(event); - } - - private void onPlayerDeath(EntityDeathEvent event, Player player) - { - if (arena.inArena(player) || arena.lobbyPlayers.contains(player)) - { - event.getDrops().clear(); - arena.playerDeath(player); - } - } - - private void onMonsterDeath(EntityDeathEvent event) - { - EntityDamageEvent e1 = event.getEntity().getLastDamageCause(); - EntityDamageByEntityEvent e2 = (e1 instanceof EntityDamageByEntityEvent) ? (EntityDamageByEntityEvent) e1 : null; - Entity damager = (e2 != null) ? e2.getDamager() : null; - - if (damager instanceof Player) - arena.playerKill((Player) damager); - - event.getDrops().clear(); - arena.resetIdleTimer(); - return; - } - - - /****************************************************** - * - * DAMAGE LISTENERS - * - ******************************************************/ - - public void onEntityDamage(EntityDamageEvent event) - { - if (!arena.running) return; - - EntityDamageByEntityEvent e = (event instanceof EntityDamageByEntityEvent) ? (EntityDamageByEntityEvent) event : null; - Entity damagee = event.getEntity(); - Entity damager = null; - if (e != null) - { - damager = e.getDamager(); - if (damager instanceof Projectile) - damager = ((Projectile) damager).getShooter(); - } - - event.setCancelled(false); - - // Pet wolf - if (damagee instanceof Wolf && arena.pets.contains(damagee)) - onPetDamage(event, (Wolf) damagee, damager); - - // Player - else if (damagee instanceof Player) - onPlayerDamage(event, (Player) damagee, damager); - - // Monster - else if (arena.monsters.contains(damagee)) - onMonsterDamage(event, damagee, damager); - } - - private void onPlayerDamage(EntityDamageEvent event, Player player, Entity damager) - { - // Cancel all damage in the lobby - if (arena.lobbyPlayers.contains(player)) - event.setCancelled(true); - - // If not in the lobby or the arena, return - else if (!arena.inArena(player)) - return; - - // Cancel block explosion damage if detonate-damage: false - else if (!arena.detDamage && event.getCause() == DamageCause.BLOCK_EXPLOSION) - event.setCancelled(true); - - // If PvP is disabled and damager is a player, cancel damage - else if (!arena.pvp && damager instanceof Player) - event.setCancelled(true); - - // Log damage - if (!event.isCancelled()) - arena.arenaPlayerMap.get(player).getStats().dmgTaken += event.getDamage(); - } - - private void onPetDamage(EntityDamageEvent event, Wolf pet, Entity damager) - { - if (arena.hellhounds && (damager == null || damager instanceof Player)) - pet.setFireTicks(32768); - - event.setCancelled(true); - } - - private void onMonsterDamage(EntityDamageEvent event, Entity monster, Entity damager) - { - if (damager instanceof Player) - { - Player p = (Player) damager; - if (!arena.inArena(p)) - { - event.setCancelled(true); - return; - } - - arena.arenaPlayerMap.get(p).getStats().dmgDone += event.getDamage(); - arena.arenaPlayerMap.get(p).getStats().hits++; - } - else if (damager instanceof Wolf && arena.pets.contains(damager)) - { - event.setDamage(1); - arena.arenaPlayerMap.get((Player) ((Wolf) damager).getOwner()).getStats().dmgDone += event.getDamage(); - } - else if (damager instanceof LivingEntity) - { - if (!arena.monsterInfight) - event.setCancelled(true); - } - - // Boss - if (arena.bossWave != null && monster.equals(arena.bossWave.getEntity())) - { - if (event.getCause() == DamageCause.LIGHTNING) - { - event.setCancelled(true); - return; - } - - // Subtract boss health, and reset actual entity health - arena.bossWave.subtractHealth(event.getDamage()); - arena.bossWave.getEntity().setHealth(arena.bossWave.getEntity().getMaxHealth()); - - // Set damage to 1 for knockback and feedback - event.setDamage(1); - - // If the boss is dead, remove the entity and create an explosion! - if (arena.bossWave.getHealth() <= 0) - { - arena.bossWave.clear(); - arena.bossWave = null; - } - else if (arena.bossWave.getHealth() <= 100 && !arena.bossWave.isLowHealthAnnounced()) - { - MAUtils.tellAll(arena, Msg.WAVE_BOSS_LOW_HEALTH); - arena.bossWave.setLowHealthAnnounced(true); - } - } - } - - public void onEntityCombust(EntityCombustEvent event) - { - if (arena.monsters.contains(event.getEntity())) - event.setCancelled(true); - } - - public void onEntityTarget(EntityTargetEvent event) - { - if (!arena.running || event.isCancelled()) - return; - - if (arena.pets.contains(event.getEntity())) - { - if (event.getReason() != TargetReason.TARGET_ATTACKED_OWNER && event.getReason() != TargetReason.OWNER_ATTACKED_TARGET) - return; - - if (!(event.getTarget() instanceof Player)) - return; - - // If the target is a player, cancel. - event.setCancelled(true); - } - - else if (arena.monsters.contains(event.getEntity())) - { - if (event.getReason() == TargetReason.FORGOT_TARGET) - event.setTarget(MAUtils.getClosestPlayer(event.getEntity(), arena)); - - else if (event.getReason() == TargetReason.TARGET_DIED) - event.setTarget(MAUtils.getClosestPlayer(event.getEntity(), arena)); - - else if (event.getReason() == TargetReason.TARGET_ATTACKED_ENTITY) - if (arena.pets.contains(event.getTarget())) - event.setCancelled(true); - - else if (event.getReason() == TargetReason.CLOSEST_PLAYER) - if (!arena.inArena((Player) event.getTarget())) - event.setCancelled(true); - } - } - - public void onEndermanPickup(EndermanPickupEvent event) - { - if (arena.inRegion(event.getBlock().getLocation())) - event.setCancelled(true); - } - - public void onEndermanPlace(EndermanPlaceEvent event) - { - if (arena.inRegion(event.getLocation())) - event.setCancelled(true); - } - - public void onEntityRegainHealth(EntityRegainHealthEvent event) - { - if (!arena.running) return; - - if (!(event.getEntity() instanceof Player) || !arena.inArena((Player)event.getEntity())) - return; - - if (event.getRegainReason() == RegainReason.REGEN) - event.setCancelled(true); - } - - public void onPlayerAnimation(PlayerAnimationEvent event) - { - if (!arena.running || !arena.inArena(event.getPlayer())) - return; - - arena.arenaPlayerMap.get(event.getPlayer()).getStats().swings++; - } - - public void onPlayerDropItem(PlayerDropItemEvent event) - { - Player p = event.getPlayer(); - - // Player is in the lobby - if (arena.inLobby(p)) - { - MAUtils.tellPlayer(p, Msg.LOBBY_DROP_ITEM); - event.setCancelled(true); - } - - // Player is in the arena - else if (arena.inArena(p)) - { - if (!arena.shareInArena) - { - MAUtils.tellPlayer(p, Msg.LOBBY_DROP_ITEM); - event.setCancelled(true); - } - } - - // Player died/left - else if (p.getLocation().equals(arena.spectatorLoc) || p.getLocation().equals(arena.locations.get(p))) - { - MobArena.warning("Player '" + p.getName() + "' tried to steal item " + event.getItemDrop().getItemStack().getType()); - event.getItemDrop().remove(); - } - - // Player is in the spectator area - else if (arena.specPlayers.contains(p)) - { - MAUtils.tellPlayer(p, Msg.LOBBY_DROP_ITEM); - event.setCancelled(true); - } - } - - public void onPlayerBucketEmpty(PlayerBucketEmptyEvent event) - { - if (!arena.readyPlayers.contains(event.getPlayer()) && !arena.inArena(event.getPlayer())) - return; - - if (!arena.running) - { - event.getBlockClicked().getRelative(event.getBlockFace()).setTypeId(0); - event.setCancelled(true); - return; - } - - Block liquid = event.getBlockClicked().getRelative(event.getBlockFace()); - arena.blocks.add(liquid); - } - - public void onPlayerInteract(PlayerInteractEvent event) - { - Player p = event.getPlayer(); - if (arena.inArena(p) || !arena.inLobby(p)) - return; - - // Player is in the lobby, so disallow using items. - Action a = event.getAction(); - if (a == Action.RIGHT_CLICK_AIR || a == Action.RIGHT_CLICK_BLOCK) - { - event.setUseItemInHand(Result.DENY); - event.setCancelled(true); - } - - // If there's no block involved, just return. - if (!event.hasBlock()) - return; - - // Iron block - if (event.getClickedBlock().getTypeId() == 42) - handleReadyBlock(p); - - // Sign - else if (event.getClickedBlock().getState() instanceof Sign) - { - Sign sign = (Sign) event.getClickedBlock().getState(); - handleSign(sign, p); - } - } - - private void handleReadyBlock(Player p) - { - if (arena.classMap.containsKey(p)) - { - MAUtils.tellPlayer(p, Msg.LOBBY_PLAYER_READY); - arena.playerReady(p); - } - else MAUtils.tellPlayer(p, Msg.LOBBY_PICK_CLASS); - } - - private void handleSign(Sign sign, Player p) - { - // Check if the first line is a class name. - String className = ChatColor.stripColor(sign.getLine(0)); - if (!arena.classes.contains(className) && !className.equalsIgnoreCase("random")) - return; - - // Check for permission. - if (!plugin.has(p, "mobarena.classes." + className) && !className.equalsIgnoreCase("random")) - { - MAUtils.tellPlayer(p, Msg.LOBBY_CLASS_PERMISSION); - return; - } - - // Delay the inventory stuff to ensure that right-clicking works. - delayAssignClass(p, className); - } - - private void delayAssignClass(final Player p, final String className) - { - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - arena.assignClass(p, className); - - if (!className.equalsIgnoreCase("random")) - MAUtils.tellPlayer(p, Msg.LOBBY_CLASS_PICKED, className, arena.classItems.get(className).get(0).getType()); - else - MAUtils.tellPlayer(p, Msg.LOBBY_CLASS_RANDOM); - } - }); - } - - public void onPlayerQuit(PlayerQuitEvent event) - { - Player p = event.getPlayer(); - if (!arena.enabled || (!arena.inArena(p) && !arena.inLobby(p))) - return; - - arena.playerLeave(p); - } - - public void onPlayerKick(PlayerKickEvent event) - { - Player p = event.getPlayer(); - if (!arena.enabled || (!arena.inArena(p) && !arena.inLobby(p))) - return; - - arena.playerLeave(p); - } - - public void onPlayerTeleport(PlayerTeleportEvent event) - { - if (!arena.enabled || !arena.setup || arena.allowWarp || arena.edit) - return; - - Location to = event.getTo(); - Location from = event.getFrom(); - Player p = event.getPlayer(); - - if (arena.inRegion(from)) - { - // Covers the case in which both locations are in the arena. - if ((arena.inRegion(to) && arena.running) || isWarp(to) || to.equals(arena.locations.get(p))) - return; - - MAUtils.tellPlayer(p, Msg.WARP_FROM_ARENA); - event.setCancelled(true); - } - else if (arena.inRegion(to)) - { - if (isWarp(from) || isWarp(to) || to.equals(arena.locations.get(p))) - return; - - MAUtils.tellPlayer(p, Msg.WARP_TO_ARENA); - event.setCancelled(true); - } - } - - private boolean isWarp(Location l) - { - return l.equals(arena.arenaLoc) || l.equals(arena.lobbyLoc) || l.equals(arena.spectatorLoc); - } - - /*public void onPlayerTeleport(PlayerTeleportEvent event) - { - if (!arena.running || arena.edit || !arena.enabled || !arena.setup || arena.allowWarp) - return; - - Location to = event.getTo(); - Location from = event.getFrom(); - - if (!arena.inRegion(to) && !arena.inRegion(from)) - return; - - Player p = event.getPlayer(); - Location old = arena.locations.get(p); - - if (arena.inArena(p) || arena.inLobby(p) || arena.specPlayers.contains(p)) - { - if (arena.inRegion(from)) - { - if (arena.inRegion(to) || to.equals(arena.arenaLoc) || to.equals(arena.lobbyLoc) || to.equals(arena.spectatorLoc) || to.equals(old)) - return; - - MAUtils.tellPlayer(p, Msg.WARP_FROM_ARENA); - event.setCancelled(true); - return; - } - - if (arena.inRegion(to)) - { - if (to.equals(arena.arenaLoc) || to.equals(arena.lobbyLoc) || to.equals(arena.spectatorLoc) || to.equals(old)) - return; - - MAUtils.tellPlayer(p, Msg.WARP_TO_ARENA); - event.setCancelled(true); - return; - } - - return; - } - - if (arena.running && arena.inRegion(to)) - { - event.setCancelled(true); - return; - } - }*/ - - public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) - { - Player p = event.getPlayer(); - - if (!arena.inArena(p) && !arena.inLobby(p)) - return; - - String[] args = event.getMessage().split(" "); - - if ((args.length > 1 && MACommands.COMMANDS.contains(args[1].trim())) || - MACommands.ALLOWED_COMMANDS.contains(event.getMessage().substring(1).trim()) || - MACommands.ALLOWED_COMMANDS.contains(args[0])) - return; - - event.setCancelled(true); - MAUtils.tellPlayer(p, Msg.MISC_COMMAND_NOT_ALLOWED); - } -} diff --git a/src/com/garbagemule/MobArena/MAMessages.java b/src/com/garbagemule/MobArena/MAMessages.java index f1883bb..0bbf4ca 100644 --- a/src/com/garbagemule/MobArena/MAMessages.java +++ b/src/com/garbagemule/MobArena/MAMessages.java @@ -7,104 +7,110 @@ import java.io.FileInputStream; import java.io.FileWriter; import java.io.InputStreamReader; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.Msg; + public class MAMessages { - public static void init(MobArenaPlugin plugin) { + public static void init(MobArena plugin) { // Grab the file - File msgFile = new File(MobArena.dir, "anouncements.properties"); - + File msgFile = new File(plugin.getDataFolder(), "announcements.properties"); + // If it couldn't be loaded for some reason - if (!load(msgFile)) + if (!load(plugin, msgFile)) return; - + // Otherwise, start parsing! - parseFile(msgFile); + parseFile(plugin, msgFile); } - - private static boolean load(File file) { + + private static boolean load(MobArena plugin, File file) { // If the file exists, continue on! if (file.exists()) { return true; } - + // Otherwise, create it, and populate it with the defaults. try { file.createNewFile(); BufferedWriter bw = new BufferedWriter(new FileWriter(file)); - + for (Msg m : Msg.values()) { if (m.hasSpoutMsg()) { bw.write(m.name() + "=" + m + "|" + m.toSpoutString()); - } else { + } + else { bw.write(m.name() + "=" + m); } bw.newLine(); } bw.close(); - + return true; } catch (Exception e) { - MobArena.warning("Couldn't initialize announcements-file. Using defaults."); + plugin.warning("Couldn't initialize announcements-file. Using defaults."); return false; } } - - private static void parseFile(File file) { + + private static void parseFile(MobArena plugin, File file) { try { - FileInputStream fis = new FileInputStream(file); + FileInputStream fis = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); - BufferedReader br = new BufferedReader(isr); - + BufferedReader br = new BufferedReader(isr); + // Check for BOM character. br.mark(1); int bom = br.read(); if (bom != 65279) br.reset(); - + String s; while ((s = br.readLine()) != null) - process(s); - + process(plugin, s); + br.close(); } catch (Exception e) { e.printStackTrace(); - MobArena.warning("Problem with announcements-file. Using defaults."); + plugin.warning("Problem with announcements-file. Using defaults."); return; } } - + /** * Helper-method for parsing the strings from the * announcements-file. */ - private static void process(String s) { + private static void process(MobArena plugin, String s) { // If the line ends with = or |, just add a space - if (s.endsWith("=") || s.endsWith("|")) s += " "; - + if (s.endsWith("=") || s.endsWith("|")) { + s += " "; + } + // Split the string by the equals-sign. String[] split = s.split("="); if (split.length != 2) { - MobArena.warning("Couldn't parse \"" + s + "\". Check announcements-file."); + plugin.warning("Couldn't parse \"" + s + "\". Check announcements-file."); return; } - + // Split the value by the pipe-sign. String[] vals = split[1].split("\\|"); - + // For simplicity... String key = split[0]; String val = vals.length == 2 ? vals[0] : split[1]; String spoutVal = vals.length == 2 ? vals[1] : null; - + try { Msg msg = Msg.valueOf(key); msg.set(val); msg.setSpout(spoutVal); } catch (Exception e) { - MobArena.warning(key + " is not a valid key. Check announcements-file."); + plugin.warning(key + " is not a valid key. Check announcements-file."); return; } } diff --git a/src/com/garbagemule/MobArena/MAPlayerListener.java b/src/com/garbagemule/MobArena/MAPlayerListener.java deleted file mode 100644 index 478c8a1..0000000 --- a/src/com/garbagemule/MobArena/MAPlayerListener.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.garbagemule.MobArena; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerAnimationEvent; -import org.bukkit.event.player.PlayerBucketEmptyEvent; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerListener; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerTeleportEvent; - - -public class MAPlayerListener extends PlayerListener -{ - private MobArena plugin; - private ArenaMaster am; - - public MAPlayerListener(MobArena plugin, ArenaMaster am) - { - this.plugin = plugin; - this.am = am; - } - - public void onPlayerAnimation(PlayerAnimationEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerAnimation(event); - } - - public void onPlayerInteract(PlayerInteractEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerInteract(event); - } - - public void onPlayerDropItem(PlayerDropItemEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerDropItem(event); - } - - public void onPlayerBucketEmpty(PlayerBucketEmptyEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerBucketEmpty(event); - } - - public void onPlayerTeleport(PlayerTeleportEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerTeleport(event); - } - - public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) - { - if (!am.enabled) return; - for (Arena arena : am.arenas) - arena.eventListener.onPlayerCommandPreprocess(event); - } - - public void onPlayerQuit(PlayerQuitEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onPlayerQuit(event); - } - - public void onPlayerKick(PlayerKickEvent event) - { - for (Arena arena : am.arenas) - arena.eventListener.onPlayerKick(event); - } - - public void onPlayerJoin(PlayerJoinEvent event) - { - if (!am.updateNotify || !event.getPlayer().isOp()) return; - - final Player p = event.getPlayer(); - Bukkit.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, - new Runnable() - { - public void run() - { - MAUtils.checkForUpdates(plugin, p, false); - } - }, 60); - } -} diff --git a/src/com/garbagemule/MobArena/MASpawnThread.java b/src/com/garbagemule/MobArena/MASpawnThread.java index 98cb3bb..77bc7d9 100644 --- a/src/com/garbagemule/MobArena/MASpawnThread.java +++ b/src/com/garbagemule/MobArena/MASpawnThread.java @@ -1,283 +1,341 @@ package com.garbagemule.MobArena; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; import org.bukkit.Location; -import org.bukkit.entity.Creeper; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Creature; import org.bukkit.entity.Entity; import org.bukkit.inventory.ItemStack; -import com.garbagemule.MobArena.util.WaveUtils; -import com.garbagemule.MobArena.waves.Wave; +import com.garbagemule.MobArena.ArenaPlayer; +import com.garbagemule.MobArena.MAUtils; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.events.NewWaveEvent; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.region.ArenaRegion; +import com.garbagemule.MobArena.waves.*; +import com.garbagemule.MobArena.waves.enums.WaveType; +import com.garbagemule.MobArena.waves.types.BossWave; +import com.garbagemule.MobArena.waves.types.SupplyWave; +import com.garbagemule.MobArena.waves.types.UpgradeWave; -/** - * Core class for handling wave spawning. - * Currently, every 4th wave is a special wave, and all other waves - * are default waves. The distribution coefficients are used to spread - * out the distribution of each default monster however the server - * host chooses. It is possible to create default waves that consist of - * only one type of monster, or ones that have no creepers, for example. - */ public class MASpawnThread implements Runnable { private MobArena plugin; private Arena arena; - private int wave, taskId, previousSize, playerCount; - - // NEW WAVES - private Wave defaultWave; - private TreeSet recurrentWaves; - private TreeSet singleWaves; - - public MASpawnThread(MobArena plugin, Arena arena) - { - // WAVES - defaultWave = arena.recurrentWaves.first(); - recurrentWaves = arena.recurrentWaves; - singleWaves = new TreeSet(arena.singleWaves); - - this.plugin = plugin; - this.arena = arena; - wave = 1; - playerCount = arena.arenaPlayers.size(); - } - - public void run() - { - // Clear out all dead monsters in the monster set. - removeDeadMonsters(); - - // If there are no players in the arena, return. - if (arena.arenaPlayers.isEmpty()) - return; + private ArenaRegion region; + private RewardManager rewardManager; + private WaveManager waveManager; + private MonsterManager monsterManager; - // Check if wave needs to be cleared first. If so, return! - if (arena.waveClear && wave > 1 && !arena.monsters.isEmpty()) - return; - - // Check if we're on a boss wave - if (!arena.waveClear && arena.bossWave != null) - return; - - // Grant rewards (if any) for this wave - grantRewards(wave); - - // Detonate creepers if needed - detonateCreepers(arena.detCreepers); - - // Check if this is the final wave, in which case, end instead of spawn - if (wave > 1 && wave == arena.finalWave) - { - for (Player p : arena.arenaPlayers) - arena.playerDeath(p); + private int playerCount, monsterLimit; + private boolean waveClear, bossClear; + + /** + * Create a new monster spawner for the input arena. + * Note that the arena's WaveManager is reset + * @param plugin a MobArena instance + * @param arena an arena + */ + public MASpawnThread(MobArena plugin, Arena arena) { + this.plugin = plugin; + this.arena = arena; + this.region = arena.getRegion(); + this.rewardManager = arena.getRewardManager(); + this.waveManager = arena.getWaveManager(); + this.monsterManager = arena.getMonsterManager(); + + reset(); + } + + /** + * Reset the spawner, so all systems and settings are + * ready for a new session. + */ + public void reset() { + waveManager.reset(); + playerCount = arena.getPlayersInArena().size(); + monsterLimit = arena.getSettings().getInt("monster-limit", 100); + waveClear = arena.getSettings().getBoolean("clear-wave-before-next", false); + bossClear = arena.getSettings().getBoolean("clear-boss-before-next", false); + } + + public void run() { + // If the arena isn't running or if there are no players in it. + if (!arena.isRunning() || arena.getPlayersInArena().isEmpty()) { return; } - - // Find the wave to spawn - spawnWave(wave); - + + // Clear out all dead monsters in the monster set. + removeDeadMonsters(); + removeCheatingPlayers(); + + // In case some players were removed, check again. + if (!arena.isRunning()) { + return; + } + + // Grab the wave number. + int nextWave = waveManager.getWaveNumber() + 1; + + // Check if wave needs to be cleared first. If so, return! + if (!isWaveClear()) { + arena.scheduleTask(this, 60); + return; + } + + // Fire off the event. If cancelled, try again in 3 seconds. + NewWaveEvent event = new NewWaveEvent(arena, waveManager.getNext(), nextWave); + plugin.getServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + arena.scheduleTask(this, 60); + return; + } + + // Grant rewards (if any) for the wave about to spawn + grantRewards(nextWave); + + // Check if this is the final wave, in which case, end instead of spawn + if (nextWave > 1 && (nextWave - 1) == waveManager.getFinalWave()) { + List players = new ArrayList(arena.getPlayersInArena()); + for (Player p : players) { + arena.playerLeave(p); + } + + return; + } + + // Spawn the next wave. + spawnWave(nextWave); + // Update stats - updateStats(wave); - - wave++; - if (arena.monsters.isEmpty()) - arena.resetIdleTimer(); + updateStats(nextWave); + + // Reschedule the spawner for the next wave. + arena.scheduleTask(this, arena.getSettings().getInt("wave-interval", 3) * 20); } - - private void removeDeadMonsters() - { - List tmp = new LinkedList(arena.monsters); - for (Entity e : tmp) - { - if (e == null) continue; - - if (e.isDead() || !arena.inRegion(e.getLocation())) - { - arena.monsters.remove(e); + + private void spawnWave(int wave) { + Wave w = waveManager.next(); + + w.announce(arena, wave); + + if (w.getType() == WaveType.UPGRADE) { + handleUpgradeWave(w); + return; + } + + Map monsters = w.getMonstersToSpawn(wave, playerCount, arena); + List spawnpoints = w.getSpawnpoints(arena); + + World world = arena.getWorld(); + int totalSpawnpoints = spawnpoints.size(); + int index = 0; + double mul = w.getHealthMultiplier(); + + for (Map.Entry entry : monsters.entrySet()) { + for (int i = 0; i < entry.getValue(); i++, index++) { + // Check if monster limit has been reached. + if (monsterManager.getMonsters().size() >= monsterLimit) { + return; + } + + // Grab a spawnpoint + Location spawnpoint = spawnpoints.get(index % totalSpawnpoints); + + // Spawn the monster + LivingEntity e = entry.getKey().spawn(arena, world, spawnpoint); + + // Add it to the arena. + monsterManager.addMonster(e); + + // Set the health. + int health = (int) Math.max(1D, e.getMaxHealth() * mul); + e.setHealth(Math.min(health, e.getMaxHealth())); + + // Switch on the type. + switch (w.getType()){ + case BOSS: + BossWave bw = (BossWave) w; + int maxHealth = bw.getMaxHealth(playerCount); + MABoss boss = monsterManager.addBoss(e, maxHealth); + bw.addMABoss(boss); + bw.activateAbilities(arena); + break; + case SWARM: + health = (int) (mul < 1D ? e.getMaxHealth() * mul : 1); + health = Math.max(1, health); + e.setHealth(Math.min(health, e.getMaxHealth())); + break; + case SUPPLY: + SupplyWave sw = (SupplyWave) w; + monsterManager.addSupplier(e, sw.getDropList()); + break; + default: + break; + } + } + } + } + + private void handleUpgradeWave(Wave w) { + UpgradeWave uw = (UpgradeWave) w; + + for (Player p : arena.getPlayersInArena()) { + String className = arena.getArenaPlayer(p).getArenaClass().getName(); + //String className = arena.getClassOfPlayer(p); + uw.grantItems(p, className); + uw.grantItems(p, "All"); + } + } + + /** + * Check if the wave is clear for new spawns. + * If clear-boss-before-next: true, bosses must be dead. + * If clear-wave-before-next: true, all monsters must be dead. + * @param wave the next wave number + * @return true, if the wave is "clear" for new spawns. + */ + private boolean isWaveClear() { + // Check for monster limit + if (monsterManager.getMonsters().size() >= monsterLimit) { + return false; + } + + // Check for boss clear + if (bossClear && !monsterManager.getBossMonsters().isEmpty()) { + return false; + } + + // Check for wave clear + if (waveClear && !monsterManager.getMonsters().isEmpty()) { + return false; + } + + // Check for final wave + if (!monsterManager.getMonsters().isEmpty() && waveManager.getWaveNumber() == waveManager.getFinalWave()) { + return false; + } + + return true; + } + + private void removeDeadMonsters() { + List tmp = new ArrayList(monsterManager.getMonsters()); + for (Entity e : tmp) { + if (e == null) { + continue; + } + + if (e.isDead() || !region.contains(e.getLocation())) { + monsterManager.removeMonster(e); e.remove(); } } } - - private void grantRewards(int wave) - { - for (Map.Entry> entry : arena.everyWaveMap.entrySet()) - if (wave % entry.getKey() == 0) + + private void removeCheatingPlayers() { + List players = new ArrayList(arena.getPlayersInArena()); + for (Player p : players) { + if (region.contains(p.getLocation())) { + continue; + } + + Messenger.tellPlayer(p, "Leaving so soon?"); + p.getInventory().clear(); + arena.playerLeave(p); + } + } + + private void grantRewards(int wave) { + for (Map.Entry> entry : arena.getEveryWaveEntrySet()) { + if (wave % entry.getKey() == 0) { addReward(entry.getValue()); - - if (arena.afterWaveMap.containsKey(wave)) - addReward(arena.afterWaveMap.get(wave)); - } - - private void spawnWave(int wave) - { - Wave w = null; - - // Check the first element of the single waves. - if (!singleWaves.isEmpty() && singleWaves.first().matches(wave)) - { - w = singleWaves.pollFirst(); + } } - else - { - SortedSet matches = getMatchingRecurrentWaves(wave); - w = matches.isEmpty() ? defaultWave : matches.last(); - } - - // Notify listeners. - for (MobArenaListener listener : plugin.getAM().listeners) - listener.onWave(arena, wave, w.getName(), w.getBranch(), w.getType()); - arena.setWave(w); - w.spawn(wave); - } - - private void updateStats(int wave) - { - for (ArenaPlayer ap: arena.getArenaPlayerSet()) - if (arena.getArenaPlayers().contains(ap.getPlayer())) - ap.getStats().lastWave++; - } - - private SortedSet getMatchingRecurrentWaves(int wave) - { - TreeSet result = new TreeSet(WaveUtils.getRecurrentComparator()); - - for (Wave w : recurrentWaves) - { - if (w.matches(wave)) - result.add(w); + List after = arena.getAfterWaveReward(wave); + if (after != null) { + addReward(after); } - - return result; } - - - /*//////////////////////////////////////////////////////////////////// - // - // Getters/setters - // - ////////////////////////////////////////////////////////////////////*/ - - public int getWave() - { - return wave; + private void updateStats(int wave) { + for (ArenaPlayer ap : arena.getArenaPlayerSet()) { + if (arena.getPlayersInArena().contains(ap.getPlayer())) { + ap.getStats().inc("lastWave"); + } + } } - - public int getTaskId() - { - return taskId; - } - - public int getPreviousSize() - { - return previousSize; - } - - public int getPlayerCount() - { + + /* + * //////////////////////////////////////////////////////////////////// + * // + * // Getters/setters + * // + * //////////////////////////////////////////////////////////////////// + */ + + public int getPlayerCount() { return playerCount; } - - public void setTaskId(int taskId) - { - this.taskId = taskId; - } - - public void setPreviousSize(int previousSize) - { - this.previousSize = previousSize; - } - + /** * Rewards all players with an item from the input String. */ - private void addReward(List rewards) - { - for (Player p : arena.arenaPlayers) - { - if (arena.log.players.get(p) == null) - continue; - + private void addReward(List rewards) { + for (Player p : arena.getPlayersInArena()) { ItemStack reward = MAUtils.getRandomReward(rewards); - arena.log.players.get(p).rewards.add(reward); - - if (reward == null) - { - MAUtils.tellPlayer(p, "ERROR! Problem with economy rewards. Notify server host!"); - MobArena.warning("Could not add null reward. Please check the config-file!"); - } - else if (reward.getTypeId() == MobArena.ECONOMY_MONEY_ID) - { - if (plugin.Method != null) - MAUtils.tellPlayer(p, Msg.WAVE_REWARD, plugin.Method.format(reward.getAmount())); - else MobArena.warning("Tried to add money, but no economy plugin detected!"); - } - else - { - MAUtils.tellPlayer(p, Msg.WAVE_REWARD, MAUtils.toCamelCase(reward.getType().toString()) + ":" + reward.getAmount(), reward.getType()); - } - } - } - - /** - * "Detonates" all the Creepers in the monsterSet. - */ - public void detonateCreepers(boolean really) - { - if (!really) - return; - - Set tmp = new HashSet(); - for (Entity e : arena.monsters) - { - if (!(e instanceof Creeper) || e.isDead()) - continue; - - tmp.add(e); - } + rewardManager.addReward(p, reward); - Location loc; - for (Entity e : tmp) - { - if (e == null) continue; - - arena.monsters.remove(e); - loc = e.getLocation().getBlock().getRelative(0,2,0).getLocation(); - arena.world.createExplosion(loc, 2); - e.remove(); + if (reward == null) { + Messenger.tellPlayer(p, "ERROR! Problem with rewards. Notify server host!"); + plugin.warning("Could not add null reward. Please check the config-file!"); + } + else if (reward.getTypeId() == MobArena.ECONOMY_MONEY_ID) { + if (plugin.giveMoney(p, reward.getAmount())) { + Messenger.tellPlayer(p, Msg.WAVE_REWARD, plugin.economyFormat(reward.getAmount())); + } + else { + plugin.warning("Tried to add money, but no economy plugin detected!"); + } + } + else { + Messenger.tellPlayer(p, Msg.WAVE_REWARD, MAUtils.toCamelCase(reward.getType().toString()) + ":" + reward.getAmount(), reward.getType()); + } } } - + /** * Update the targets of all monsters, if their targets aren't alive. */ - public void updateTargets() - { + public void updateTargets() { Creature c; Entity target; - for (Entity e : arena.monsters) - { + for (Entity e : monsterManager.getMonsters()) { if (!(e instanceof Creature)) - continue; + continue; // TODO: Remove the try-catch when Bukkit API is fixed. c = (Creature) e; - try { target = c.getTarget(); } catch (ClassCastException cce) { continue; } - - if (target instanceof Player && arena.inArena((Player) target)) + try { + target = c.getTarget(); + } + catch (ClassCastException cce) { continue; - - c.setTarget(MAUtils.getClosestPlayer(e, arena)); + } + + if (target instanceof Player && arena.getPlayersInArena().contains((Player) target)) { + continue; + } + + c.setTarget(MAUtils.getClosestPlayer(plugin, e, arena)); } } } \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/MAUtils.java b/src/com/garbagemule/MobArena/MAUtils.java index 30a7ec1..adaba4d 100644 --- a/src/com/garbagemule/MobArena/MAUtils.java +++ b/src/com/garbagemule/MobArena/MAUtils.java @@ -1,14 +1,11 @@ package com.garbagemule.MobArena; -import java.lang.reflect.Field; -import java.net.URI; -import java.net.HttpURLConnection; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.util.HashSet; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.LinkedList; @@ -16,15 +13,8 @@ import java.util.Map; import java.util.HashMap; import java.util.Random; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.bukkit.block.Sign; -import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.DyeColor; import org.bukkit.World; import org.bukkit.Material; import org.bukkit.Location; @@ -32,101 +22,22 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.entity.Wolf; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -//import org.bukkit.util.config.Configuration; -import org.getspout.spoutapi.SpoutManager; -import org.getspout.spoutapi.player.SpoutPlayer; +import org.bukkit.plugin.java.JavaPlugin; -import com.garbagemule.MobArena.util.Config; +import com.garbagemule.MobArena.MAUtils; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.region.ArenaRegion; import com.garbagemule.MobArena.util.EntityPosition; -import com.garbagemule.MobArena.util.InventoryItem; +import com.garbagemule.MobArena.util.ItemParser; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.util.config.ConfigUtils; public class MAUtils { public static final String sep = File.separator; - // Weapons - public static final List WEAPONS_TYPE = new LinkedList(); - public static final List SWORDS_TYPE = new LinkedList(); - public static final List AXES_TYPE = new LinkedList(); - public static final List PICKAXES_TYPE = new LinkedList(); - public static final List SPADES_TYPE = new LinkedList(); - public static final List HOES_TYPE = new LinkedList(); - // Armor - public static final List ARMORS_TYPE = new LinkedList(); - public static final List HELMETS_TYPE = new LinkedList(); - public static final List CHESTPLATES_TYPE = new LinkedList(); - public static final List LEGGINGS_TYPE = new LinkedList(); - public static final List BOOTS_TYPE = new LinkedList(); - static - { - // Weapons - SWORDS_TYPE.add(Material.WOOD_SWORD); - SWORDS_TYPE.add(Material.STONE_SWORD); - SWORDS_TYPE.add(Material.GOLD_SWORD); - SWORDS_TYPE.add(Material.IRON_SWORD); - SWORDS_TYPE.add(Material.DIAMOND_SWORD); - - AXES_TYPE.add(Material.WOOD_AXE); - AXES_TYPE.add(Material.STONE_AXE); - AXES_TYPE.add(Material.GOLD_AXE); - AXES_TYPE.add(Material.IRON_AXE); - AXES_TYPE.add(Material.DIAMOND_AXE); - - PICKAXES_TYPE.add(Material.WOOD_PICKAXE); - PICKAXES_TYPE.add(Material.STONE_PICKAXE); - PICKAXES_TYPE.add(Material.GOLD_PICKAXE); - PICKAXES_TYPE.add(Material.IRON_PICKAXE); - PICKAXES_TYPE.add(Material.DIAMOND_PICKAXE); - - SPADES_TYPE.add(Material.WOOD_SPADE); - SPADES_TYPE.add(Material.STONE_SPADE); - SPADES_TYPE.add(Material.GOLD_SPADE); - SPADES_TYPE.add(Material.IRON_SPADE); - SPADES_TYPE.add(Material.DIAMOND_SPADE); - - HOES_TYPE.add(Material.WOOD_HOE); - HOES_TYPE.add(Material.STONE_HOE); - HOES_TYPE.add(Material.GOLD_HOE); - HOES_TYPE.add(Material.IRON_HOE); - HOES_TYPE.add(Material.DIAMOND_HOE); - - WEAPONS_TYPE.addAll(SWORDS_TYPE); - WEAPONS_TYPE.addAll(AXES_TYPE); - WEAPONS_TYPE.addAll(PICKAXES_TYPE); - WEAPONS_TYPE.addAll(SPADES_TYPE); - WEAPONS_TYPE.addAll(HOES_TYPE); - - // Armor - HELMETS_TYPE.add(Material.LEATHER_HELMET); - HELMETS_TYPE.add(Material.GOLD_HELMET); - HELMETS_TYPE.add(Material.CHAINMAIL_HELMET); - HELMETS_TYPE.add(Material.IRON_HELMET); - HELMETS_TYPE.add(Material.DIAMOND_HELMET); - HELMETS_TYPE.add(Material.PUMPKIN); - - CHESTPLATES_TYPE.add(Material.LEATHER_CHESTPLATE); - CHESTPLATES_TYPE.add(Material.GOLD_CHESTPLATE); - CHESTPLATES_TYPE.add(Material.CHAINMAIL_CHESTPLATE); - CHESTPLATES_TYPE.add(Material.IRON_CHESTPLATE); - CHESTPLATES_TYPE.add(Material.DIAMOND_CHESTPLATE); - - LEGGINGS_TYPE.add(Material.LEATHER_LEGGINGS); - LEGGINGS_TYPE.add(Material.GOLD_LEGGINGS); - LEGGINGS_TYPE.add(Material.CHAINMAIL_LEGGINGS); - LEGGINGS_TYPE.add(Material.IRON_LEGGINGS); - LEGGINGS_TYPE.add(Material.DIAMOND_LEGGINGS); - - BOOTS_TYPE.add(Material.LEATHER_BOOTS); - BOOTS_TYPE.add(Material.GOLD_BOOTS); - BOOTS_TYPE.add(Material.CHAINMAIL_BOOTS); - BOOTS_TYPE.add(Material.IRON_BOOTS); - BOOTS_TYPE.add(Material.DIAMOND_BOOTS); - - ARMORS_TYPE.addAll(HELMETS_TYPE); - ARMORS_TYPE.addAll(CHESTPLATES_TYPE); - ARMORS_TYPE.addAll(LEGGINGS_TYPE); - ARMORS_TYPE.addAll(BOOTS_TYPE); - } @@ -136,168 +47,35 @@ public class MAUtils // ///////////////////////////////////////////////////////////////////// */ - public static Map getArenaContainers(Config config, World world, String arena) - { - Map containers = new HashMap(); - String arenaPath = "arenas." + arena + ".coords.containers"; - - if (config.getKeys(arenaPath) == null) - return containers; - - for (String point : config.getKeys(arenaPath)) - containers.put(point, makeLocation(world, config.getString(arenaPath + "." + point))); - - return containers; - } - - /** - * Grab all the spawnpoints for a specific arena. - */ - public static Map getArenaSpawnpoints(Config config, World world, String arena) - { - Map spawnpoints = new HashMap(); - String arenaPath = "arenas." + arena + ".coords.spawnpoints"; - - if (config.getKeys(arenaPath) == null) - return spawnpoints; - - for (String point : config.getKeys(arenaPath)) - if (!point.matches("^(.)*boss(.)*$")) - spawnpoints.put(point, makeLocation(world, config.getString(arenaPath + "." + point))); - - return spawnpoints; - } - - public static Map getArenaBossSpawnpoints(Config config, World world, String arena) - { - Map spawnpoints = new HashMap(); - String arenaPath = "arenas." + arena + ".coords.spawnpoints"; - - if (config.getKeys(arenaPath) == null) - return spawnpoints; - - for (String point : config.getKeys(arenaPath)) - if (point.matches("^(.)*boss(.)*$")) - spawnpoints.put(point, makeLocation(world, config.getString(arenaPath + "." + point))); - - return spawnpoints; - } - - /** - * Returns a map of classnames mapped to lists of ItemStacks. - */ - public static Map> getClassItems(Config config, String type) - { - Map> result = new HashMap>(); - - for (String className : config.getKeys("classes")) - result.put(className, makeItemStackList(config.getString("classes." + className + "." + type))); - - return result; - } - - public static Map> getClassPerms(Config config) - { - Map> result = new HashMap>(); - - Set classes = config.getKeys("classes"); - if (classes == null) return result; - - for (String c : classes) - { - // Get all the permissions for the class and return if empty. - List keys = config.getStringList("classes." + c + ".permissions", new LinkedList()); - if (keys.isEmpty()) continue; - - // If the string starts with a hyphen (-), use false. - Map perms = new HashMap(); - for (String p : keys) - { - boolean b = !p.startsWith("-"); - String s = b ? p : p.substring(1); - perms.put(s,b); - } - - result.put(c,perms); - } - - return result; - } - - public static List getEntryFee(Config config, String arena) - { - return makeItemStackList(config.getString("arenas." + arena + ".settings.entry-fee", null)); - } - - /** - * Takes a comma-separated list of items in the : format and - * returns a list of ItemStacks created from that data. - */ - public static List makeItemStackList(String string) - { - List result = new LinkedList(); - if (string == null || string.isEmpty()) return result; - - // Trim commas and whitespace, and split items by commas - string = string.trim(); - if (string.endsWith(",")) - string = string.substring(0, string.length()-1); - String[] items = string.split(","); - - for (String item : items) - { - // Trim whitespace and split by colons. - item = item.trim(); - String[] parts = item.split(":"); - - // Grab the amount. - int amount = 1; - if (parts.length == 1 && parts[0].matches("\\$[0-9]+")) - amount = Integer.parseInt(parts[0].substring(1, parts[0].length())); - else if (parts.length == 2 && parts[1].matches("(-)?[0-9]+")) - amount = Integer.parseInt(parts[1]); - else if (parts.length == 3 && parts[2].matches("(-)?[0-9]+")) - amount = Integer.parseInt(parts[2]); - - - // Make the ItemStack. - ItemStack stack = (parts.length == 3) ? - makeItemStack(parts[0], amount, parts[1]) : - makeItemStack(parts[0], amount); - - if (stack != null) - result.add(stack); - } - return result; - } - /** * Generates a map of wave numbers and rewards based on the * type of wave ("after" or "every") and the config-file. If * no keys exist in the config-file, an empty map is returned. */ - public static Map> getArenaRewardMap(Config config, String arena, String type) + public static Map> getArenaRewardMap(MobArena plugin, Config config, String arena, String type) { - String arenaPath = "arenas." + arena + ".rewards.waves."; + //String arenaPath = "arenas." + arena + ".rewards.waves."; + String typePath = ConfigUtils.waveRewardList(arena, type); Map> result = new HashMap>(); - if (config.getKeys(arenaPath + type) == null) + if (config.getKeys(typePath) == null) { if (type.equals("every")) { - config.setProperty(arenaPath + "every.3", "feather, bone, stick"); - config.setProperty(arenaPath + "every.5", "dirt:4, gravel:4, stone:4"); - config.setProperty(arenaPath + "every.10", "iron_ingot:10, gold_ingot:8"); + config.set(typePath + ".3", "feather, bone, stick"); + config.set(typePath + ".5", "dirt:4, gravel:4, stone:4"); + config.set(typePath + ".10", "iron_ingot:10, gold_ingot:8"); } else if (type.equals("after")) { - config.setProperty(arenaPath + "after.7", "minecart, storage_minecart, powered_minecart"); - config.setProperty(arenaPath + "after.13", "iron_sword, iron_pickaxe, iron_spade"); - config.setProperty(arenaPath + "after.16", "diamond_sword"); + config.set(typePath + ".7", "minecart, storage_minecart, powered_minecart"); + config.set(typePath + ".13", "iron_sword, iron_pickaxe, iron_spade"); + config.set(typePath + ".16", "diamond_sword"); } } - Set waves = config.getKeys(arenaPath + type); + //Set waves = config.getKeys(arenaPath + type); + Set waves = config.getKeys(typePath); if (waves == null) return result; for (String n : waves) @@ -306,25 +84,13 @@ public class MAUtils continue; int wave = Integer.parseInt(n); - String rewards = config.getString(arenaPath + type + "." + n); + String path = ConfigUtils.waveReward(arena, type, wave); + String rewards = config.getString(path); - result.put(wave, makeItemStackList(rewards)); + result.put(wave, ItemParser.parseItems(rewards)); } return result; } - - public static List getAllowedCommands(Config config) - { - String commands = config.getString("global-settings.allowed-commands"); - if (commands == null) - { - config.setProperty("global-settings.allowed-commands", "/list, /pl"); - config.save(); - commands = config.getString("global-settings.allowed-commands"); - } - - return stringToList(commands); - } @@ -334,293 +100,6 @@ public class MAUtils // ///////////////////////////////////////////////////////////////////// */ - public static PlayerInventory clearInventory(Player p) - { - PlayerInventory inv = p.getInventory(); - inv.clear(); - inv.setHelmet(null); - inv.setChestplate(null); - inv.setLeggings(null); - inv.setBoots(null); - return inv; - } - - public static boolean storeInventory(Player p) - { - // Set up the files and paths - String invPath = "plugins" + sep + "MobArena" + sep + "inventories"; - new File(invPath).mkdir(); - File backupFile = new File(invPath + sep + p.getName() + ".inv"); - - // If a backup file already exists, restore the inventory first - if (backupFile.exists() && !restoreInventory(p)) - return false; - - // Grab the inventory contents. - ItemStack[] items = p.getInventory().getContents(); - ItemStack[] armor = p.getInventory().getArmorContents(); - - try - { - backupFile.createNewFile(); - - InventoryItem[] inv = new InventoryItem[items.length + armor.length]; - for (int i = 0; i < items.length; i++) - inv[i] = InventoryItem.parseItemStack(items[i]); - for (int i = 0; i < armor.length; i++) - inv[i + items.length] = InventoryItem.parseItemStack(armor[i]); - - FileOutputStream fos = new FileOutputStream(backupFile); - ObjectOutputStream oos = new ObjectOutputStream(fos); - oos.writeObject(inv); - oos.close(); - } - catch (Exception e) - { - e.printStackTrace(); - MobArena.warning("Could not create backup file for " + p.getName() + "."); - return false; - } - - clearInventory(p); - return true; - } - - public static boolean restoreInventory(Player p) - { - // Grab the items from the MobArena .inv file - ItemStack[] stacks = getInventoryFile(p); - if (stacks == null) - { - MobArena.warning("Couldn't restore player inventory for '" + p.getName() + "'"); - return false; - } - - // If the player isn't online, hack the playerName.dat file - if (!p.isOnline()) - System.out.println("THE WORLD IS GOING TO END! SOMETHING IS WRONG!");//return writeInventoryData(p, stacks); - - // Otherwise, restore the inventory directly - ItemStack[] items = new ItemStack[stacks.length-4]; - ItemStack[] armor = new ItemStack[4]; - - for (int i = 0; i < stacks.length - 4; i++) - items[i] = stacks[i]; - for (int i = 0; i < 4; i++) - armor[i] = stacks[stacks.length - 4 + i]; - - // Restore the inventory. - PlayerInventory inv = p.getInventory(); - inv.setArmorContents(armor); - for (int i = 0; i < items.length; i++) - inv.setItem(i, items[i]); - - return true; - } - - public static ItemStack[] getInventoryFile(Player p) - { - String invPath = "plugins" + sep + "MobArena" + sep + "inventories"; - File backupFile = new File(invPath + sep + p.getName() + ".inv"); - - try - { - // If the backup-file couldn't be found, return. - if (!backupFile.exists()) - return null; - - // Grab the MAInventoryItem array from the backup-file. - FileInputStream fis = new FileInputStream(backupFile); - ObjectInputStream ois = new ObjectInputStream(fis); - InventoryItem[] fromFile = (InventoryItem[]) ois.readObject(); - ois.close(); - - // Delete the file - backupFile.delete(); - - return InventoryItem.toItemStacks(fromFile); - } - catch (Exception e) - { - e.printStackTrace(); - MobArena.warning("Could not get inventory file for '" + p.getName() + "'"); - return null; - } - } - - /* Checks if all inventory and armor slots are empty. */ - public static boolean hasEmptyInventory(Player p) - { - ItemStack[] inventory = p.getInventory().getContents(); - ItemStack[] armor = p.getInventory().getArmorContents(); - - // For inventory, check for null - for (ItemStack stack : inventory) - if (stack != null) return false; - - // For armor, check for id 0, or AIR - for (ItemStack stack : armor) - if (stack.getTypeId() != 0) return false; - - return true; - } - - /** - * Gives the player all of the items in the list of ItemStacks. - */ - public static void giveItems(Player p, List stacks, boolean autoEquip, boolean rewards, MobArena plugin) - { - if (stacks == null) - return; - - // Give the player some items! - PlayerInventory inv = p.getInventory(); - for (ItemStack stack : stacks) - { - if (stack == null) - continue; - - // If this is money, don't add to inventory. - if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) - { - if (plugin != null && plugin.Method != null) - plugin.Method.getAccount(p.getName()).add(stack.getAmount()); - - continue; - } - - // If these are rewards, don't tamper with them. - if (rewards) - { - giveItem(inv, stack); - continue; - } - - // If this is an armor piece, equip it. - if (autoEquip && ARMORS_TYPE.contains(stack.getType())) - { - equipArmorPiece(stack, inv); - continue; - } - - // If this is a weapon, set its durability to "unlimited". - if (WEAPONS_TYPE.contains(stack.getType())) - stack.setDurability(Short.MIN_VALUE); - - giveItem(inv, stack); - } - } - - public static void giveItems(Player p, List stacks, boolean autoEquip, MobArena plugin) - { - giveItems(p, stacks, autoEquip, false, plugin); - } - - public static void giveItem(PlayerInventory inv, ItemStack stack) - { - // If the stack isn't too big, just add it - if (stack.getAmount() <= 64) - { - inv.addItem(stack); - return; - } - - // Otherwise, grab the id, durability and data. - int id = stack.getTypeId(); - int amount = stack.getAmount(); - short durability = stack.getDurability(); - Byte data = stack.getData() != null ? stack.getData().getData() : null; - - // Initialize the quotient and remainder. - int quotient = amount / 64; - int remainder = amount % 64; - - // Add a bunch of stacks of amount 64. - for (int i = 0; i < quotient; i++) - inv.addItem(new ItemStack(id, 64, durability, data)); - - // Add the rest. - if (remainder > 0) - inv.addItem(new ItemStack(id, remainder, durability, data)); - } - - public static void giveRewards(Player p, List stacks, MobArena plugin) - { - giveItems(p, stacks, false, true, plugin); - } - - public static int getPetAmount(Player p) - { - int result = 0; - - for (ItemStack stack : p.getInventory().getContents()) - { - if (stack == null || stack.getTypeId() != 352) - continue; - - result += stack.getAmount(); - } - - return result; - } - - /* Helper method for equipping armor pieces. */ - public static void equipArmorPiece(ItemStack stack, PlayerInventory inv) - { - Material type = stack.getType(); - - if (HELMETS_TYPE.contains(type)) - inv.setHelmet(stack); - else if (CHESTPLATES_TYPE.contains(type)) - inv.setChestplate(stack); - else if (LEGGINGS_TYPE.contains(type)) - inv.setLeggings(stack); - else if (BOOTS_TYPE.contains(type)) - inv.setBoots(stack); - } - - /* Helper methods for making ItemStacks out of strings and ints */ - public static ItemStack makeItemStack(String name, int amount, String data) - { - // If this is economy money, create a dummy ItemStack. - if (name.matches("\\$[0-9]+")) - return new ItemStack(MobArena.ECONOMY_MONEY_ID, amount); - - try - { - byte offset = 0; - - Material material = (name.matches("[0-9]+")) ? - Material.getMaterial(Integer.parseInt(name)) : - Material.valueOf(name.toUpperCase()); - - if (material == Material.INK_SACK) - offset = 15; - - if (material == Material.INK_SACK || material == Material.WOOL) - { - DyeColor dye = (data.matches("[0-9]+")) ? - DyeColor.getByData((byte) Math.abs(offset - Integer.parseInt(data))) : - DyeColor.valueOf(data.toUpperCase()); - - return new ItemStack(material, amount, (short) Math.abs(offset - dye.getData())); - } - else - { - return new ItemStack(material, amount, (short) Integer.parseInt(data)); - } - } - catch (Exception e) - { - MobArena.warning("Could not create item \"" + name + "\". Check config.yml"); - return null; - } - } - public static ItemStack makeItemStack(String name, int amount) - { - return makeItemStack(name, amount, "0"); - } - /* Helper method for grabbing a random reward */ public static ItemStack getRandomReward(List rewards) { @@ -661,420 +140,14 @@ public class MAUtils - /* ///////////////////////////////////////////////////////////////////// // - - REGION METHODS - - // ///////////////////////////////////////////////////////////////////// */ - - /** - * Check if a Location is inside two points (x1,y1,z1) (x2,y2,z2) - */ - public static boolean inRegion(Location loc, double x1, double y1, double z1, double x2, double y2, double z2) - { - double x = loc.getBlockX(); - double y = loc.getBlockY(); - double z = loc.getBlockZ(); - - return x >= x1 && x <= x2 && - y >= y1 && y <= y2 && - z >= z1 && z <= z2; - } - - /** - * Create a frame spanned by the two input coordinates. - * @return An int arry holding x,y,z and the original type IDs of each block. - */ - public static Set showRegion(Player p, World world, Location p1, Location p2, int id, byte color) - { - Set result = new HashSet(); - - int x1 = p1.getBlockX(); int y1 = p1.getBlockY(); int z1 = p1.getBlockZ(); - int x2 = p2.getBlockX(); int y2 = p2.getBlockY(); int z2 = p2.getBlockZ(); - - int[] buffer; - - for (int i = x1; i <= x2; i++) - { - buffer = new int[] {i, y1, z1, world.getBlockTypeIdAt(i, y1, z1)}; - result.add(buffer); - p.sendBlockChange(new Location(world, i, y1, z1), id, color); - - buffer = new int[] {i, y2, z1, world.getBlockTypeIdAt(i, y2, z1)}; - result.add(buffer); - p.sendBlockChange(new Location(world, i, y2, z1), id, color); - - buffer = new int[] {i, y1, z2, world.getBlockTypeIdAt(i, y1, z2)}; - result.add(buffer); - p.sendBlockChange(new Location(world, i, y1, z2), id, color); - - buffer = new int[] {i, y2, z2, world.getBlockTypeIdAt(i, y2, z2)}; - result.add(buffer); - p.sendBlockChange(new Location(world, i, y2, z2), id, color); - } - for (int j = y1+1; j <= y2-1; j++) - { - buffer = new int[] {x1, j, z1, world.getBlockTypeIdAt(x1, j, z1)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x1, j, z1), id, color); - - buffer = new int[] {x2, j, z1, world.getBlockTypeIdAt(x2, j, z1)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x2, j, z1), id, color); - - buffer = new int[] {x1, j, z2, world.getBlockTypeIdAt(x1, j, z2)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x1, j, z2), id, color); - - buffer = new int[] {x2, j, z2, world.getBlockTypeIdAt(x2, j, z2)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x2, j, z2), id, color); - } - for (int k = z1+1; k <= z2-1; k++) - { - buffer = new int[] {x1, y1, k, world.getBlockTypeIdAt(x1, y1, k)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x1, y1, k), id, color); - - buffer = new int[] {x2, y1, k, world.getBlockTypeIdAt(x2, y1, k)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x2, y1, k), id, color); - - buffer = new int[] {x1, y2, k, world.getBlockTypeIdAt(x1, y2, k)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x1, y2, k), id, color); - - buffer = new int[] {x2, y2, k, world.getBlockTypeIdAt(x2, y2, k)}; - result.add(buffer); - p.sendBlockChange(new Location(world, x2, y2, k), id, color); - } - return result; - } - - public static Set showRegion(Player p, World world, Location p1, Location p2, int id) - { - return showRegion(p, world, p1, p2, id, (byte) 0); - } - - /** - * Take all the blocks with coordinates (buffer[0], buffer[1], buffer[2]) and set - * their type ID to buffer[3]. Used to hide regions shown by showRegion. - */ - public static void hideRegion(int[] buffer) - { - - } - - /** - * Create a Location object from the config-file. - */ - public static Location getArenaCoord(Config config, World world, String arena, String coord) - { - //config.load(); - String str = config.getString("arenas." + arena + ".coords." + coord); - if (str == null) - return null; - return makeLocation(world, str); - } - - /** - * Save an arena location to the Config. - */ - public static void setArenaCoord(Config config, Arena arena, String coord, Location loc) - { - if (coord.equals("arena") || coord.equals("lobby") || coord.equals("spectator")) - loc.setY(loc.getY() + 1); - - config.setProperty("arenas." + arena.configName() + ".coords." + coord, makeCoord(loc)); - config.save(); - arena.load(config); - - if (coord.equals("p1") || coord.equals("p2")) - fixRegion(config, loc.getWorld(), arena); - if (coord.equals("l1") || coord.equals("l2")) - fixLobby(config, loc.getWorld(), arena); - } - - public static boolean delArenaCoord(Config config, Arena arena, String coord) - { - if (config.getString("arenas." + arena.configName() + ".coords." + coord) == null) - return false; - - config.removeProperty("arenas." + arena.configName() + ".coords." + coord); - config.save(); - arena.load(config); - return true; - } - - public static void fixRegion(Config config, World world, Arena arena) - { - if (arena.p1 == null || arena.p2 == null) - return; - - if (arena.p1.getX() > arena.p2.getX()) - { - double tmp = arena.p1.getX(); - arena.p1.setX(arena.p2.getX()); - arena.p2.setX(tmp); - } - - if (arena.p1.getZ() > arena.p2.getZ()) - { - double tmp = arena.p1.getZ(); - arena.p1.setZ(arena.p2.getZ()); - arena.p2.setZ(tmp); - } - - if (arena.p1.getY() > arena.p2.getY()) - { - double tmp = arena.p1.getY(); - arena.p1.setY(arena.p2.getY()); - arena.p2.setY(tmp); - } - - if (!arena.world.getName().equals(world.getName())) - arena.world = world; - - arena.serializeConfig(); - arena.load(config); - } - - private static void fixLobby(Config config, World world, Arena arena) - { - if (arena.l1 == null || arena.l2 == null) - return; - - if (arena.l1.getX() > arena.l2.getX()) - { - double tmp = arena.l1.getX(); - arena.l1.setX(arena.l2.getX()); - arena.l2.setX(tmp); - } - - if (arena.l1.getZ() > arena.l2.getZ()) - { - double tmp = arena.l1.getZ(); - arena.l1.setZ(arena.l2.getZ()); - arena.l2.setZ(tmp); - } - - if (arena.l1.getY() > arena.l2.getY()) - { - double tmp = arena.l1.getY(); - arena.l1.setY(arena.l2.getY()); - arena.l2.setY(tmp); - } - arena.serializeConfig(); - arena.load(config); - } - - /** - * Create a Location from the input String in the input World. - */ - public static Location makeLocation(World world, String str, boolean extras) - { - String[] parts = str.split(","); - - double x = Double.parseDouble(parts[0].trim()); - double y = Double.parseDouble(parts[1].trim()); - double z = Double.parseDouble(parts[2].trim()); - - if (extras && parts.length > 3) - { - float yaw = Float.parseFloat(parts[3].trim()); - float pitch = Float.parseFloat(parts[4].trim()); - return new Location(world, x, y, z, yaw, pitch); - } - - return new Location(world, x, y, z); - } - - public static Location makeLocation(World world, String str) - { - return makeLocation(world, str, true); - } - - /** - * Create a location String from the input Location. - */ - public static String makeCoord(Location loc, boolean extras) - { - int x = loc.getBlockX(); - int y = loc.getBlockY(); - int z = loc.getBlockZ(); - - if (extras) - { - float yaw = loc.getYaw(); - float pitch = loc.getPitch(); - return x + "," + y + "," + z + "," + yaw + "," + pitch; - } - - return x + "," + y + "," + z; - } - - public static String makeCoord(Location loc) - { - return makeCoord(loc, true); - } - - /** - * Check if a location is within any arena region. - */ - public static boolean inRegions(Location loc, Arena... arenas) - { - for (Arena arena : arenas) - { - if (arena.inRegion(loc)) - return true; - } - - return false; - } - - public static double getDouble(Config config, String path) - { - return getDouble(config, path, 0D); - } - - public static double getDouble(Config config, String path, double def) - { - Object o = config.getProperty(path); - if (o instanceof Double) - return (Double) o; - else if (o instanceof Number) - return (Integer) o; - return def; - } - - public static int getInt(Config config, String path) - { - return getInt(config, path, 0); - } - - public static int getInt(Config config, String path, int def) - { - Object o = config.getProperty(path); - if (o instanceof Integer) - return (Integer) o; - return def; - } - - - /* ///////////////////////////////////////////////////////////////////// // MISC METHODS // ///////////////////////////////////////////////////////////////////// */ - public static boolean tellSpoutPlayer(Player p, Msg msg, String s, Material logo) - { - // Grab the SpoutPlayer. - SpoutPlayer sp = MobArena.hasSpout ? SpoutManager.getPlayer(p) : null; - - if (msg.hasSpoutMsg() && sp != null && sp.isSpoutCraftEnabled()) - { - // Grab the message text. - String text = msg.toSpoutString(s); - - // If more than 26 characters, truncate. - if (text.length() > 26) - text = text.substring(0, 26); - - // If the logo is null, use an iron sword. - if (logo == null) - logo = msg.getLogo(); - - // Send the notification. - sp.sendNotification("MobArena", text, logo, (short) 0, 2000); - return true; - } - else return tellPlayer(p, msg.toString(s)); - } - public static boolean tellSpoutPlayer(Player p, Msg msg, Material logo) - { - return tellSpoutPlayer(p, msg, null, logo); - } - - public static boolean tellSpoutPlayer(Player p, Msg msg, String s) - { - return tellSpoutPlayer(p, msg, s, null); - } - - public static boolean tellSpoutPlayer(Player p, Msg msg) - { - return tellSpoutPlayer(p, msg, null, null); - } - - public static boolean tellPlayer(CommandSender p, String msg) - { - // If the input sender is null or the string is empty, return. - if (p == null || msg.equals(" ")) - return false; - - // Otherwise, send the message with the [MobArena] tag. - p.sendMessage(ChatColor.GREEN + "[MobArena] " + ChatColor.WHITE + msg); - return true; - } - - public static boolean tellPlayer(CommandSender p, Msg msg, String s, boolean spout, Material logo) - { - if (spout && p instanceof Player) - return tellSpoutPlayer((Player) p, msg, s, logo); - - return tellPlayer(p, msg.toString(s)); - } - - public static boolean tellPlayer(CommandSender p, Msg msg, String s, Material logo) - { - return tellPlayer(p, msg, s, MobArena.hasSpout, logo); - } - - public static boolean tellPlayer(CommandSender p, Msg msg, String s) - { - return tellPlayer(p, msg, s, MobArena.hasSpout, null); - } - - public static boolean tellPlayer(CommandSender p, Msg msg) - { - return tellPlayer(p, msg, null, MobArena.hasSpout, null); - } - - /** - * Sends a message to all players in and around the arena. - */ - public static void tellAll(Arena arena, Msg msg, String s, boolean notifyPlayers) - { - Set tmp = new HashSet(); - tmp.addAll(arena.arenaPlayers); - tmp.addAll(arena.lobbyPlayers); - tmp.addAll(arena.readyPlayers); - tmp.addAll(arena.notifyPlayers); - tmp.addAll(arena.specPlayers); - if (notifyPlayers) tmp.addAll(arena.notifyPlayers); - for (Player p : tmp) - tellPlayer(p, msg, s); - } - - public static void tellAll(Arena arena, Msg msg, String s) - { - tellAll(arena, msg, s, false); - } - - public static void tellAll(Arena arena, Msg msg, boolean notifyPlayers) - { - tellAll(arena, msg, null, notifyPlayers); - } - - public static void tellAll(Arena arena, Msg msg) - { - tellAll(arena, msg, null, false); - } - - public static Player getClosestPlayer(Entity e, Arena arena) - { + public static Player getClosestPlayer(MobArena plugin, Entity e, Arena arena) { // Set up the comparison variable and the result. double current = Double.POSITIVE_INFINITY; Player result = null; @@ -1082,19 +155,16 @@ public class MAUtils /* Iterate through the ArrayList, and update current and result every * time a squared distance smaller than current is found. */ //for (Player p : arena.livePlayers) - for (Player p : arena.arenaPlayers) - { - if (!arena.world.equals(p.getWorld())) - { - MobArena.info("Player '" + p.getName() + "' is not in the right world. Kicking..."); + for (Player p : arena.getPlayersInArena()) { + if (!arena.getWorld().equals(p.getWorld())) { + plugin.info("Player '" + p.getName() + "' is not in the right world. Kicking..."); p.kickPlayer("[MobArena] Cheater! (Warped out of the arena world.)"); - tellPlayer(p, "You warped out of the arena world."); + Messenger.tellPlayer(p, "You warped out of the arena world."); continue; } - double dist = distanceSquared(p, e.getLocation()); - if (dist < current && dist < 256D) - { + double dist = distanceSquared(plugin, p, e.getLocation()); + if (dist < current && dist < 256D) { current = dist; result = p; } @@ -1102,31 +172,19 @@ public class MAUtils return result; } - public static double distanceSquared(Player p, Location l) - { - try - { + public static double distanceSquared(MobArena plugin, Player p, Location l) { + try { return p.getLocation().distanceSquared(l); } - catch(Exception e) - { + catch (Exception e) { p.kickPlayer("Banned for life! No, but stop trying to cheat in MobArena!"); - MobArena.warning(p.getName() + " tried to cheat in MobArena and has been kicked."); + if (plugin != null) { + plugin.warning(p.getName() + " tried to cheat in MobArena and has been kicked."); + } return Double.MAX_VALUE; } } - /** - * Convert a proper arena name to a config-file name. - * All spaces are replaced by underscores, and the whole String is - * lowercased. - */ - public static String nameArenaToConfig(String name) - { - String tmp = name.replace(" ", "_"); - return tmp.toLowerCase(); - } - /** * Convert a config-name to a proper spaced and capsed arena name. * The input String is split around all underscores, and every part @@ -1135,13 +193,13 @@ public class MAUtils public static String nameConfigToArena(String name) { String[] parts = name.split("_"); - if (parts.length == 1) + if (parts.length == 1) { return toCamelCase(parts[0]); + } String separator = " "; StringBuffer buffy = new StringBuffer(name.length()); - for (String part : parts) - { + for (String part : parts) { buffy.append(toCamelCase(part)); buffy.append(separator); } @@ -1154,53 +212,42 @@ public class MAUtils * Returns the input String with a capital first letter, and all the * other letters become lower case. */ - public static String toCamelCase(String name) - { + public static String toCamelCase(String name) { return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); } /** * Turn a list into a space-separated string-representation of the list. */ - public static String listToString(List list, boolean none, MobArena plugin) + public static String listToString(Collection list, boolean none, MobArena plugin) { - if (list == null || list.isEmpty()) - { - if (none) - return Msg.MISC_NONE.toString(); - else - return ""; + if (list == null || list.isEmpty()) { + return (none ? Msg.MISC_NONE.toString() : ""); } StringBuffer buffy = new StringBuffer(); int trimLength = 0; - E type = list.get(0); - if (type instanceof Player) - { - for (E e : list) - { + E type = list.iterator().next(); + if (type instanceof Player) { + for (E e : list) { buffy.append(((Player) e).getName()); buffy.append(" "); } } - else if (type instanceof ItemStack) - { + else if (type instanceof ItemStack) { trimLength = 2; ItemStack stack; - for (E e : list) - { + for (E e : list) { stack = (ItemStack) e; - if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) - { - if (plugin.Method != null) - { - buffy.append(plugin.Method.format(stack.getAmount())); + if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) { + String formatted = plugin.economyFormat(stack.getAmount()); + if (formatted != null) { + buffy.append(formatted); buffy.append(", "); } - else - { - MobArena.warning("Tried to do some money stuff, but no economy plugin was detected!"); + else { + plugin.warning("Tried to do some money stuff, but no economy plugin was detected!"); return buffy.toString(); } continue; @@ -1212,18 +259,15 @@ public class MAUtils buffy.append(", "); } } - else - { - for (E e : list) - { + else { + for (E e : list) { buffy.append(e.toString()); buffy.append(" "); } } return buffy.toString().substring(0, buffy.length() - trimLength); } - public static String listToString(List list, MobArena plugin) { return listToString(list, true, plugin); } - public static String listToString(List list) { return listToString(list, true, (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena")); } + public static String listToString(Collection list, JavaPlugin plugin) { return listToString(list, true, (MobArena) plugin); } /** * Returns a String-list version of a comma-separated list. @@ -1241,200 +285,6 @@ public class MAUtils return result; } - /** - * Turns the current set of players into an array, and grabs a random - * element out of it. - */ - public static Player getRandomPlayer(Arena arena) - { - Random random = new Random(); - //Player[] array = (Player[]) arena.livePlayers.toArray(); - Player[] array = (Player[]) arena.arenaPlayers.toArray(); - return array[random.nextInt(array.length)]; - } - - /** - * Verifies that all important variables are declared. Returns true - * if, and only if, the warppoints, region, distribution coefficients, - * and spawnpoints are all set up. - */ - public static boolean verifyData(Arena arena) - { - return ((arena.arenaLoc != null) && - (arena.lobbyLoc != null) && - (arena.spectatorLoc != null) && - (arena.p1 != null) && - (arena.p2 != null) && - (arena.spawnpoints.size() > 0)); - } - - public static boolean verifyLobby(Arena arena) - { - return ((arena.l1 != null) && - (arena.l2 != null)); - } - - public static void checkData(Arena arena, CommandSender p) - { - if (arena.arenaLoc == null) - tellPlayer(p, "Missing warp: arena"); - if (arena.lobbyLoc == null) - tellPlayer(p, "Missing warp: lobby"); - if (arena.spectatorLoc == null) - tellPlayer(p, "Missing warp: spectator"); - if (arena.p1 == null) - tellPlayer(p, "Missing region point: p1"); - if (arena.p2 == null) - tellPlayer(p, "Missing region point: p2"); - if (arena.spawnpoints.size() <= 0) - tellPlayer(p, "Missing spawnpoints"); - if (arena.setup) - tellPlayer(p, "Arena is ready to be used!"); - } - - /** - * Checks if there is a new update of MobArena and notifies the - * player if the boolean specified is true - */ - public static void checkForUpdates(MobArena plugin, final Player p, boolean response) - { - String site = "http://forums.bukkit.org/threads/19144/"; - try - { - // Make a URI of the site address - URI baseURI = new URI(site); - - // Open the connection and don't redirect. - HttpURLConnection con = (HttpURLConnection) baseURI.toURL().openConnection(); - con.setConnectTimeout(5000); - con.setInstanceFollowRedirects(false); - - String header = con.getHeaderField("Location"); - - // If something's wrong with the connection... - if (header == null) - { - tellPlayer(p, "Couldn't connect to the MobArena thread."); - return; - } - - // Otherwise, grab the location header to get the real URI. - String url = new URI(con.getHeaderField("Location")).toString(); - - // Set up the regex and matcher - Pattern regex = Pattern.compile("v([0-9]+-)*[0-9]+"); - Matcher matcher = regex.matcher(url); - if (!matcher.find()) - return; - - // Split the version strings - String[] forumVersion = matcher.group().substring(1).split("-"); - String[] thisVersion = plugin.getDescription().getVersion().split("\\."); - - // If the current version is older than the forum version, notify. - for (int i = 0; i < Math.min(forumVersion.length, thisVersion.length); i++) - { - if (Integer.parseInt(forumVersion[i]) > Integer.parseInt(thisVersion[i])) - { - tellPlayer(p, "There is a new version of MobArena available!");; - return; - } - } - - if (response) tellPlayer(p, "Your version of MobArena is up to date!"); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - public static int getSpawnMonsters(World world) - { - try - { - Field f = getSpawnMonstersField(world); - net.minecraft.server.World w = (net.minecraft.server.World) ((CraftWorld) world).getHandle(); - return f.getInt(w); - } - catch (Exception e) - { - e.printStackTrace(); - return 0; - } - } - - private static Field getSpawnMonstersField(World world) - { - try - { - int version = 1240; - - Pattern pattern = Pattern.compile("b([0-9][0-9][0-9][0-9])jnks"); - Matcher matcher = pattern.matcher(Bukkit.getVersion()); - if (matcher.find()) - { - String ver = matcher.group(1); - version = (ver.matches("[0-9]+")) ? Integer.parseInt(ver) : 0; - } - - String fieldName = (version >= 1191) ? "difficulty" : "spawnMonsters"; - return net.minecraft.server.World.class.getDeclaredField(fieldName); - } - catch (Exception e) - { - e.printStackTrace(); - MobArena.error("Your CraftBukkit version is messed up somehow..."); - } - - return null; - } - - public static void setSpawnFlags(MobArena plugin, World world, int spawnMonsters, boolean allowMonsters, boolean allowAnimals) - { - for (Arena arena : plugin.getAM().getArenasInWorld(world)) - if (arena.running) - return; - - //WorldServer ws = ((CraftWorld) world).getHandle(); - //ws.spawnMonsters = spawnMonsters; - //ws.allowMonsters = allowMonsters; - //ws.allowAnimals = allowAnimals; - - try - { - net.minecraft.server.World w = (net.minecraft.server.World) ((CraftWorld) world).getHandle(); - w.allowMonsters = allowMonsters; - w.allowAnimals = allowAnimals; - - Field f = getSpawnMonstersField(world); - f.setInt(w, spawnMonsters); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - public static String getDuration(long duration) - { - long seconds = duration / 1000; - long secs = seconds % 60; - long mins = (seconds - secs) / 60 % 60; - long hrs = ((seconds - secs) - (mins * 60)) / 60 / 60; - return hrs + ":" + ((mins < 10) ? "0" + mins : mins) + ":" + ((secs < 10) ? "0" + secs : secs); - } - - public static long parseDuration(String duration) - { - String[] elements = duration.split(":"); - long hrs = Long.parseLong(elements[0]) * 60 * 60; - long mins = Long.parseLong(elements[1]) * 60; - long secs = Long.parseLong(elements[2]); - - return (hrs + mins + secs) * 1000; - } - /** * Stand back, I'm going to try science! */ @@ -1444,13 +294,12 @@ public class MAUtils undoItHippieMonster(name, plugin, false); // Grab the Configuration and ArenaMaster - ArenaMaster am = plugin.getAM(); + ArenaMaster am = plugin.getArenaMaster(); // Create the arena node in the config-file. World world = loc.getWorld(); Arena arena = am.createArenaNode(name, world); - am.arenas.add(arena); - am.selectedArena = arena; + am.setSelectedArena(arena); // Get the hippie bounds. int x1 = (int)loc.getX() - radius; @@ -1461,7 +310,7 @@ public class MAUtils int z2 = (int)loc.getZ() + radius; int lx1 = x1; - int lx2 = x1 + am.classes.size() + 3; + int lx2 = x1 + am.getClasses().size() + 3; int ly1 = y1-6; int ly2 = y1-2; int lz1 = z1; @@ -1494,7 +343,7 @@ public class MAUtils catch (Exception e) { e.printStackTrace(); - MobArena.warning("Couldn't create backup file. Aborting auto-generate..."); + plugin.warning("Couldn't create backup file. Aborting auto-generate..."); return false; } @@ -1586,7 +435,8 @@ public class MAUtils world.getBlockAt(i,j,k).setTypeId(0); // Place the hippie signs - Iterator iterator = am.classes.iterator(); + //Iterator iterator = am.getClasses().iterator(); + Iterator iterator = am.getClasses().keySet().iterator(); for (int i = lx1+2; i <= lx2-2; i++) // Signs { world.getBlockAt(i,ly1+1,lz2-1).setTypeIdAndData(63, (byte)0x8, false); @@ -1595,18 +445,22 @@ public class MAUtils } world.getBlockAt(lx2-2,ly1+1,lz1+2).setType(Material.IRON_BLOCK); - // Set up the monster points. - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "p1", new Location(world, x1, ly1, z1)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "p2", new Location(world, x2, y2+1, z2)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "arena", new Location(world, loc.getX(), y1+1, loc.getZ())); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "lobby", new Location(world, x1+2, ly1+1, z1+2)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "spectator", new Location(world, loc.getX(), y2+1, loc.getZ())); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "spawnpoints.s1", new Location(world, x1+3, y1+2, z1+3)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "spawnpoints.s2", new Location(world, x1+3, y1+2, z2-3)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "spawnpoints.s3", new Location(world, x2-3, y1+2, z1+3)); - MAUtils.setArenaCoord(plugin.getMAConfig(), arena, "spawnpoints.s4", new Location(world, x2-3, y1+2, z2-3)); + // Set up the monster points. + ArenaRegion region = arena.getRegion(); + region.set("p1", new Location(world, x1, ly1, z1)); + region.set("p2", new Location(world, x2, y2+1, z2)); - am.updateAll(); + region.set("arena", new Location(world, loc.getX(), y1+1, loc.getZ())); + region.set("lobby", new Location(world, x1+2, ly1+1, z1+2)); + region.set("spectator", new Location(world, loc.getX(), y2+1, loc.getZ())); + + region.addSpawn("s1", new Location(world, x1+3, y1+2, z1+3)); + region.addSpawn("s2", new Location(world, x1+3, y1+2, z2-3)); + region.addSpawn("s3", new Location(world, x2-3, y1+2, z1+3)); + region.addSpawn("s4", new Location(world, x2-3, y1+2, z2-3)); + region.save(); + + am.reloadConfig(); return true; } @@ -1627,11 +481,11 @@ public class MAUtils } catch (Exception e) { - if (error) MobArena.warning("Couldn't find backup file for arena '" + name + "'"); + if (error) plugin.warning("Couldn't find backup file for arena '" + name + "'"); return false; } - World world = Bukkit.getServer().getWorld(preciousPatch.keySet().iterator().next().getWorld()); + World world = plugin.getServer().getWorld(preciousPatch.keySet().iterator().next().getWorld()); for (Map.Entry entry : preciousPatch.entrySet()) { @@ -1639,12 +493,12 @@ public class MAUtils } Config config = plugin.getMAConfig(); - config.removeProperty("arenas." + name); + config.remove("arenas." + name); config.save(); file.delete(); - plugin.getAM().updateAll(); + plugin.getArenaMaster().reloadConfig(); return true; } } \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/Messenger.java b/src/com/garbagemule/MobArena/Messenger.java new file mode 100644 index 0000000..bdaa140 --- /dev/null +++ b/src/com/garbagemule/MobArena/Messenger.java @@ -0,0 +1,103 @@ +package com.garbagemule.MobArena; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.getspout.spoutapi.SpoutManager; +import org.getspout.spoutapi.player.SpoutPlayer; + +import com.garbagemule.MobArena.framework.Arena; + +public class Messenger +{ + public static boolean tellSpoutPlayer(Player p, Msg msg, String s, Material logo) { + // Grab the SpoutPlayer. + SpoutPlayer sp = MobArena.hasSpout ? SpoutManager.getPlayer(p) : null; + + if (msg.hasSpoutMsg() && sp != null && sp.isSpoutCraftEnabled()) { + // Grab the message text. + String text = msg.toSpoutString(s); + + // If more than 26 characters, truncate. + if (text.length() > 26) + text = text.substring(0, 26); + + // If the logo is null, use an iron sword. + if (logo == null) + logo = msg.getLogo(); + + // Send the notification. + sp.sendNotification("MobArena", text, logo, (short) 0, 2000); + return true; + } + else { + return tellPlayer(p, msg.toString(s)); + } + } + + public static boolean tellSpoutPlayer(Player p, Msg msg, Material logo) { + return tellSpoutPlayer(p, msg, null, logo); + } + + public static boolean tellSpoutPlayer(Player p, Msg msg, String s) { + return tellSpoutPlayer(p, msg, s, null); + } + + public static boolean tellSpoutPlayer(Player p, Msg msg) { + return tellSpoutPlayer(p, msg, null, null); + } + + public static boolean tellPlayer(CommandSender p, String msg) { + // If the input sender is null or the string is empty, return. + if (p == null || msg.equals(" ")) + return false; + + // Otherwise, send the message with the [MobArena] tag. + p.sendMessage(ChatColor.GREEN + "[MobArena] " + ChatColor.WHITE + msg); + return true; + } + + public static boolean tellPlayer(CommandSender p, Msg msg, String s, boolean spout, Material logo) { + if (spout && p instanceof Player) + return tellSpoutPlayer((Player) p, msg, s, logo); + + return tellPlayer(p, msg.toString(s)); + } + + public static boolean tellPlayer(CommandSender p, Msg msg, String s, Material logo) { + return tellPlayer(p, msg, s, MobArena.hasSpout, logo); + } + + public static boolean tellPlayer(CommandSender p, Msg msg, String s) { + return tellPlayer(p, msg, s, MobArena.hasSpout, null); + } + + public static boolean tellPlayer(CommandSender p, Msg msg) { + return tellPlayer(p, msg, null, MobArena.hasSpout, null); + } + + public static void tellAll(Arena arena, Msg msg, String s, boolean notifyPlayers) { + List players = new ArrayList(); + players.addAll(arena.getPlayersInArena()); + players.addAll(arena.getPlayersInLobby()); + players.addAll(arena.getSpectators()); + for (Player p : players) + tellPlayer(p, msg, s); + } + + public static void tellAll(Arena arena, Msg msg, String s) { + tellAll(arena, msg, s, false); + } + + public static void tellAll(Arena arena, Msg msg, boolean notifyPlayers) { + tellAll(arena, msg, null, notifyPlayers); + } + + public static void tellAll(Arena arena, Msg msg) { + tellAll(arena, msg, null, false); + } +} diff --git a/src/com/garbagemule/MobArena/MobArena.java b/src/com/garbagemule/MobArena/MobArena.java index c4549ed..1a60211 100644 --- a/src/com/garbagemule/MobArena/MobArena.java +++ b/src/com/garbagemule/MobArena/MobArena.java @@ -1,221 +1,279 @@ package com.garbagemule.MobArena; import java.io.File; +import java.util.HashSet; import java.util.Random; +import java.util.Set; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import net.milkbowl.vault.economy.EconomyResponse.ResponseType; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.Event.Priority; -import org.bukkit.event.block.BlockListener; -import org.bukkit.event.player.PlayerListener; -import org.bukkit.event.entity.EntityListener; import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.PluginManager; -//import org.bukkit.util.config.Configuration; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.ServicesManager; +import org.bukkit.plugin.java.JavaPlugin; +import com.garbagemule.MobArena.ArenaMasterImpl; +import com.garbagemule.MobArena.MAMessages; +import com.garbagemule.MobArena.commands.CommandHandler; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.health.HealthStrategy; +import com.garbagemule.MobArena.health.HealthStrategyHeroes; +import com.garbagemule.MobArena.health.HealthStrategyStandard; +import com.garbagemule.MobArena.listeners.MAGlobalListener; import com.garbagemule.MobArena.listeners.MagicSpellsListener; -import com.garbagemule.MobArena.spout.Spouty; -import com.garbagemule.MobArena.util.Config; +import com.garbagemule.MobArena.listeners.SpoutScreenListener; import com.garbagemule.MobArena.util.FileUtils; -import com.garbagemule.register.payment.Method; -import com.garbagemule.register.payment.Methods; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.util.config.ConfigUtils; +import com.garbagemule.MobArena.util.inventory.InventoryManager; /** * MobArena * @author garbagemule */ -public class MobArena extends JavaPlugin implements MobArenaPlugin +public class MobArena extends JavaPlugin { - //private Configuration config; private Config config; - private ArenaMaster am; + private ArenaMaster arenaMaster; - // Economy stuff - protected Method Method; + // Inventories from disconnects + private Set inventoriesToRestore; + + // Heroes + private boolean hasHeroes; + private HealthStrategy healthStrategy; + + // Vault + private Economy economy; // Spout stuff public static boolean hasSpout; - // Global variables - public static PluginDescriptionFile desc; - public static File dir, arenaDir; - public static final double MIN_PLAYER_DISTANCE = 15D; public static final double MIN_PLAYER_DISTANCE_SQUARED = 225D; public static final int ECONOMY_MONEY_ID = -29; public static Random random = new Random(); - public void onEnable() - { - // Description file and data folders - desc = getDescription(); - dir = getDataFolder(); - arenaDir = new File(dir, "arenas"); - if (!dir.exists()) dir.mkdirs(); - if (!arenaDir.exists()) arenaDir.mkdir(); - + public void onEnable() { // Create default files and initialize config-file - FileUtils.extractDefaults("config.yml"); - loadConfig(); + FileUtils.extractDefaults(this, "config.yml"); + loadConfigFile(); // Download external libraries if needed. - FileUtils.fetchLibs(config); + FileUtils.fetchLibs(this, config); // Set up soft dependencies - setupRegister(); + setupVault(); + setupHeroes(); setupSpout(); setupMagicSpells(); + setupStrategies(); - // Set up the ArenaMaster and the announcements - am = new ArenaMasterStandard(this); - am.initialize(); + // Set up the ArenaMaster + arenaMaster = new ArenaMasterImpl(this); + arenaMaster.initialize(); + // Register any inventories to restore. + registerInventories(); + + // Make sure all the announcements are configured. MAMessages.init(this); // Register event listeners registerListeners(); // Announce enable! - info("v" + desc.getVersion() + " enabled."); + info("v" + this.getDescription().getVersion() + " enabled."); } - public void onDisable() - { + public void onDisable() { // Disable Spout features. hasSpout = false; // Force all arenas to end. - if (am == null) return; - for (Arena arena : am.arenas) + if (arenaMaster == null) return; + for (Arena arena : arenaMaster.getArenas()) { arena.forceEnd(); - am.arenaMap.clear(); + } + arenaMaster.resetArenaMap(); info("disabled."); } - private void loadConfig() - { - File file = new File(dir, "config.yml"); + private void loadConfigFile() { + File dir = this.getDataFolder(); + if (!dir.exists()) dir.mkdir(); + + File file = new File(this.getDataFolder(), "config.yml"); config = new Config(file); config.load(); + + updateSettings(config); config.setHeader(getHeader()); + config.save(); } - private void registerListeners() - { + private void registerListeners() { // Bind the /ma, /mobarena commands to MACommands. - MACommands commandExecutor = new MACommands(this, am); - getCommand("ma").setExecutor(commandExecutor); - getCommand("mobarena").setExecutor(commandExecutor); + CommandHandler handler = new CommandHandler(this); + getCommand("ma").setExecutor(handler); + getCommand("mobarena").setExecutor(handler); - // Create event listeners. - PluginManager pm = getServer().getPluginManager(); - PlayerListener playerListener = new MAPlayerListener(this, am); - EntityListener entityListener = new MAEntityListener(am); - BlockListener blockListener = new MABlockListener(am); + PluginManager pm = this.getServer().getPluginManager(); + pm.registerEvents(new MAGlobalListener(this, arenaMaster), this); - // Register events. - pm.registerEvent(Event.Type.BLOCK_BREAK, blockListener, Priority.Highest, this); - pm.registerEvent(Event.Type.BLOCK_BURN, blockListener, Priority.Highest, this); - pm.registerEvent(Event.Type.BLOCK_PLACE, blockListener, Priority.Highest, this); - pm.registerEvent(Event.Type.BLOCK_IGNITE, blockListener, Priority.Highest, this); - pm.registerEvent(Event.Type.SIGN_CHANGE, blockListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_INTERACT, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_DROP_ITEM, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_BUCKET_EMPTY, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_TELEPORT, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_QUIT, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_KICK, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_JOIN, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.PLAYER_ANIMATION, playerListener, Priority.Normal, this); - pm.registerEvent(Event.Type.ENTITY_DAMAGE, entityListener, Priority.Low, this); // Must cancel before Heroes - pm.registerEvent(Event.Type.ENTITY_DEATH, entityListener, Priority.Lowest, this); // Lowest because of Tombstone - pm.registerEvent(Event.Type.ENTITY_REGAIN_HEALTH, entityListener, Priority.Normal, this); - pm.registerEvent(Event.Type.ENTITY_EXPLODE, entityListener, Priority.Highest, this); - pm.registerEvent(Event.Type.ENTITY_COMBUST, entityListener, Priority.Normal, this); - pm.registerEvent(Event.Type.ENTITY_TARGET, entityListener, Priority.Normal, this); - pm.registerEvent(Event.Type.ENDERMAN_PICKUP, entityListener, Priority.Normal, this); - pm.registerEvent(Event.Type.ENDERMAN_PLACE, entityListener, Priority.Normal, this); - pm.registerEvent(Event.Type.CREATURE_SPAWN, entityListener, Priority.Highest, this); - pm.registerEvent(Event.Type.PLAYER_COMMAND_PREPROCESS, playerListener, Priority.Monitor, this); // I know Monitor is bad, but other plugins suck! :( + if (hasSpout) { + pm.registerEvents(new SpoutScreenListener(this), this); + } } // Permissions stuff - public boolean has(Player p, String s) - { - // If the permission is set, check if player has permission - if (p.isPermissionSet(s)) - return p.hasPermission(s); - - // Otherwise, only allow commands that aren't admin/setup commands. - return !s.matches("^mobarena\\.setup\\..*$") && !s.matches("^mobarena\\.admin\\..*$"); + public boolean has(Player p, String s) { + return p.hasPermission(s); + } + + public boolean has(CommandSender sender, String s) { + if (sender instanceof ConsoleCommandSender) { + return true; + } + return has((Player) sender, s); } // Console printing - public static void info(String msg) { Bukkit.getServer().getLogger().info("[MobArena] " + msg); } - public static void warning(String msg) { Bukkit.getServer().getLogger().warning("[MobArena] " + msg); } - public static void error(String msg) { Bukkit.getServer().getLogger().severe("[MobArena] " + msg); } + public void info(String msg) { getServer().getLogger().info("[MobArena] " + msg); } + public void warning(String msg) { getServer().getLogger().warning("[MobArena] " + msg); } + public void error(String msg) { getServer().getLogger().severe("[MobArena] " + msg); } - @Override - public void tell(CommandSender sender, String msg) { - if (sender == null || msg.equals("") || msg.equals(" ")) + private void setupVault() { + Plugin vaultPlugin = this.getServer().getPluginManager().getPlugin("Vault"); + if (vaultPlugin == null) { + warning("Vault was not found. Economy rewards will not work!"); return; + } - sender.sendMessage(ChatColor.GREEN + "[MobArena] " + ChatColor.WHITE + msg); + ServicesManager manager = this.getServer().getServicesManager(); + RegisteredServiceProvider e = manager.getRegistration(net.milkbowl.vault.economy.Economy.class); + + if (e != null) { + economy = e.getProvider(); + } else { + warning("Vault found, but no economy plugin detected. Economy rewards will not work!"); + } } - @Override - public void tell(CommandSender sender, Msg msg) { - tell(sender, msg.toString()); + private void setupHeroes() { + Plugin heroesPlugin = this.getServer().getPluginManager().getPlugin("Heroes"); + if (heroesPlugin == null) return; + + hasHeroes = true; } - private void setupRegister() - { - Methods.setMethod(getServer().getPluginManager()); - - Method = Methods.getMethod(); - if (Method != null) - info("Payment method found (" + Method.getName() + " version: " + Method.getVersion() + ")"); - else - info("No payment method found!"); - } - - private void setupSpout() - { - if (hasSpout) return; - + private void setupSpout() { Plugin spoutPlugin = this.getServer().getPluginManager().getPlugin("Spout"); - hasSpout = spoutPlugin != null; - if (!hasSpout) return; + if (spoutPlugin == null) return; - Spouty.registerEvents(this); + hasSpout = true; } - private void setupMagicSpells() - { + private void setupMagicSpells() { Plugin spells = this.getServer().getPluginManager().getPlugin("MagicSpells"); if (spells == null) return; - PluginManager pm = getServer().getPluginManager(); - - pm.registerEvent(Event.Type.CUSTOM_EVENT, new MagicSpellsListener(this), Priority.Normal, this); + this.getServer().getPluginManager().registerEvents(new MagicSpellsListener(this), this); } - public Config getMAConfig() { return config; } - public ArenaMaster getAM() { return am; } // More convenient. + private void setupStrategies() { + healthStrategy = (hasHeroes ? new HealthStrategyHeroes() : new HealthStrategyStandard()); + } - @Override - public ArenaMaster getArenaMaster() { return am; } + public HealthStrategy getHealthStrategy() { + return healthStrategy; + } - private String getHeader() - { + public Config getMAConfig() { + return config; + } + + public ArenaMaster getArenaMaster() { + return arenaMaster; + } + + private void updateSettings(Config config) { + Set arenas = config.getKeys("arenas"); + if (arenas == null) return; + + for (String arena : arenas) { + String path = "arenas." + arena + ".settings"; + ConfigUtils.replaceAllNodes(this, config, path, "settings.yml"); + } + } + + private String getHeader() { String sep = System.getProperty("line.separator"); - return "# MobArena v" + desc.getVersion() + " - Config-file" + sep + - "# Read the Wiki for details on how to set up this file: http://goo.gl/F5TTc" + sep + - "# Note: You -must- use spaces instead of tabs!"; + return "MobArena v" + this.getDescription().getVersion() + " - Config-file" + sep + + "Read the Wiki for details on how to set up this file: http://goo.gl/F5TTc" + sep + + "Note: You -must- use spaces instead of tabs!\r"; + } + + private void registerInventories() { + this.inventoriesToRestore = new HashSet(); + + File dir = new File(getDataFolder(), "inventories"); + if (!dir.exists()) { + dir.mkdir(); + return; + } + + for (File f : dir.listFiles()) { + if (f.getName().endsWith(".inv")) { + inventoriesToRestore.add(f.getName().substring(0, f.getName().indexOf("."))); + } + } + } + + public void restoreInventory(Player p) { + if (!inventoriesToRestore.contains(p.getName())) { + return; + } + + if (InventoryManager.restoreFromFile(this, p)) { + inventoriesToRestore.remove(p.getName()); + } + } + + public boolean giveMoney(Player p, int amount) { + if (economy != null) { + EconomyResponse result = economy.depositPlayer(p.getName(), amount); + return (result.type == ResponseType.SUCCESS); + } + return false; + } + + public boolean takeMoney(Player p, int amount) { + if (economy != null) { + EconomyResponse result = economy.withdrawPlayer(p.getName(), amount); + return (result.type == ResponseType.SUCCESS); + } + return false; + } + + public boolean hasEnough(Player p, double amount) { + if (economy != null) { + return (economy.getBalance(p.getName()) >= amount); + } + return true; + } + + public String economyFormat(double amount) { + if (economy != null) { + return economy.format(amount); + } + return null; } } \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/MobArenaHandler.java b/src/com/garbagemule/MobArena/MobArenaHandler.java index 1b429f8..147a33c 100644 --- a/src/com/garbagemule/MobArena/MobArenaHandler.java +++ b/src/com/garbagemule/MobArena/MobArenaHandler.java @@ -6,16 +6,18 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; + public class MobArenaHandler { private MobArena plugin; /** * Primary constructor. - * The boolean 'ma' is flagged true, and the field 'plugin' is initalized, if the server is running MobArena. + * The field 'plugin' is initalized, if the server is running MobArena. */ - public MobArenaHandler() - { + public MobArenaHandler() { plugin = (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena"); } @@ -32,12 +34,13 @@ public class MobArenaHandler * @param loc A location. * @return true, if the Location is inside of any arena region. */ - public boolean inRegion(Location loc) - { - for (Arena arena : plugin.getAM().arenas) - if (arena.inRegion(loc)) + public boolean inRegion(Location loc) { + for (Arena arena : plugin.getArenaMaster().getArenas()) { + if (arena.getRegion().contains(loc)) { return true; - + } + } + return false; } @@ -47,9 +50,8 @@ public class MobArenaHandler * @param loc A location * @return true, if the Location is inside of the arena region. */ - public boolean inRegion(Arena arena, Location loc) - { - return (arena != null && arena.inRegion(loc)); + public boolean inRegion(Arena arena, Location loc) { + return (arena != null && arena.getRegion().contains(loc)); } /** @@ -58,13 +60,12 @@ public class MobArenaHandler * @param loc A location * @return true, if the Location is inside of the arena region. */ - public boolean inRegion(String arenaName, Location loc) - { - Arena arena = plugin.getAM().getArenaWithName(arenaName); + public boolean inRegion(String arenaName, Location loc) { + Arena arena = plugin.getArenaMaster().getArenaWithName(arenaName); if (arena == null) throw new NullPointerException("There is no arena with that name"); - - return arena.inRegion(loc); + + return arena.getRegion().contains(loc); } /** @@ -72,8 +73,7 @@ public class MobArenaHandler * @param loc A location. * @return true, if the Location is inside of the region of an arena that is currently running. */ - public boolean inRunningRegion(Location loc) - { + public boolean inRunningRegion(Location loc) { return inRegion(loc, false, true); } @@ -82,8 +82,7 @@ public class MobArenaHandler * @param loc A location. * @return true, if the Location is inside of the region of an arena that is currently enabled. */ - public boolean inEnabledRegion(Location loc) - { + public boolean inEnabledRegion(Location loc) { return inRegion(loc, true, false); } @@ -94,17 +93,19 @@ public class MobArenaHandler * @param running if true, the method will check if the arena is running, overrides enabled * @return true, if the location is inside of the region of an arena that is currently enabled/running, depending on the parameters. */ - private boolean inRegion(Location loc, boolean enabled, boolean running) - { + private boolean inRegion(Location loc, boolean enabled, boolean running) { // If the plugin doesn't exist, always return false. - if (plugin.getAM() == null) return false; - + if (plugin.getArenaMaster() == null) return false; + // Return true if location is within just one arena's region. - for (Arena arena : plugin.getAM().arenas) - if (arena.inRegion(loc)) - if ((running && arena.running) || (enabled && arena.enabled)) + for (Arena arena : plugin.getArenaMaster().getArenas()) { + if (arena.getRegion().contains(loc)) { + if ((running && arena.isRunning()) || (enabled && arena.isEnabled())) { return true; - + } + } + } + return false; } @@ -121,9 +122,8 @@ public class MobArenaHandler * @param player The player * @return true, if the player is in an arena */ - public boolean isPlayerInArena(Player player) - { - return (plugin.getAM().getArenaWithPlayer(player) != null); + public boolean isPlayerInArena(Player player) { + return (plugin.getArenaMaster().getArenaWithPlayer(player) != null); } /** @@ -131,9 +131,8 @@ public class MobArenaHandler * @param playerName The name of the player * @return true, if the player is in an arena */ - public boolean isPlayerInArena(String playerName) - { - return (plugin.getAM().getArenaWithPlayer(playerName) != null); + public boolean isPlayerInArena(String playerName) { + return (plugin.getArenaMaster().getArenaWithPlayer(playerName) != null); } /** @@ -141,12 +140,11 @@ public class MobArenaHandler * @param player The player * @return The class name of the player if the player is in the arena, null otherwise */ - public String getPlayerClass(Player player) - { - Arena arena = plugin.getAM().getArenaWithPlayer(player); + public String getPlayerClass(Player player) { + Arena arena = plugin.getArenaMaster().getArenaWithPlayer(player); if (arena == null) return null; - - return arena.classMap.get(player); + + return getPlayerClass(arena, player); } /** @@ -156,9 +154,14 @@ public class MobArenaHandler * @param player The player to look up * @return The class name of the player, if the player is in the arena, null otherwise */ - public String getPlayerClass(Arena arena, Player player) - { - return arena.classMap.get(player); + public String getPlayerClass(Arena arena, Player player) { + ArenaPlayer ap = arena.getArenaPlayer(player); + if (ap == null) return null; + + ArenaClass ac = ap.getArenaClass(); + if (ac == null) return null; + + return ac.getName(); } /** @@ -166,9 +169,8 @@ public class MobArenaHandler * @param entity The monster entity * @return true, if the monster is in an arena */ - public boolean isMonsterInArena(LivingEntity entity) - { - return plugin.getAM().getArenaWithMonster(entity) != null; + public boolean isMonsterInArena(LivingEntity entity) { + return plugin.getArenaMaster().getArenaWithMonster(entity) != null; } /** @@ -176,9 +178,8 @@ public class MobArenaHandler * @param wolf The pet wolf * @return true, if the pet is in an arena */ - public boolean isPetInArena(LivingEntity wolf) - { - return plugin.getAM().getArenaWithPet(wolf) != null; + public boolean isPetInArena(LivingEntity wolf) { + return plugin.getArenaMaster().getArenaWithPet(wolf) != null; } @@ -194,9 +195,8 @@ public class MobArenaHandler * @param loc A location * @return an Arena object, or null */ - public Arena getArenaAtLocation(Location loc) - { - return plugin.getAM().getArenaAtLocation(loc); + public Arena getArenaAtLocation(Location loc) { + return plugin.getArenaMaster().getArenaAtLocation(loc); } /** @@ -204,9 +204,8 @@ public class MobArenaHandler * @param p A player * @return an Arena object, or null */ - public Arena getArenaWithPlayer(Player p) - { - return plugin.getAM().getArenaWithPlayer(p); + public Arena getArenaWithPlayer(Player p) { + return plugin.getArenaMaster().getArenaWithPlayer(p); } /** @@ -214,9 +213,8 @@ public class MobArenaHandler * @param wolf A pet wolf * @return an Arena object, or null */ - public Arena getArenaWithPet(Entity wolf) - { - return plugin.getAM().getArenaWithPet(wolf); + public Arena getArenaWithPet(Entity wolf) { + return plugin.getArenaMaster().getArenaWithPet(wolf); } /** @@ -224,8 +222,7 @@ public class MobArenaHandler * @param monster A monster * @return an Arena object, or null */ - public Arena getArenaWithMonster(Entity monster) - { - return plugin.getAM().getArenaWithMonster(monster); + public Arena getArenaWithMonster(Entity monster) { + return plugin.getArenaMaster().getArenaWithMonster(monster); } } diff --git a/src/com/garbagemule/MobArena/MobArenaListener.java b/src/com/garbagemule/MobArena/MobArenaListener.java deleted file mode 100644 index 0391c6b..0000000 --- a/src/com/garbagemule/MobArena/MobArenaListener.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.garbagemule.MobArena; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.waves.Wave.WaveBranch; -import com.garbagemule.MobArena.waves.Wave.WaveType; - -public abstract class MobArenaListener -{ - protected MobArena plugin; - - public MobArenaListener() - { - plugin = (MobArena) Bukkit.getServer().getPluginManager().getPlugin("MobArena"); - plugin.getAM().listeners.add(this); - } - - public void onArenaStart(Arena arena) {} - public void onArenaEnd(Arena arena) {} - public void onWave(Arena arena, int waveNumber, String waveName, WaveBranch waveBranch, WaveType waveType) {} - public void onPlayerJoin(Arena arena, Player p) {} - public void onPlayerLeave(Arena arena, Player p) {} - public void onPlayerDeath(Arena arena, Player p) {} -} diff --git a/src/com/garbagemule/MobArena/MobArenaPlugin.java b/src/com/garbagemule/MobArena/MobArenaPlugin.java deleted file mode 100644 index 33ef25e..0000000 --- a/src/com/garbagemule/MobArena/MobArenaPlugin.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.garbagemule.MobArena; - -import org.bukkit.command.CommandSender; - -import com.garbagemule.MobArena.util.Config; - -public interface MobArenaPlugin -{ - /** - * Get the Config object associated with this MobArena instance. - * @return the Config object used by MobArena - */ - public Config getMAConfig(); - - /** - * Get the ArenaMaster used by this instance of MobArena. - * @return the ArenaMaster object - */ - public ArenaMaster getArenaMaster(); - - /** - * Send a message to a player or the console. - * The message is prefixed with [MobArena]. - * @param recipient the player or console to send the message to - * @param msg the message to send - */ - public void tell(CommandSender recipient, String msg); - - /** - * Send a predefined announcement to a player or the console. - * Convenience method: the above method is called with Msg.toString() - * @param recipient the player or console to send the message to - * @param msg the message to send - */ - public void tell(CommandSender recipient, Msg msg); -} diff --git a/src/com/garbagemule/MobArena/MonsterManager.java b/src/com/garbagemule/MobArena/MonsterManager.java new file mode 100644 index 0000000..4f97228 --- /dev/null +++ b/src/com/garbagemule/MobArena/MonsterManager.java @@ -0,0 +1,130 @@ +package com.garbagemule.MobArena; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Wolf; +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.waves.MABoss; + +public class MonsterManager +{ + private Set monsters, sheep; + private Set pets; + private Map bosses; + private Map> suppliers; + + public MonsterManager() { + this.monsters = new HashSet(); + this.sheep = new HashSet(); + this.pets = new HashSet(); + this.bosses = new HashMap(); + this.suppliers = new HashMap>(); + } + + public void reset() { + monsters.clear(); + sheep.clear(); + pets.clear(); + bosses.clear(); + suppliers.clear(); + } + + public void clear() { + removeAll(monsters); + removeAll(sheep); + removeAll(pets); + removeAll(bosses.keySet()); + removeAll(suppliers.keySet()); + + reset(); + } + + private void removeAll(Collection collection) { + for (LivingEntity e : collection) { + if (e != null) { + e.remove(); + } + } + } + + public Set getMonsters() { + return monsters; + } + + public void addMonster(LivingEntity e) { + monsters.add(e); + } + + public boolean removeMonster(Entity e) { + return monsters.remove(e); + } + + public Set getExplodingSheep() { + return sheep; + } + + public void addExplodingSheep(LivingEntity e) { + sheep.add(e); + } + + public boolean removeExplodingSheep(LivingEntity e) { + return sheep.remove(e); + } + + public Set getPets() { + return pets; + } + + public void addPet(Wolf w) { + pets.add(w); + } + + public boolean hasPet(Entity e) { + return pets.contains(e); + } + + public void removePets(Player p) { + for (Wolf w : pets) { + if (w == null || !(w.getOwner() instanceof Player) || !((Player) w.getOwner()).getName().equals(p.getName())) + continue; + + w.setOwner(null); + w.remove(); + } + } + + public void addSupplier(LivingEntity e, List drops) { + suppliers.put(e, drops); + } + + public List getLoot(Entity e) { + return suppliers.get(e); + } + + public MABoss addBoss(LivingEntity e, int maxHealth) { + MABoss b = new MABoss(e, maxHealth); + bosses.put(e, b); + return b; + } + + public MABoss removeBoss(LivingEntity e) { + return bosses.remove(e); + } + + public MABoss getBoss(LivingEntity e) { + return bosses.get(e); + } + + public Set getBossMonsters() { + return bosses.keySet(); + } +} diff --git a/src/com/garbagemule/MobArena/Msg.java b/src/com/garbagemule/MobArena/Msg.java index cbe8a61..ede3e66 100644 --- a/src/com/garbagemule/MobArena/Msg.java +++ b/src/com/garbagemule/MobArena/Msg.java @@ -53,6 +53,8 @@ public enum Msg WAVE_DEFAULT("Wave #%!", "Wave #%!", Material.YELLOW_FLOWER), WAVE_SPECIAL("Wave #%! [SPECIAL]", "Wave #%! [SPECIAL]", Material.RED_ROSE), WAVE_SWARM("Wave #%! [SWARM]", "Wave #%! [SWARM]", Material.LONG_GRASS), + WAVE_SUPPLY("Wave #%! [SUPPLY]", "Wave #%! [SUPPLY]", Material.BREAD), + WAVE_UPGRADE("Wave #%! [UPGRADE]", "Wave #%! [UPGRADE]", Material.DIAMOND), WAVE_BOSS("Wave #%! [BOSS]", "Wave #%! [BOSS]", Material.FIRE), WAVE_BOSS_ABILITY("Boss used ability: %!", "Boss: %", Material.FIRE), WAVE_BOSS_LOW_HEALTH("Boss is almost dead!", "Boss almost dead!", Material.FIRE), @@ -61,6 +63,10 @@ public enum Msg MISC_LIST_ARENAS("Available arenas: %"), MISC_COMMAND_NOT_ALLOWED("You can't use that command in the arena!"), MISC_NO_ACCESS("You don't have access to this command."), + MISC_NOT_FROM_CONSOLE("You can't use this command from the console."), + MISC_HELP("For a list of commands, type /ma help"), + MISC_MULTIPLE_MATCHES("Did you mean one of these commands?"), + MISC_NO_MATCHES("Command not found. Type /ma help"), MISC_NONE(""); private String msg, spoutMsg; diff --git a/src/com/garbagemule/MobArena/PlayerData.java b/src/com/garbagemule/MobArena/PlayerData.java new file mode 100644 index 0000000..439dcbb --- /dev/null +++ b/src/com/garbagemule/MobArena/PlayerData.java @@ -0,0 +1,108 @@ +package com.garbagemule.MobArena; + +import java.util.Collection; + +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffect; + +public class PlayerData +{ + private Player player; + + private int health, food, level; + private float exp; + private GameMode mode = null; + private Location entry = null; + private Collection potions; + + public PlayerData(Player player) { + this.player = player; + this.mode = player.getGameMode(); + this.entry = player.getLocation(); + this.potions = player.getActivePotionEffects(); + + update(); + } + + /** + * Updates the information that is restored, when a player + * dies in the arena, that is, health, food level, and + * experience. Used when a player re-joins an arena while + * already being a spectator. + */ + public void update() { + this.health = player.getHealth(); + this.food = player.getFoodLevel(); + this.level = player.getLevel(); + this.exp = player.getExp(); + } + + /** + * Restores health, food level, and experience as per the + * currently stored values of this object. Used when a + * player leaves the arena. + */ + public void restoreData() { + player.setFoodLevel(food); + player.setLevel(level); + player.setExp(exp); + } + + public Player getPlayer() { + return player; + } + + public int health() { + return health; + } + + public void setHealth(int health) { + this.health = health; + } + + public int food() { + return food; + } + + public void setFood(int food) { + this.food = food; + } + + public int level() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + + public float exp() { + return exp; + } + + public void setExp(int exp) { + this.exp = exp; + } + + public GameMode getMode() { + return mode; + } + + public Collection getPotionEffects() { + return potions; + } + + public void setMode(GameMode mode) { + this.mode = mode; + } + + public Location entry() { + return entry; + } + + public void setEntry(Location entry) { + this.entry = entry; + } +} diff --git a/src/com/garbagemule/MobArena/RewardManager.java b/src/com/garbagemule/MobArena/RewardManager.java new file mode 100644 index 0000000..3c186d6 --- /dev/null +++ b/src/com/garbagemule/MobArena/RewardManager.java @@ -0,0 +1,63 @@ +package com.garbagemule.MobArena; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.framework.Arena; + +public class RewardManager +{ + private MobArena plugin; + @SuppressWarnings("unused") + private Arena arena; + private Map> players; + private Set rewarded; + + public RewardManager(Arena arena) { + this.plugin = arena.getPlugin(); + this.arena = arena; + this.players = new HashMap>(); + this.rewarded = new HashSet(); + } + + public void reset() { + players.clear(); + rewarded.clear(); + } + + public void addReward(Player p, ItemStack stack) { + if (!players.containsKey(p)) { + players.put(p, new ArrayList()); + } + players.get(p).add(stack); + } + + public void grantRewards(Player p) { + if (rewarded.contains(p)) return; + + List rewards = players.get(p); + if (rewards == null) return; + + for (ItemStack stack : rewards) { + if (stack == null) { + continue; + } + + if (stack.getTypeId() == MobArena.ECONOMY_MONEY_ID) { + plugin.giveMoney(p, stack.getAmount()); + continue; + } + + p.getInventory().addItem(stack); + } + + rewarded.add(p); + } +} diff --git a/src/com/garbagemule/MobArena/commands/Command.java b/src/com/garbagemule/MobArena/commands/Command.java new file mode 100644 index 0000000..9851e53 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/Command.java @@ -0,0 +1,20 @@ +package com.garbagemule.MobArena.commands; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.framework.ArenaMaster; + +public interface Command +{ + /** + * Execute the command using the given arguments. + * If any arguments are provided, the first argument is -NOT- the + * same as the command name, i.e. the CommandHandler must strip the + * array of this element. + * @param am an ArenaMaster instance + * @param sender the sender + * @param args array of arguments + * @return true, if the command was successful + */ + public boolean execute(ArenaMaster am, CommandSender sender, String... args); +} diff --git a/src/com/garbagemule/MobArena/commands/CommandHandler.java b/src/com/garbagemule/MobArena/commands/CommandHandler.java index c3c6362..2c2fa65 100644 --- a/src/com/garbagemule/MobArena/commands/CommandHandler.java +++ b/src/com/garbagemule/MobArena/commands/CommandHandler.java @@ -1,123 +1,217 @@ package com.garbagemule.MobArena.commands; -import java.io.File; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; -import org.bukkit.command.*; -import org.bukkit.entity.Player; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; import com.garbagemule.MobArena.*; -import com.garbagemule.MobArena.util.classparsing.*; +import com.garbagemule.MobArena.commands.user.*; +import com.garbagemule.MobArena.commands.admin.*; +import com.garbagemule.MobArena.commands.setup.*; +import com.garbagemule.MobArena.framework.ArenaMaster; public class CommandHandler implements CommandExecutor { - /* COMMAND_PATH is an incomplete package-name for all commands in MobArena, - * hence the trailing dot, and the COMMAND_TYPES array contains all the - * different types of commands (the sub-folders). Both are used in the - * loadCommands() method. */ - private static final String COMMAND_PATH = "com.garbagemule.MobArena.commands."; - private static final String[] COMMAND_TYPES = new String[]{"user", "admin", "setup"}; + private MobArena plugin; + private ArenaMaster am; - private MobArenaPlugin plugin; - private Map commands; + private Map commands; - public CommandHandler(MobArenaPlugin plugin) { - this.plugin = plugin; - this.commands = new HashMap(); - loadCommands(); + public CommandHandler(MobArena plugin) { + this.plugin = plugin; + this.am = plugin.getArenaMaster(); + + registerCommands(); } - + @Override - public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { - if (args.length == 0) { - plugin.tell(sender, Msg.MISC_NONE); + public boolean onCommand(CommandSender sender, org.bukkit.command.Command bcmd, String label, String[] args) { + // Grab the base and arguments. + String base = (args.length > 0 ? args[0] : ""); + String last = (args.length > 0 ? args[args.length - 1] : ""); + + // If there's no base argument, show a helpful message. + if (base.equals("")) { + Messenger.tellPlayer(sender, Msg.MISC_HELP); return true; } - // Grab the command from the map, and if null, didn't work. - MACommand command = commands.get(args[0]); - if (command == null) { - System.out.println("Command '" + label + "' not recognized"); + // The help command is a little special + if (base.equals("?") || base.equals("help")) { + showHelp(sender); return true; } - // Cast if sender is a Player, null otherwise. - Player p = (sender instanceof Player) ? (Player) sender : null; + // Get all commands that match the base. + List matches = getMatchingCommands(base); - // If null, must be console. - if (p == null) { - if (!command.executeFromConsole(plugin, sender, args)) { - sender.sendMessage("Can't use that command from the console."); + // If there's more than one match, display them. + if (matches.size() > 1) { + Messenger.tellPlayer(sender, Msg.MISC_MULTIPLE_MATCHES); + for (Command cmd : matches) { + showUsage(cmd, sender); } return true; } - // Otherwise, check permissions - if (!p.hasPermission(command.getPermission())) { - sender.sendMessage("No permission"); + // If there are no matches at all, notify. + if (matches.size() == 0) { + Messenger.tellPlayer(sender, Msg.MISC_NO_MATCHES); return true; } - // Execute! - command.execute(plugin, p, trimFirstArg(args)); + // Grab the only match. + Command command = matches.get(0); + CommandInfo info = command.getClass().getAnnotation(CommandInfo.class); + + // First check if the sender has permission. + if (!plugin.has(sender, info.permission())) { + Messenger.tellPlayer(sender, Msg.MISC_NO_ACCESS); + return true; + } + + // Check if the last argument is a ?, in which case, display usage and description + if (last.equals("?") || last.equals("help")) { + showUsage(command, sender); + return true; + } + + // Otherwise, execute the command! + String[] params = trimFirstArg(args); + command.execute(am, sender, params); return true; } /** - * Since Java doesn't support pointer arithmetic, this method - * attempts to remove the first element of a String[] object - * by creating a new array of the same size -1 and copying all - * the elements over. - * @param args a String[] to trim - * @return a String[] with the same elements except for the very first + * Get all commands that match a given string. + * @param arg the given string + * @return a list of commands whose patterns match the given string */ - private String[] trimFirstArg(String[] args) { - String[] result = new String[args.length - 1]; - for (int i = 0; i < result.length; i++) { - result[i] = args[i+1]; + private List getMatchingCommands(String arg) { + List result = new ArrayList(); + + // Grab the commands that match the argument. + for (Entry entry : commands.entrySet()) { + if (arg.matches(entry.getKey())) { + result.add(entry.getValue()); + } } + return result; } /** - * Load all of the MobArena commands from either the jar or the bin - * folder into the commands-map. Each one of a commands' names/aliases - * is added to the map, such that both "/ma join" and "/ma j" work, for - * instance. + * Show the usage and description messages of a command to a player. + * The usage will only be shown, if the player has permission for the command. + * @param cmd a Command + * @param sender a CommandSender */ - private void loadCommands() { - // Prepare a strategy for the class parser. - ClassIterationStrategy strategy = null; - File f = new File("plugins" + File.separator + "MobArena.jar"); - - try { - // If jar exists, use it - otherwise, use bin. - strategy = (f.exists()) ? new JarClassIterationStrategy(f) : - new DirClassIterationStrategy(new File("bin")); + private void showUsage(Command cmd, CommandSender sender) { + CommandInfo info = cmd.getClass().getAnnotation(CommandInfo.class); + if (!plugin.has(sender, info.permission())) return; - // Create a new ClassParser with the given strategy. - ClassParser parser = new ClassParser(strategy); - - // Iterate through all three folders, user/admin/setup - for (String type : COMMAND_TYPES) { - Collection> classes = parser.getClasses(COMMAND_PATH + type); - - for (Class c : classes) { - // Make sure the class is indeed an instance of MACommand - Object obj = c.newInstance(); - if (!(obj instanceof MACommand)) - continue; - - // Then cast, and add for each command name/alias - MACommand command = (MACommand) obj; - for (String name : command.getNames()) { - commands.put(name, command); - } - } - } - } catch (Exception e) { + Messenger.tellPlayer(sender, info.usage() + " " + ChatColor.YELLOW + info.desc()); + } + + /** + * Remove the first argument of a string. This is because the very first + * element of the arguments array will be the command itself. + * @param args an array of length n + * @return the same array minus the first element, and thus of length n-1 + */ + private String[] trimFirstArg(String[] args) { + return Arrays.copyOfRange(args, 1, args.length); + } + + /** + * List all the available MobArena commands for the CommandSender. + * @param sender a player or the console + */ + private void showHelp(CommandSender sender) { + Messenger.tellPlayer(sender, "Available MobArena commands:"); + + for (Command cmd : commands.values()) { + showUsage(cmd, sender); + } + } + + /** + * Register all the commands directly. + * This could also be done with a somewhat dirty classloader/resource reader + * method, but this is neater, albeit more manual work. + */ + private void registerCommands() { + commands = new HashMap(); + + // mobarena.use + register(ArenaListCommand.class); + register(JoinCommand.class); + register(LeaveCommand.class); + register(NotReadyCommand.class); + register(SpecCommand.class); + register(PlayerListCommand.class); + + // mobarena.admin + register(DisableCommand.class); + register(EnableCommand.class); + register(ForceCommand.class); + register(KickCommand.class); + register(RestoreCommand.class); + + // mobarena.setup + register(AddArenaCommand.class); + register(AddClassPermCommand.class); + register(AddContainerCommand.class); + register(AddSpawnpointCommand.class); + register(ArenaCommand.class); + register(CheckDataCommand.class); + register(ConfigCommand.class); + register(ContainersCommand.class); + register(EditArenaCommand.class); + register(ExpandLobbyRegionCommand.class); + register(ExpandRegionCommand.class); + register(ListClassesCommand.class); + register(ListClassPermsCommand.class); + register(ProtectCommand.class); + register(RemoveArenaCommand.class); + register(RemoveClassCommand.class); + register(RemoveClassPermCommand.class); + register(RemoveContainerCommand.class); + register(RemoveSpawnpointCommand.class); + register(SetArenaCommand.class); + register(SetClassCommand.class); + register(SetLobbyRegionCommand.class); + register(SetRegionCommand.class); + register(SetWarpCommand.class); + register(ShowRegionCommand.class); + register(SpawnpointsCommand.class); + register(AutoGenerateCommand.class); + register(AutoDegenerateCommand.class); + } + + /** + * Register a single command. + * The Command's CommandInfo annotation is queried to find its pattern + * string, which is used to map the commands. + * @param c a Command + */ + private void register(Class c) { + CommandInfo info = c.getAnnotation(CommandInfo.class); + if (info == null) return; + + try { + commands.put(info.pattern(), c.newInstance()); + } + catch (Exception e) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/src/com/garbagemule/MobArena/commands/CommandInfo.java b/src/com/garbagemule/MobArena/commands/CommandInfo.java new file mode 100644 index 0000000..e19a850 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/CommandInfo.java @@ -0,0 +1,33 @@ +package com.garbagemule.MobArena.commands; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandInfo +{ + /** + * The actual name of the command. Not really used anywhere. + */ + public String name(); + + /** + * A regex pattern that allows minor oddities and alternatives to the command name. + */ + public String pattern(); + + /** + * The usage message, i.e. how the command should be used. + */ + public String usage(); + + /** + * A description of what the command does. + */ + public String desc(); + + /** + * The permission required to execute this command. + */ + public String permission(); +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/commands/Commands.java b/src/com/garbagemule/MobArena/commands/Commands.java new file mode 100644 index 0000000..cfbf6a5 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/Commands.java @@ -0,0 +1,31 @@ +package com.garbagemule.MobArena.commands; + +import java.util.List; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.Messenger; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +public class Commands +{ + public static boolean isPlayer(CommandSender sender) { + return (sender instanceof Player); + } + + public static boolean sanityChecks(Player p, ArenaMaster am, Arena arena, String arg1, List arenas) { + if (arenas.size() > 1 && arg1.isEmpty()) + Messenger.tellPlayer(p, Msg.JOIN_ARG_NEEDED); + else if (arena == null) + Messenger.tellPlayer(p, Msg.ARENA_DOES_NOT_EXIST); + else if (am.getArenaWithPlayer(p) != null && !arena.equals(am.getArenaWithPlayer(p))) + Messenger.tellPlayer(p, Msg.JOIN_IN_OTHER_ARENA); + else + return true; + + return false; + } +} diff --git a/src/com/garbagemule/MobArena/commands/MACommand.java b/src/com/garbagemule/MobArena/commands/MACommand.java deleted file mode 100644 index b8b3400..0000000 --- a/src/com/garbagemule/MobArena/commands/MACommand.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.garbagemule.MobArena.commands; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.MobArenaPlugin; - -public interface MACommand -{ - /** - * Get an array of names/aliases for this command. - * Every command must have at least one name/alias to work. - * @return a String[] of names/aliases - */ - public String[] getNames(); - - /** - * Get the permission required to execute this command. - * If there is no permission required, the method returns - * the empty string. - * @return a permission or null - */ - public String getPermission(); - - /** - * Execute the implementing command with the input Player as the executor. - * @param sender The Player executing the command - * @param args A list of arguments - * @return true, if the command was executed successfully, false otherwise - */ - public boolean execute(MobArenaPlugin plugin, Player sender, String... args); - - /** - * Execute the implementing command with the console as the executor. - * @param sender The console executing the command - * @param args A list of arguments - * @return true, if the command was executed successfully, false otherwise - */ - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args); -} diff --git a/src/com/garbagemule/MobArena/commands/admin/DisableCommand.java b/src/com/garbagemule/MobArena/commands/admin/DisableCommand.java new file mode 100644 index 0000000..870a006 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/admin/DisableCommand.java @@ -0,0 +1,55 @@ +package com.garbagemule.MobArena.commands.admin; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "disable", + pattern = "disable|off", + usage = "/ma disable (|all)", + desc = "disable MobArena or individual arenas", + permission = "mobarena.admin.enable" +) +public class DisableCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + if (arg1.equals("all")) { + for (Arena arena : am.getArenas()) { + disable(arena, sender); + } + return true; + } + + if (!arg1.equals("")) { + Arena arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + + disable(arena, sender); + return true; + } + + am.setEnabled(true); + am.saveConfig(); + Messenger.tellPlayer(sender, "MobArena " + ChatColor.RED + "disabled"); + + return true; + } + + private void disable(Arena arena, CommandSender sender) { + arena.setEnabled(false); + arena.getSettings().getParent().save(); + Messenger.tellPlayer(sender, "Arena '" + arena.configName() + "' " + ChatColor.RED + "disabled"); + } +} diff --git a/src/com/garbagemule/MobArena/commands/admin/EnableCommand.java b/src/com/garbagemule/MobArena/commands/admin/EnableCommand.java index 2d83d6c..25246dc 100644 --- a/src/com/garbagemule/MobArena/commands/admin/EnableCommand.java +++ b/src/com/garbagemule/MobArena/commands/admin/EnableCommand.java @@ -1,30 +1,55 @@ package com.garbagemule.MobArena.commands.admin; +import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; import com.garbagemule.MobArena.*; import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; -public class EnableCommand implements MACommand +@CommandInfo( + name = "enable", + pattern = "enable|on", + usage = "/ma enable", + desc = "enable MobArena or individual arenas", + permission = "mobarena.admin.enable" +) +public class EnableCommand implements Command { @Override - public String[] getNames() { - return new String[] { "enable" }; - } + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + if (arg1.equals("all")) { + for (Arena arena : am.getArenas()) { + enable(arena, sender); + } + return true; + } + + if (!arg1.equals("")) { + Arena arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } - @Override - public String getPermission() { - return "mobarena.admin.enable"; + enable(arena, sender); + return true; + } + + am.setEnabled(true); + am.saveConfig(); + Messenger.tellPlayer(sender, "MobArena " + ChatColor.GREEN + "enabled"); + + return true; } - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { - return false; - } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; + private void enable(Arena arena, CommandSender sender) { + arena.setEnabled(true); + arena.getSettings().getParent().save(); + Messenger.tellPlayer(sender, "Arena '" + arena.configName() + "' " + ChatColor.GREEN + "enabled"); } } diff --git a/src/com/garbagemule/MobArena/commands/admin/ForceCommand.java b/src/com/garbagemule/MobArena/commands/admin/ForceCommand.java new file mode 100644 index 0000000..e17a219 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/admin/ForceCommand.java @@ -0,0 +1,90 @@ +package com.garbagemule.MobArena.commands.admin; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "force", + pattern = "force", + usage = "/ma force start|end ()", + desc = "force start or end an arena", + permission = "mobarena.admin.force" +) +public class ForceCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + if (arg1.equals("end")) { + // With no arguments, end all. + if (arg2.equals("")) { + for (Arena arena : am.getArenas()) { + arena.forceEnd(); + } + + Messenger.tellPlayer(sender, Msg.FORCE_END_ENDED); + am.resetArenaMap(); + return true; + } + + // Otherwise, grab the arena in question. + Arena arena = am.getArenaWithName(arg2); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + + if (arena.getAllPlayers().isEmpty()) { + Messenger.tellPlayer(sender, Msg.FORCE_END_EMPTY); + return false; + } + + // And end it! + arena.forceEnd(); + Messenger.tellPlayer(sender, Msg.FORCE_END_ENDED); + + return true; + } + + if (arg1.equals("start")) { + // Require argument. + if (arg2.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma force start "); + return false; + } + + // Grab the arena. + Arena arena = am.getArenaWithName(arg2); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + + if (arena.isRunning()) { + Messenger.tellPlayer(sender, Msg.FORCE_START_RUNNING); + return false; + } + + if (arena.getReadyPlayersInLobby().isEmpty()) { + Messenger.tellPlayer(sender, Msg.FORCE_START_NOT_READY); + return false; + } + + // And start it! + arena.forceStart(); + Messenger.tellPlayer(sender, Msg.FORCE_START_STARTED); + + return true; + } + + Messenger.tellPlayer(sender, "Usage: /ma force start|end ()"); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/admin/KickCommand.java b/src/com/garbagemule/MobArena/commands/admin/KickCommand.java new file mode 100644 index 0000000..14b415d --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/admin/KickCommand.java @@ -0,0 +1,47 @@ +package com.garbagemule.MobArena.commands.admin; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "kick", + pattern = "kick|kcik", + usage = "/ma kick ", + desc = "kick a player from an arena", + permission = "mobarena.admin.kick" +) +public class KickCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma kick "); + return false; + } + + Arena arena = am.getArenaWithPlayer(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "That player is not in an arena."); + return false; + } + + // Grab the Player object. + Player bp = am.getPlugin().getServer().getPlayer(arg1); + + // Force leave. + arena.playerLeave(bp); + + Messenger.tellPlayer(sender, "Player '" + arg1 + "' was kicked from arena '" + arena.configName() + "'."); + Messenger.tellPlayer(bp, "You were kicked by " + sender.getName() + "."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/admin/RestoreCommand.java b/src/com/garbagemule/MobArena/commands/admin/RestoreCommand.java new file mode 100644 index 0000000..def673e --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/admin/RestoreCommand.java @@ -0,0 +1,43 @@ +package com.garbagemule.MobArena.commands.admin; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.inventory.InventoryManager; + +@CommandInfo( + name = "restore", + pattern = "restore", + usage = "/ma restore ", + desc = "restore a player's inventory", + permission = "mobarena.admin.restore" +) +public class RestoreCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma restore "); + return false; + } + + if (am.getArenaWithPlayer(arg1) != null) { + Messenger.tellPlayer(sender, "Player is currently in an arena."); + return false; + } + + if (InventoryManager.restoreFromFile(am.getPlugin(), am.getPlugin().getServer().getPlayer(arg1))) { + Messenger.tellPlayer(sender, "Restored " + arg1 + "'s inventory!"); + } else { + Messenger.tellPlayer(sender, "Failed to restore " + arg1 + "'s inventory."); + } + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AddArenaCommand.java b/src/com/garbagemule/MobArena/commands/setup/AddArenaCommand.java new file mode 100644 index 0000000..0743406 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AddArenaCommand.java @@ -0,0 +1,51 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "addarena", + pattern = "(add|new)arena", + usage = "/ma addarena ", + desc = "add a new arena", + permission = "mobarena.setup.addarena" +) +public class AddArenaCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma addarena "); + return true; + } + + Arena arena = am.getArenaWithName(arg1); + if (arena != null) { + Messenger.tellPlayer(sender, "An arena with that name already exists."); + return true; + } + + arena = am.createArenaNode(arg1, p.getWorld()); + am.setSelectedArena(arena); + + Messenger.tellPlayer(sender, "New arena with name '" + arg1 + "' created!"); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AddClassPermCommand.java b/src/com/garbagemule/MobArena/commands/setup/AddClassPermCommand.java new file mode 100644 index 0000000..f4ecd4e --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AddClassPermCommand.java @@ -0,0 +1,47 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.TextUtils; + +@CommandInfo( + name = "addclassperm", + pattern = "add(class)?perm(.*)", + usage = "/ma addclassperm ", + desc = "add a per-class permission", + permission = "mobarena.setup.classes" +) +public class AddClassPermCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + if (arg1.equals("") || arg2.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma addclassperm "); + return false; + } + + // Grab the arena class + ArenaClass arenaClass = am.getClasses().get(arg1); + if (arenaClass == null) { + Messenger.tellPlayer(sender, "The class '" + TextUtils.camelCase(arg1) + "' does not exist."); + return false; + } + + // Try to add the permission. + if (am.addClassPermission(arg1, arg2)) { + Messenger.tellPlayer(sender, "Added permission '" + arg2 + "' to class '" + TextUtils.camelCase(arg1) + "'."); + return true; + } + + // If it wasn't added, notify. + Messenger.tellPlayer(sender, "Permission '" + arg2 + "' was NOT added to class '" + TextUtils.camelCase(arg1) + "'."); + return false; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AddContainerCommand.java b/src/com/garbagemule/MobArena/commands/setup/AddContainerCommand.java new file mode 100644 index 0000000..dd586fa --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AddContainerCommand.java @@ -0,0 +1,47 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.block.ContainerBlock; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "addcontainer", + pattern = "add(chest|container)", + usage = "/ma addcontainer ", + desc = "add a new container for the selected arena", + permission = "mobarena.setup.containers" +) +public class AddContainerCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + if (!arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) { + Messenger.tellPlayer(sender, "Usage: /ma addcontainer "); + return false; + } + + if (!(p.getTargetBlock(null, 50).getState() instanceof ContainerBlock)) { + Messenger.tellPlayer(sender, "You must look at container."); + return false; + } + + am.getSelectedArena().getRegion().addChest(arg1, p.getTargetBlock(null, 50).getLocation()); + Messenger.tellPlayer(sender, "Container '" + arg1 + "' added for arena \"" + am.getSelectedArena().configName() + "\""); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AddSpawnpointCommand.java b/src/com/garbagemule/MobArena/commands/setup/AddSpawnpointCommand.java new file mode 100644 index 0000000..d771207 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AddSpawnpointCommand.java @@ -0,0 +1,41 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "addspawn", + pattern = "addspawn(point)?", + usage = "/ma addspawn ", + desc = "add a new spawnpoint for the selected arena", + permission = "mobarena.setup.spawnpoints" +) +public class AddSpawnpointCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + if (!arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) { + Messenger.tellPlayer(sender, "Usage: /ma addspawn "); + return true; + } + + am.getSelectedArena().getRegion().addSpawn(arg1, p.getLocation()); + Messenger.tellPlayer(sender, "Spawnpoint " + arg1 + " added for arena \"" + am.getSelectedArena().configName() + "\""); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ArenaCommand.java b/src/com/garbagemule/MobArena/commands/setup/ArenaCommand.java new file mode 100644 index 0000000..c7c5d41 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ArenaCommand.java @@ -0,0 +1,37 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "arena", + pattern = "arena", + usage = "/ma arena", + desc = "list the currently selected arena", + permission = "mobarena.setup.arena" +) +public class ArenaCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + Messenger.tellPlayer(sender, "Currently selected arena: " + ChatColor.GREEN + am.getSelectedArena().configName()); + + StringBuffer buffy = new StringBuffer(); + if (am.getArenas().size() > 1) { + for (Arena arena : am.getArenas()) { + if (!arena.equals(am.getSelectedArena())) { + buffy.append(arena.configName() + " "); + } + } + } + else buffy.append(Msg.MISC_NONE); + + Messenger.tellPlayer(sender, "Other arenas: " + buffy.toString()); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AutoDegenerateCommand.java b/src/com/garbagemule/MobArena/commands/setup/AutoDegenerateCommand.java new file mode 100644 index 0000000..dcb5f68 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AutoDegenerateCommand.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "autodegenerate", + pattern = "auto(\\-)?degenerate", + usage = "/ma autodegenerate ", + desc = "autodegenerate an existing arena", + permission = "mobarena.setup.autodegenerate" +) +public class AutoDegenerateCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the arguments, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma autodegenerate "); + return true; + } + + if (am.getArenas().size() < 2) { + Messenger.tellPlayer(sender, "At least one arena must exist!"); + return true; + } + + // Check if arena exists. + Arena arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return true; + } + + if (!MAUtils.undoItHippieMonster(arg1, am.getPlugin(), true)) { + Messenger.tellPlayer(sender, "Could not degenerate arena."); + return true; + } + + Messenger.tellPlayer(sender, "Arena with name '" + arg1 + "' degenerated."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/AutoGenerateCommand.java b/src/com/garbagemule/MobArena/commands/setup/AutoGenerateCommand.java new file mode 100644 index 0000000..0f4347f --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/AutoGenerateCommand.java @@ -0,0 +1,54 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "autogenerate", + pattern = "auto(\\-)?generate", + usage = "/ma autogenerate ", + desc = "autogenerate a new arena", + permission = "mobarena.setup.autogenerate" +) +public class AutoGenerateCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the arguments, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma autogenerate "); + return true; + } + + // Check if arena already exists. + Arena arena = am.getArenaWithName(arg1); + if (arena != null) { + Messenger.tellPlayer(sender, "An arena with that name already exists."); + return true; + } + + if (!MAUtils.doooooItHippieMonster(p.getLocation(), 13, arg1, am.getPlugin())) { + Messenger.tellPlayer(sender, "Could not auto-generate arena."); + return true; + } + + Messenger.tellPlayer(sender, "Arena with name '" + arg1 + "' generated."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/CheckDataCommand.java b/src/com/garbagemule/MobArena/commands/setup/CheckDataCommand.java new file mode 100644 index 0000000..14bab08 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/CheckDataCommand.java @@ -0,0 +1,33 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "checkdata", + pattern = "checkdata", + usage = "/ma checkdata", + desc = "check if all required points are set up", + permission = "mobarena.setup.checkdata" +) +public class CheckDataCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + Arena arena = arg1.equals("") ? am.getSelectedArena() : am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + + arena.getRegion().checkData(am.getPlugin(), sender); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ConfigCommand.java b/src/com/garbagemule/MobArena/commands/setup/ConfigCommand.java new file mode 100644 index 0000000..a604b35 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ConfigCommand.java @@ -0,0 +1,39 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "config", + pattern = "config|cfg", + usage = "/ma config reload|save", + desc = "reload or save the config-file", + permission = "mobarena.setup.config" +) +public class ConfigCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + if (arg1.equals("reload")) { + am.reloadConfig(); + Messenger.tellPlayer(sender, "Config reloaded."); + return true; + } + + if (arg1.equals("save")) { + am.saveConfig(); + Messenger.tellPlayer(sender, "Config saved."); + return true; + } + + // Requires an argument. + Messenger.tellPlayer(sender, "Usage: /ma config reload|save"); + return false; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ContainersCommand.java b/src/com/garbagemule/MobArena/commands/setup/ContainersCommand.java new file mode 100644 index 0000000..c5f4fba --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ContainersCommand.java @@ -0,0 +1,38 @@ +package com.garbagemule.MobArena.commands.setup; + +import java.util.Set; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "containers", + pattern = "(containers|chests)", + usage = "/ma containers", + desc = "list containers for the selected arena", + permission = "mobarena.setup.containers" +) +public class ContainersCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + StringBuffer buffy = new StringBuffer(); + Set containers = am.getPlugin().getMAConfig().getKeys("arenas." + am.getSelectedArena().configName() + ".coords.containers"); + + if (containers != null) { + for (String c : containers) { + buffy.append(c); + buffy.append(" "); + } + } + else { + buffy.append(Msg.MISC_NONE); + } + + Messenger.tellPlayer(sender, "Containers for arena '" + am.getSelectedArena().configName() + "': " + buffy.toString()); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/EditArenaCommand.java b/src/com/garbagemule/MobArena/commands/setup/EditArenaCommand.java new file mode 100644 index 0000000..3c3c109 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/EditArenaCommand.java @@ -0,0 +1,75 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "editarena", + pattern = "edit(arena)?", + usage = "/ma editarena () (true|false)", + desc = "set edit mode of an arena", + permission = "mobarena.setup.editarena" +) +public class EditArenaCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + Arena arena; + + // No arguments. + if (arg1.equals("")) { + arena = am.getSelectedArena(); + arena.setEditMode(!arena.inEditMode()); + } + + // One argument. + else if (arg2.equals("")) { + // Argument is [true|false] + if (arg1.matches("true|on") || arg1.matches("false|off")) { + arena = am.getSelectedArena(); + arena.setEditMode(arg1.equals("true")); + } + // Argument is + else { + arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "There is no arena with that name."); + Messenger.tellPlayer(sender, "Usage: /ma editarena (true|false)"); + Messenger.tellPlayer(sender, " or /ma editarena (true|false)"); + return true; + } + arena.setEditMode(!arena.inEditMode()); + } + } + + // Two arguments + else { + if (!(arg2.matches("true|on") || arg2.matches("false|off"))) { + Messenger.tellPlayer(sender, "Usage: /ma editarena (true|false)"); + Messenger.tellPlayer(sender, " or /ma editarena (true|false)"); + return true; + } + arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "There is no arena with that name."); + Messenger.tellPlayer(sender, "Usage: /ma editarena (true|false)"); + Messenger.tellPlayer(sender, " or /ma editarena (true|false)"); + return true; + } + arena.setEditMode(arg2.equals("true")); + } + + Messenger.tellPlayer(sender, "Edit mode for arena '" + arena.configName() + "': " + ((arena.inEditMode()) ? ChatColor.GREEN + "true" : ChatColor.RED + "false")); + if (arena.inEditMode()) Messenger.tellPlayer(sender, "Remember to turn it back off after editing!"); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ExpandLobbyRegionCommand.java b/src/com/garbagemule/MobArena/commands/setup/ExpandLobbyRegionCommand.java new file mode 100644 index 0000000..24804dd --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ExpandLobbyRegionCommand.java @@ -0,0 +1,56 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "expandlobbyregion", + pattern = "expandlobby(region)?", + usage = "/ma expandlobbyregion up|down|out", + desc = "expand the arena lobby region", + permission = "mobarena.setup.expandlobbyregion" +) +public class ExpandLobbyRegionCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + if (args.length != 2 || !arg1.matches("(-)?[0-9]+")) { + Messenger.tellPlayer(sender, "Usage: /ma expandlobbyregion up|down|out"); + return false; + } + + if (!am.getSelectedArena().getRegion().isLobbyDefined()) { + Messenger.tellPlayer(sender, "You must first define l1 and l2"); + return true; + } + + if (arg2.equals("up")) { + am.getSelectedArena().getRegion().expandLobbyUp(Integer.parseInt(arg1)); + } + else if (arg2.equals("down")) { + am.getSelectedArena().getRegion().expandLobbyDown(Integer.parseInt(arg1)); + } + else if (arg2.equals("out")) { + am.getSelectedArena().getRegion().expandLobbyOut(Integer.parseInt(arg1)); + } + else { + Messenger.tellPlayer(sender, "Usage: /ma expandlobbyregion up|down|out"); + return true; + } + + // In case of a "negative" region, fix it! + am.getSelectedArena().getRegion().fixRegion(); + + Messenger.tellPlayer(sender, "Lobby region for '" + am.getSelectedArena().configName() + "' expanded " + arg2 + " by " + arg1 + " blocks."); + am.getSelectedArena().getRegion().save(); + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ExpandRegionCommand.java b/src/com/garbagemule/MobArena/commands/setup/ExpandRegionCommand.java new file mode 100644 index 0000000..2a17c6a --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ExpandRegionCommand.java @@ -0,0 +1,56 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "expandregion", + pattern = "expand(region)?", + usage = "/ma expandregion up|down|out", + desc = "expand the arena region", + permission = "mobarena.setup.expandregion" +) +public class ExpandRegionCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + if (args.length != 2 || !arg1.matches("(-)?[0-9]+")) { + Messenger.tellPlayer(sender, "Usage: /ma expandregion up|down|out"); + return false; + } + + if (!am.getSelectedArena().getRegion().isDefined()) { + Messenger.tellPlayer(sender, "You must first define p1 and p2"); + return true; + } + + if (arg2.equals("up")) { + am.getSelectedArena().getRegion().expandUp(Integer.parseInt(arg1)); + } + else if (arg2.equals("down")) { + am.getSelectedArena().getRegion().expandDown(Integer.parseInt(arg1)); + } + else if (arg2.equals("out")) { + am.getSelectedArena().getRegion().expandOut(Integer.parseInt(arg1)); + } + else { + Messenger.tellPlayer(sender, "Usage: /ma expandregion up|down|out"); + return true; + } + + // In case of a "negative" region, fix it! + am.getSelectedArena().getRegion().fixRegion(); + + Messenger.tellPlayer(sender, "Region for '" + am.getSelectedArena().configName() + "' expanded " + arg2 + " by " + arg1 + " blocks."); + am.getSelectedArena().getRegion().save(); + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ListClassPermsCommand.java b/src/com/garbagemule/MobArena/commands/setup/ListClassPermsCommand.java new file mode 100644 index 0000000..466a043 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ListClassPermsCommand.java @@ -0,0 +1,57 @@ +package com.garbagemule.MobArena.commands.setup; + +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.TextUtils; + +@CommandInfo( + name = "listclassperms", + pattern = "(list)?classperm(.*)s", + usage = "/ma listclassperms ", + desc = "list per-class permissions", + permission = "mobarena.setup.classes" +) +public class ListClassPermsCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument. + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma listclassperms "); + return true; + } + + ArenaClass arenaClass = am.getClasses().get(arg1); + String className = TextUtils.camelCase(arg1); + + if (arenaClass == null) { + Messenger.tellPlayer(sender, "The class '" + className + "' does not exist."); + return true; + } + + Messenger.tellPlayer(sender, "Permissions for '" + className + "':"); + Map perms = arenaClass.getPermissions(); + if (perms.isEmpty()) { + Messenger.tellPlayer(sender, ""); + return true; + } + + for (Entry entry : arenaClass.getPermissions().entrySet()) { + String perm = entry.getKey(); + if (!entry.getValue()) { + perm = "^" + perm; + } + Messenger.tellPlayer(sender, "- " + perm); + } + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ListClassesCommand.java b/src/com/garbagemule/MobArena/commands/setup/ListClassesCommand.java new file mode 100644 index 0000000..fc08fc0 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ListClassesCommand.java @@ -0,0 +1,34 @@ +package com.garbagemule.MobArena.commands.setup; + +import java.util.Set; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "listclasses", + pattern = "(list)?class(.)*", + usage = "/ma listclasses", + desc = "list all current classes", + permission = "mobarena.setup.classes" +) +public class ListClassesCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + Messenger.tellPlayer(sender, "Current classes:"); + Set classes = am.getClasses().keySet(); + if (classes == null || classes.isEmpty()) { + Messenger.tellPlayer(sender, ""); + return true; + } + + for (String c : classes) { + Messenger.tellPlayer(sender, "- " + c); + } + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ProtectCommand.java b/src/com/garbagemule/MobArena/commands/setup/ProtectCommand.java new file mode 100644 index 0000000..4770e59 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ProtectCommand.java @@ -0,0 +1,75 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "protect", + pattern = "protect", + usage = "/ma protect () (true|false)", + desc = "set the protection of an arena", + permission = "mobarena.setup.protect" +) +public class ProtectCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + Arena arena; + + // No arguments + if (arg1.equals("")) { + arena = am.getSelectedArena(); + arena.setProtected(!arena.isProtected()); + } + + // One argument + else if (arg2.equals("")) { + // true/false + if (arg1.matches("true|on") || arg1.matches("false|off")) { + arena = am.getSelectedArena(); + arena.setProtected(arg1.matches("true|on")); + } + // Arena name + else { + arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "There is no arena with that name."); + Messenger.tellPlayer(sender, "Usage: /ma protect (true|false)"); + Messenger.tellPlayer(sender, " or /ma protect (true|false)"); + return true; + } + arena.setProtected(!arena.isProtected()); + } + } + + // Two arguments + else { + if (!(arg2.matches("true|on") || arg2.matches("false|off"))) { + Messenger.tellPlayer(sender, "Usage: /ma protect (true|false)"); + Messenger.tellPlayer(sender, " or /ma protect (true|false)"); + return true; + } + arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "There is no arena with that name."); + Messenger.tellPlayer(sender, "Usage: /ma protect (true|false)"); + Messenger.tellPlayer(sender, " or /ma protect (true|false)"); + return true; + } + arena.setProtected(arg2.equals("true")); + } + + arena.getSettings().getParent().save(); + Messenger.tellPlayer(sender, "Protection for arena '" + arena.configName() + "': " + ((arena.isProtected()) ? ChatColor.GREEN + "on" : ChatColor.RED + "off")); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/RemoveArenaCommand.java b/src/com/garbagemule/MobArena/commands/setup/RemoveArenaCommand.java new file mode 100644 index 0000000..532459f --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/RemoveArenaCommand.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "removearena", + pattern = "(del(.)*|r(e)?m(ove)?)arena", + usage = "/ma removearena ", + desc = "remove an arena", + permission = "mobarena.setup.removearena" +) +public class RemoveArenaCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma removearena "); + return false; + } + + if (am.getArenas().size() == 1) { + Messenger.tellPlayer(sender, "At least one arena must exist."); + return false; + } + + Arena arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, "There is no arena with that name."); + return false; + } + + am.removeArenaNode(arena); + + if (am.getSelectedArena().equals(arena)) { + am.setSelectedArena(am.getArenas().get(0)); + } + + Messenger.tellPlayer(sender, "Arena '" + arena.configName() + "' deleted."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/RemoveClassCommand.java b/src/com/garbagemule/MobArena/commands/setup/RemoveClassCommand.java new file mode 100644 index 0000000..0a42a1d --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/RemoveClassCommand.java @@ -0,0 +1,42 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.TextUtils; + +@CommandInfo( + name = "removeclass", + pattern = "(del(.)*|r(e)?m(ove)?)class", + usage = "/ma removeclass ", + desc = "remove the given class", + permission = "mobarena.setup.classes" +) +public class RemoveClassCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument. + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma removeclass "); + return false; + } + + // Find the class + ArenaClass arenaClass = am.getClasses().get(arg1); + String className = TextUtils.camelCase(arg1); + if (arenaClass == null) { + Messenger.tellPlayer(sender, "The class '" + className + "' does not exist."); + return false; + } + + am.removeClassNode(className); + Messenger.tellPlayer(sender, "Removed class '" + className + "'."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/RemoveClassPermCommand.java b/src/com/garbagemule/MobArena/commands/setup/RemoveClassPermCommand.java new file mode 100644 index 0000000..2e046c6 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/RemoveClassPermCommand.java @@ -0,0 +1,47 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.TextUtils; + +@CommandInfo( + name = "removeclassperm", + pattern = "(del(.)*|r(e)?m(ove)?)(class)?perm(.*)", + usage = "/ma removeclassperm ", + desc = "remove a per-class permission", + permission = "mobarena.setup.classes" +) +public class RemoveClassPermCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + if (arg1.equals("") || arg2.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma removeclassperm "); + return false; + } + + // Grab the arena class + ArenaClass arenaClass = am.getClasses().get(arg1); + if (arenaClass == null) { + Messenger.tellPlayer(sender, "The class '" + TextUtils.camelCase(arg1) + "' does not exist."); + return false; + } + + // Remove the permission. + if (am.removeClassPermission(arg1, arg2)) { + Messenger.tellPlayer(sender, "Removed permission '" + arg2 + "' from class '" + TextUtils.camelCase(arg1) + "'."); + return true; + } + + // If it wasn't removed, notify. + Messenger.tellPlayer(sender, "Permission '" + arg2 + "' was NOT removed from class '" + TextUtils.camelCase(arg1) + "'."); + return false; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/RemoveContainerCommand.java b/src/com/garbagemule/MobArena/commands/setup/RemoveContainerCommand.java new file mode 100644 index 0000000..f5508b2 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/RemoveContainerCommand.java @@ -0,0 +1,35 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "removecontainer", + pattern = "(del(.)*|r(e)?m(ove)?)(container|chest)", + usage = "/ma removecontainer ", + desc = "remove a container from the selected arena", + permission = "mobarena.setup.containers" +) +public class RemoveContainerCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (!arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) { + Messenger.tellPlayer(sender, "Usage: /ma removecontainer "); + return false; + } + + if (am.getSelectedArena().getRegion().removeSpawn(arg1)) + Messenger.tellPlayer(sender, "Container " + arg1 + " removed for arena '" + am.getSelectedArena().configName() + "'"); + else + Messenger.tellPlayer(sender, "Could not find the container " + arg1 + "for the arena '" + am.getSelectedArena().configName() + "'"); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/RemoveSpawnpointCommand.java b/src/com/garbagemule/MobArena/commands/setup/RemoveSpawnpointCommand.java new file mode 100644 index 0000000..6ad5261 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/RemoveSpawnpointCommand.java @@ -0,0 +1,35 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "delspawn", + pattern = "(del(.)*|r(e)?m(ove)?)spawn(point)?", + usage = "/ma delspawn ", + desc = "add a new arena", + permission = "mobarena.setup.spawnpoints" +) +public class RemoveSpawnpointCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (!arg1.matches("^[a-zA-Z][a-zA-Z0-9]*$")) { + Messenger.tellPlayer(sender, "Usage: /ma removespawn "); + return true; + } + + if (am.getSelectedArena().getRegion().removeSpawn(arg1)) + Messenger.tellPlayer(sender, "Spawnpoint " + arg1 + " removed for arena '" + am.getSelectedArena().configName() + "'"); + else + Messenger.tellPlayer(sender, "Could not find the spawnpoint " + arg1 + "for the arena '" + am.getSelectedArena().configName() + "'"); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/SetArenaCommand.java b/src/com/garbagemule/MobArena/commands/setup/SetArenaCommand.java new file mode 100644 index 0000000..2f131e3 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/SetArenaCommand.java @@ -0,0 +1,41 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "setarena", + pattern = "(set|select)arena", + usage = "/ma setarena ", + desc = "set an arena as the selected arena", + permission = "mobarena.setup.setarena" +) +public class SetArenaCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Require an argument + if (arg1.equals("")) { + Messenger.tellPlayer(sender, "Usage: /ma setarena "); + return false; + } + + Arena arena = am.getArenaWithName(arg1); + if (arena != null) { + am.setSelectedArena(arena); + Messenger.tellPlayer(sender, "Currently selected arena: " + arena.configName()); + } + else { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + } + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/SetClassCommand.java b/src/com/garbagemule/MobArena/commands/setup/SetClassCommand.java new file mode 100644 index 0000000..fe54050 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/SetClassCommand.java @@ -0,0 +1,65 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.TextUtils; + +@CommandInfo( + name = "setclass", + pattern = "setclass|saveclass", + usage = "/ma setclass (safe) ", + desc = "save your inventory as a class", + permission = "mobarena.setup.classes" +) +public class SetClassCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + String arg2 = (args.length > 1 ? args[1] : ""); + + // Cast the sender. + Player p = (Player) sender; + + // Require an argument. + if (arg1.equals("")) { + Messenger.tellPlayer(p, "Usage: /ma setclass (safe) "); + return true; + } + + // Check if we're overwriting. + boolean safe = arg1.equals("safe"); + + if (safe && arg2.equals("")) { + Messenger.tellPlayer(p, "Usage: /ma setclass (safe) "); + return true; + } + + // If so, use arg2, otherwise, use arg1 + String className = TextUtils.camelCase(safe ? arg2 : arg1); + + // Create the class. + ArenaClass arenaClass = am.createClassNode(className, p.getInventory(), safe); + + // If the class is null, it was not created. + if (arenaClass == null) { + Messenger.tellPlayer(p, "That class already exists!"); + Messenger.tellPlayer(p, "To overwrite, omit the 'safe' parameter."); + return true; + } + + // Otherwise, yay! + Messenger.tellPlayer(p, "Class '" + className + "' set with your current inventory."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/SetLobbyRegionCommand.java b/src/com/garbagemule/MobArena/commands/setup/SetLobbyRegionCommand.java new file mode 100644 index 0000000..a7880be --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/SetLobbyRegionCommand.java @@ -0,0 +1,41 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "setlobbyregion", + pattern = "set(lobbyregion|l)", + usage = "/ma setlobbyregion l1|l2", + desc = "set the lobby region points of an arena", + permission = "mobarena.setup.setlobbyregion" +) +public class SetLobbyRegionCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + if (!(arg1.equals("l1") || arg1.equals("l2"))) { + Messenger.tellPlayer(sender, "Usage: /ma setlobbyregion l1|l2"); + return true; + } + + am.getSelectedArena().getRegion().set(arg1, p.getLocation()); + Messenger.tellPlayer(sender, "Lobby region point " + arg1 + " for arena '" + am.getSelectedArena().configName() + "' set."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/SetRegionCommand.java b/src/com/garbagemule/MobArena/commands/setup/SetRegionCommand.java index 32e77e6..8ab70cb 100644 --- a/src/com/garbagemule/MobArena/commands/setup/SetRegionCommand.java +++ b/src/com/garbagemule/MobArena/commands/setup/SetRegionCommand.java @@ -2,28 +2,40 @@ package com.garbagemule.MobArena.commands.setup; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class SetRegionCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "setregion", + pattern = "set(region|p)", + usage = "/ma setregion p1|p2", + desc = "set the region points of an arena", + permission = "mobarena.setup.setregion" +) +public class SetRegionCommand implements Command { @Override - public String[] getNames() { - return new String[] { "setregion" }; - } - - @Override - public String getPermission() { - return "mobarena.setup.setregion"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { - return false; - } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + if (!(arg1.equals("p1") || arg1.equals("p2"))) { + Messenger.tellPlayer(sender, "Usage: /ma setregion p1|p2"); + return true; + } + + am.getSelectedArena().getRegion().set(arg1, p.getLocation()); + Messenger.tellPlayer(sender, "Region point " + arg1 + " for arena '" + am.getSelectedArena().configName() + "' set."); + return true; } } diff --git a/src/com/garbagemule/MobArena/commands/setup/SetWarpCommand.java b/src/com/garbagemule/MobArena/commands/setup/SetWarpCommand.java new file mode 100644 index 0000000..49933bd --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/SetWarpCommand.java @@ -0,0 +1,42 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "setwarp", + pattern = "set(warp|point)", + usage = "/ma setwarp arena|lobby|spectator", + desc = "set a warp point for an arena", + permission = "mobarena.setup.setwarp" +) +public class SetWarpCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + if (!(arg1.equals("arena") || arg1.equals("lobby") || arg1.equals("spectator"))) { + Messenger.tellPlayer(sender, "Usage: /ma setwarp arena|lobby|spectator"); + return true; + } + + am.getSelectedArena().getRegion().set(arg1, p.getLocation()); + Messenger.tellPlayer(sender, "Warp point " + arg1 + " was set for arena '" + am.getSelectedArena().configName() + "'"); + Messenger.tellPlayer(sender, "Type /ma checkdata to see if you're missing anything..."); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/ShowRegionCommand.java b/src/com/garbagemule/MobArena/commands/setup/ShowRegionCommand.java new file mode 100644 index 0000000..74c2fb0 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/ShowRegionCommand.java @@ -0,0 +1,59 @@ +package com.garbagemule.MobArena.commands.setup; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "showregion", + pattern = "show(region)?", + usage = "/ma showregion ()", + desc = "show an arena region", + permission = "mobarena.setup.showregion" +) +public class ShowRegionCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + Arena arena; + + if (arg1.equals("")) { + arena = am.getArenaAtLocation(p.getLocation()); + if (arena == null) { + arena = am.getSelectedArena(); + } + + if (!arena.getRegion().isDefined()) { + Messenger.tellPlayer(sender, "The region is not defined for the selected arena."); + return false; + } + } + else { + arena = am.getArenaWithName(arg1); + + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + } + + arena.getRegion().showRegion(p); + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/setup/SpawnpointsCommand.java b/src/com/garbagemule/MobArena/commands/setup/SpawnpointsCommand.java new file mode 100644 index 0000000..fc5e5c6 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/setup/SpawnpointsCommand.java @@ -0,0 +1,38 @@ +package com.garbagemule.MobArena.commands.setup; + +import java.util.Set; + +import org.bukkit.command.CommandSender; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "spawnpoints", + pattern = "spawn(point)?s", + usage = "/ma spawnpoints", + desc = "list spawnpoints for the selected arena", + permission = "mobarena.setup.spawnpoints" +) +public class SpawnpointsCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + StringBuffer buffy = new StringBuffer(); + Set spawnpoints = am.getPlugin().getMAConfig().getKeys("arenas." + am.getSelectedArena().configName() + ".coords.spawnpoints"); + + if (spawnpoints != null) { + for (String s : spawnpoints) { + buffy.append(s); + buffy.append(" "); + } + } + else { + buffy.append(Msg.MISC_NONE); + } + + Messenger.tellPlayer(sender, "Spawnpoints for arena '" + am.getSelectedArena().configName() + "': " + buffy.toString()); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/commands/user/ArenaListCommand.java b/src/com/garbagemule/MobArena/commands/user/ArenaListCommand.java index 7d27ad8..d8f98e6 100644 --- a/src/com/garbagemule/MobArena/commands/user/ArenaListCommand.java +++ b/src/com/garbagemule/MobArena/commands/user/ArenaListCommand.java @@ -4,51 +4,34 @@ import java.util.List; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class ArenaListCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "arenalist", + pattern = "arenas|arenal.*|lista.*", + usage = "/ma arenas", + desc = "lists all available arenas", + permission = "mobarena.use.arenalist" +) +public class ArenaListCommand implements Command { @Override - public String[] getNames() { - return new String[] { "arenas" , "list" }; - } - - @Override - public String getPermission() { - return "mobarena.use.arenalist"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { - // Grab the arena master and the args. - ArenaMaster am = plugin.getArenaMaster(); + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + List arenas; - // Get the arenas. - List arenas = am.getEnabledAndPermittedArenas(sender); - - // Turn the list into a string. - String msg = Msg.MISC_LIST_ARENAS.toString(listToString(arenas)); - - plugin.tell(sender, msg); - return true; - } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; - } - - private String listToString(List list) { - String result = ""; - - for (E e : list) { - result += e + ", "; + if (Commands.isPlayer(sender)) { + Player p = (Player) sender; + arenas = am.getPermittedArenas(p); + } else { + arenas = am.getArenas(); } - if (result.equals("")) - return Msg.MISC_NONE.toString(); - - return result.substring(0, result.length() - 2); + String list = MAUtils.listToString(arenas, am.getPlugin()); + Messenger.tellPlayer(sender, Msg.MISC_LIST_ARENAS.toString(list)); + return true; } } diff --git a/src/com/garbagemule/MobArena/commands/user/JoinCommand.java b/src/com/garbagemule/MobArena/commands/user/JoinCommand.java index 7ef7bda..b587464 100644 --- a/src/com/garbagemule/MobArena/commands/user/JoinCommand.java +++ b/src/com/garbagemule/MobArena/commands/user/JoinCommand.java @@ -1,71 +1,71 @@ package com.garbagemule.MobArena.commands.user; -import java.util.*; +import java.util.List; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class JoinCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "join", + pattern = "j|jo.*|j.*n", + usage = "/ma join ()", + desc = "join an arena", + permission = "mobarena.use.join" +) +public class JoinCommand implements Command { @Override - public String[] getNames() { - return new String[] { "join" , "j" }; - } - - @Override - public String getPermission() { - return "mobarena.use.join"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { - // Grab the arena master and get the argument. - ArenaMaster am = plugin.getArenaMaster(); - String arenaName = (args.length == 1) ? args[0] : null; - - // Get all arenas this player is eligible for. - List arenas = am.getEnabledAndPermittedArenas(sender); - - // If no arenas found, just tell the player it's not enabled. - if (arenas.size() == 0) { - plugin.tell(sender, Msg.JOIN_ARENA_NOT_ENABLED); + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); return false; } - // Require an argument in case of multiple arenas. - if (arenaName == null && arenas.size() > 1) { - plugin.tell(sender, Msg.JOIN_ARG_NEEDED); + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + List arenas = am.getEnabledArenas(); + if (!am.isEnabled() || arenas.size() < 1) { + Messenger.tellPlayer(p, Msg.JOIN_NOT_ENABLED); + return true; + } + + // Grab the arena to join + Arena arena = arenas.size() == 1 ? arenas.get(0) : am.getArenaWithName(arg1); + + // Run a couple of basic sanity checks + if (!Commands.sanityChecks(p, am, arena, arg1, arenas)) { + return true; + } + + // Run a bunch of per-arena sanity checks + if (!arena.canJoin(p)) { return false; } - /* At this point in time, if there was no argument, there must be only - * a single arena in the list. If there was an argument, we can safely - * get the arena with that name, because canJoin() will do the rest of - * the sanity checks. */ - Arena a = (arenaName == null) ? arenas.get(0) : am.getArenaWithName(arenaName); + // If player is in a boat/minecart, eject! + if (p.isInsideVehicle()) + p.leaveVehicle(); - // If the arena is null, it doesn't exist. - if (a == null) { - plugin.tell(sender, Msg.ARENA_DOES_NOT_EXIST); + // If player is in a bed, unbed! + if (p.isSleeping()) { + p.kickPlayer("Banned for life... Nah, just don't join from a bed ;)"); return false; } - // Let the arena itself do the rest of the sanity checking. - if (!a.canJoin(sender)) { + // Join the arena! + if (!arena.playerJoin(p, p.getLocation())) { return false; } - // If no problems, let the guy join and notify. - a.playerJoin(sender, sender.getLocation()); - plugin.tell(sender, Msg.JOIN_PLAYER_JOINED); - return true; } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; - } } diff --git a/src/com/garbagemule/MobArena/commands/user/LeaveCommand.java b/src/com/garbagemule/MobArena/commands/user/LeaveCommand.java index 5e015d8..6b4f59a 100644 --- a/src/com/garbagemule/MobArena/commands/user/LeaveCommand.java +++ b/src/com/garbagemule/MobArena/commands/user/LeaveCommand.java @@ -2,28 +2,43 @@ package com.garbagemule.MobArena.commands.user; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class LeaveCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "leave", + pattern = "l|le((.*))?", + usage = "/ma leave", + desc = "leave the arena", + permission = "mobarena.use.leave" +) +public class LeaveCommand implements Command { @Override - public String[] getNames() { - return new String[] { "leave" , "l" }; - } + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Cast the sender. + Player p = (Player) sender; - @Override - public String getPermission() { - return "mobarena.use.leave"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { + Arena arena = am.getArenaWithPlayer(p); + if (arena == null) { + arena = am.getArenaWithSpectator(p); + if (arena == null) { + Messenger.tellPlayer(p, Msg.LEAVE_NOT_PLAYING); + return false; + } + } + + if (arena.playerLeave(p)) { + Messenger.tellPlayer(p, Msg.LEAVE_PLAYER_LEFT); + } return true; } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; - } } diff --git a/src/com/garbagemule/MobArena/commands/user/NotReadyCommand.java b/src/com/garbagemule/MobArena/commands/user/NotReadyCommand.java index f08a272..549d0df 100644 --- a/src/com/garbagemule/MobArena/commands/user/NotReadyCommand.java +++ b/src/com/garbagemule/MobArena/commands/user/NotReadyCommand.java @@ -2,28 +2,52 @@ package com.garbagemule.MobArena.commands.user; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class NotReadyCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "notready", + pattern = "notr.*|ready", + usage = "/ma notready ()", + desc = "lists all available arenas", + permission = "mobarena.use.arenalist" +) +public class NotReadyCommand implements Command { @Override - public String[] getNames() { - return new String[] { "notready" , "check" }; - } - - @Override - public String getPermission() { - return "mobarena.use.notready"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any + String arg1 = (args.length > 0 ? args[0] : ""); + + // The arena to query. + Arena arena = null; + + if (!arg1.equals("")) { + arena = am.getArenaWithName(arg1); + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + } + else if (Commands.isPlayer(sender)) { + Player p = (Player) sender; + arena = am.getArenaWithPlayer(p); + + if (arena == null) { + Messenger.tellPlayer(sender, Msg.LEAVE_NOT_PLAYING); + return false; + } + } + else { + Messenger.tellPlayer(sender, "Usage: /ma notready "); + return false; + } + + String list = MAUtils.listToString(arena.getNonreadyPlayers(), am.getPlugin()); + Messenger.tellPlayer(sender, Msg.MISC_LIST_PLAYERS.toString(list)); return true; } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; - } } diff --git a/src/com/garbagemule/MobArena/commands/user/PlayerListCommand.java b/src/com/garbagemule/MobArena/commands/user/PlayerListCommand.java index ac656c8..c63a618 100644 --- a/src/com/garbagemule/MobArena/commands/user/PlayerListCommand.java +++ b/src/com/garbagemule/MobArena/commands/user/PlayerListCommand.java @@ -1,29 +1,54 @@ package com.garbagemule.MobArena.commands.user; +import java.util.LinkedList; +import java.util.List; + import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import com.garbagemule.MobArena.commands.*; -import com.garbagemule.MobArena.*; -public class PlayerListCommand implements MACommand +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "playerlist", + pattern = "player.*|listp.*", + usage = "/ma players ()", + desc = "lists players in an arena", + permission = "mobarena.use.playerlist" +) +public class PlayerListCommand implements Command { @Override - public String[] getNames() { - return new String[] { "who" , "players" }; - } - - @Override - public String getPermission() { - return "mobarena.use.playerlist"; - } - - @Override - public boolean execute(MobArenaPlugin plugin, Player sender, String... args) { + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + String list = null; + if (!arg1.equals("")) { + Arena arena = am.getArenaWithName(arg1); + + if (arena == null) { + Messenger.tellPlayer(sender, Msg.ARENA_DOES_NOT_EXIST); + return false; + } + + list = MAUtils.listToString(arena.getPlayersInArena(), am.getPlugin()); + } + else { + StringBuffer buffy = new StringBuffer(); + List players = new LinkedList(); + + for (Arena arena : am.getArenas()) { + players.addAll(arena.getPlayersInArena()); + } + + buffy.append(MAUtils.listToString(players, am.getPlugin())); + list = buffy.toString(); + } + + Messenger.tellPlayer(sender, Msg.MISC_LIST_PLAYERS.toString(list)); return true; } - - @Override - public boolean executeFromConsole(MobArenaPlugin plugin, CommandSender sender, String... args) { - return false; - } } diff --git a/src/com/garbagemule/MobArena/commands/user/SpecCommand.java b/src/com/garbagemule/MobArena/commands/user/SpecCommand.java new file mode 100644 index 0000000..1aeae73 --- /dev/null +++ b/src/com/garbagemule/MobArena/commands/user/SpecCommand.java @@ -0,0 +1,70 @@ +package com.garbagemule.MobArena.commands.user; + +import java.util.List; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.*; +import com.garbagemule.MobArena.commands.*; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +@CommandInfo( + name = "spec", + pattern = "s|spec.*", + usage = "/ma spec ()", + desc = "spec an arena", + permission = "mobarena.use.spec" +) +public class SpecCommand implements Command +{ + @Override + public boolean execute(ArenaMaster am, CommandSender sender, String... args) { + if (!Commands.isPlayer(sender)) { + Messenger.tellPlayer(sender, Msg.MISC_NOT_FROM_CONSOLE); + return false; + } + + // Grab the argument, if any. + String arg1 = (args.length > 0 ? args[0] : ""); + + // Cast the sender. + Player p = (Player) sender; + + List arenas = am.getEnabledArenas(); + if (!am.isEnabled() || arenas.size() < 1) { + Messenger.tellPlayer(p, Msg.JOIN_NOT_ENABLED); + return true; + } + + // Grab the arena to join + Arena arena = arenas.size() == 1 ? arenas.get(0) : am.getArenaWithName(arg1); + + // Run a couple of basic sanity checks + if (!Commands.sanityChecks(p, am, arena, arg1, arenas)) { + return false; + } + + // Run a bunch of arena-specific sanity-checks + if (!arena.canSpec(p)) { + return false; + } + + // If player is in a boat/minecart, eject! + if (p.isInsideVehicle()) + p.leaveVehicle(); + + // If player is in a bed, unbed! + if (p.isSleeping()) { + p.kickPlayer("Banned for life... Nah, just don't join from a bed ;)"); + return false; + } + + // Spectate the arena! + arena.playerSpec(p, p.getLocation()); + + Messenger.tellPlayer(p, Msg.SPEC_PLAYER_SPECTATE); + return true; + } +} diff --git a/src/com/garbagemule/MobArena/events/ArenaEndEvent.java b/src/com/garbagemule/MobArena/events/ArenaEndEvent.java new file mode 100644 index 0000000..12f8218 --- /dev/null +++ b/src/com/garbagemule/MobArena/events/ArenaEndEvent.java @@ -0,0 +1,43 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; + +public class ArenaEndEvent extends Event implements Cancellable +{ + private static final long serialVersionUID = -492437066028367597L; + + private static final HandlerList handlers = new HandlerList(); + private Arena arena; + private boolean cancelled; + + public ArenaEndEvent(Arena arena) { + this.arena = arena; + this.cancelled = false; + } + + public Arena getArena() { + return arena; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/events/ArenaPlayerDeathEvent.java b/src/com/garbagemule/MobArena/events/ArenaPlayerDeathEvent.java new file mode 100644 index 0000000..a26ce70 --- /dev/null +++ b/src/com/garbagemule/MobArena/events/ArenaPlayerDeathEvent.java @@ -0,0 +1,37 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; + +public class ArenaPlayerDeathEvent extends Event +{ + private static final long serialVersionUID = 8945689670354700831L; + + private static final HandlerList handlers = new HandlerList(); + private Player player; + private Arena arena; + + public ArenaPlayerDeathEvent(Player player, Arena arena) { + this.player = player; + this.arena = arena; + } + + public Player getPlayer() { + return player; + } + + public Arena getArena() { + return arena; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/events/ArenaPlayerJoinEvent.java b/src/com/garbagemule/MobArena/events/ArenaPlayerJoinEvent.java new file mode 100644 index 0000000..c83c2c2 --- /dev/null +++ b/src/com/garbagemule/MobArena/events/ArenaPlayerJoinEvent.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; + +public class ArenaPlayerJoinEvent extends Event implements Cancellable +{ + private static final long serialVersionUID = -1205905192685201064L; + + private static final HandlerList handlers = new HandlerList(); + private Player player; + private Arena arena; + private boolean cancelled; + + public ArenaPlayerJoinEvent(Player player, Arena arena) { + this.player = player; + this.arena = arena; + this.cancelled = false; + } + + public Player getPlayer() { + return player; + } + + public Arena getArena() { + return arena; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/events/ArenaPlayerLeaveEvent.java b/src/com/garbagemule/MobArena/events/ArenaPlayerLeaveEvent.java new file mode 100644 index 0000000..a27c33f --- /dev/null +++ b/src/com/garbagemule/MobArena/events/ArenaPlayerLeaveEvent.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; + +public class ArenaPlayerLeaveEvent extends Event implements Cancellable +{ + private static final long serialVersionUID = -8942511403871825734L; + + private static final HandlerList handlers = new HandlerList(); + private Player player; + private Arena arena; + private boolean cancelled; + + public ArenaPlayerLeaveEvent(Player player, Arena arena) { + this.player = player; + this.arena = arena; + this.cancelled = false; + } + + public Player getPlayer() { + return player; + } + + public Arena getArena() { + return arena; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/events/ArenaStartEvent.java b/src/com/garbagemule/MobArena/events/ArenaStartEvent.java new file mode 100644 index 0000000..0e5d566 --- /dev/null +++ b/src/com/garbagemule/MobArena/events/ArenaStartEvent.java @@ -0,0 +1,43 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; + +public class ArenaStartEvent extends Event implements Cancellable +{ + private static final long serialVersionUID = 4414065978731456440L; + + private static final HandlerList handlers = new HandlerList(); + private Arena arena; + private boolean cancelled; + + public ArenaStartEvent(Arena arena) { + this.arena = arena; + this.cancelled = false; + } + + public Arena getArena() { + return arena; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/events/NewWaveEvent.java b/src/com/garbagemule/MobArena/events/NewWaveEvent.java new file mode 100644 index 0000000..46b7593 --- /dev/null +++ b/src/com/garbagemule/MobArena/events/NewWaveEvent.java @@ -0,0 +1,55 @@ +package com.garbagemule.MobArena.events; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.Wave; + +public class NewWaveEvent extends Event implements Cancellable +{ + private static final long serialVersionUID = -8743818273565357180L; + + private static final HandlerList handlers = new HandlerList(); + private Arena arena; + private boolean cancelled; + + private Wave wave; + private int waveNo; + + public NewWaveEvent(Arena arena, Wave wave, int waveNo) { + this.wave = wave; + this.waveNo = waveNo; + } + + public Wave getWave() { + return wave; + } + + public int getWaveNumber() { + return waveNo; + } + + public Arena getArena() { + return arena; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/com/garbagemule/MobArena/framework/Arena.java b/src/com/garbagemule/MobArena/framework/Arena.java new file mode 100644 index 0000000..a7de49c --- /dev/null +++ b/src/com/garbagemule/MobArena/framework/Arena.java @@ -0,0 +1,231 @@ +package com.garbagemule.MobArena.framework; + +import java.util.*; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.ArenaClass; +import com.garbagemule.MobArena.ArenaListenerImpl; +import com.garbagemule.MobArena.ArenaPlayer; +import com.garbagemule.MobArena.MASpawnThread; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.MonsterManager; +import com.garbagemule.MobArena.RewardManager; +import com.garbagemule.MobArena.leaderboards.Leaderboard; +import com.garbagemule.MobArena.region.ArenaRegion; +import com.garbagemule.MobArena.repairable.Repairable; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.util.inventory.InventoryManager; +import com.garbagemule.MobArena.waves.Wave; +import com.garbagemule.MobArena.waves.WaveManager; + +public interface Arena +{ + /*///////////////////////////////////////////////////////////////////////// + // + // NEW METHODS IN REFACTORING + // + /////////////////////////////////////////////////////////////////////////*/ + + public ConfigSection getSettings(); + + public World getWorld(); + + public void setWorld(World world); + + public boolean isEnabled(); + + public void setEnabled(boolean value); + + public boolean isProtected(); + + public void setProtected(boolean value); + + public boolean isRunning(); + + public boolean inEditMode(); + + public void setEditMode(boolean value); + + public Material getClassLogo(String classname); + + public List getEntryFee(); + + public Set>> getEveryWaveEntrySet(); + + public List getAfterWaveReward(int wave); + + public Set getPlayersInArena(); + + public Set getPlayersInLobby(); + + public Set getReadyPlayersInLobby(); + + public Set getSpectators(); + + public MASpawnThread getSpawnThread(); + + public WaveManager getWaveManager(); + + public Location getPlayerEntry(Player p); + + public ArenaListenerImpl getEventListener(); + + public void setLeaderboard(Leaderboard leaderboard); + + public ArenaPlayer getArenaPlayer(Player p); + + public Set getBlocks(); + + public void addBlock(Block b); + + public boolean removeBlock(Block b); + + public boolean hasPet(Entity e); + + public void addRepairable(Repairable r); + + public ArenaRegion getRegion(); + + public InventoryManager getInventoryManager(); + + public RewardManager getRewardManager(); + + public MonsterManager getMonsterManager(); + + public void revivePlayer(Player p); + + + + + + + public void scheduleTask(Runnable r, int delay); + + + + + + + + + + + + + public boolean startArena(); + + public boolean endArena(); + + public void forceStart(); + + public void forceEnd(); + + public boolean playerJoin(Player p, Location loc); + + public void playerReady(Player p); + + public boolean playerLeave(Player p); + + public void playerDeath(Player p); + + public void playerRespawn(Player p); + + public Location getRespawnLocation(Player p); + + public void playerSpec(Player p, Location loc); + + public void storePlayerData(Player p, Location loc); + + public void storeContainerContents(); + + public void restoreContainerContents(); + + public void movePlayerToLobby(Player p); + + public void movePlayerToSpec(Player p); + + public void movePlayerToEntry(Player p); + + public void discardPlayer(Player p); + + public void repairBlocks(); + + public void queueRepairable(Repairable r); + + + + /*//////////////////////////////////////////////////////////////////// + // + // Items & Cleanup + // + ////////////////////////////////////////////////////////////////////*/ + + public void assignClass(Player p, String className); + + public void assignRandomClass(Player p); + + public void assignClassPermissions(Player p); + + public void removeClassPermissions(Player p); + + + + /*//////////////////////////////////////////////////////////////////// + // + // Initialization & Checks + // + ////////////////////////////////////////////////////////////////////*/ + + public void restoreRegion(); + + + + /*//////////////////////////////////////////////////////////////////// + // + // Getters & Misc + // + ////////////////////////////////////////////////////////////////////*/ + + public boolean inArena(Player p); + + public boolean inLobby(Player p); + + public boolean inSpec(Player p); + + public boolean isDead(Player p); + + public String configName(); + + public String arenaName(); + + public MobArena getPlugin(); + + public Wave getWave(); + + public Map getClasses(); + + public int getPlayerCount(); + + public List getAllPlayers(); + + public Collection getArenaPlayerSet(); + + public List getNonreadyPlayers(); + + public boolean canAfford(Player p); + + public boolean takeFee(Player p); + + public boolean refund(Player p); + + public boolean canJoin(Player p); + + public boolean canSpec(Player p); +} diff --git a/src/com/garbagemule/MobArena/ArenaListener.java b/src/com/garbagemule/MobArena/framework/ArenaListener.java similarity index 90% rename from src/com/garbagemule/MobArena/ArenaListener.java rename to src/com/garbagemule/MobArena/framework/ArenaListener.java index e55211c..7b75661 100644 --- a/src/com/garbagemule/MobArena/ArenaListener.java +++ b/src/com/garbagemule/MobArena/framework/ArenaListener.java @@ -1,4 +1,4 @@ -package com.garbagemule.MobArena; +package com.garbagemule.MobArena.framework; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockIgniteEvent; @@ -19,7 +19,11 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerTeleportEvent; public interface ArenaListener -{ +{ + public enum TeleportResponse { + ALLOW, REJECT, IDGAF + } + public void onBlockBreak(BlockBreakEvent event); public void onBlockPlace(BlockPlaceEvent event); public void onBlockIgnite(BlockIgniteEvent event); @@ -35,6 +39,6 @@ public interface ArenaListener public void onPlayerInteract(PlayerInteractEvent event); public void onPlayerQuit(PlayerQuitEvent event); public void onPlayerKick(PlayerKickEvent event); - public void onPlayerTeleport(PlayerTeleportEvent event); + public TeleportResponse onPlayerTeleport(PlayerTeleportEvent event); public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event); } diff --git a/src/com/garbagemule/MobArena/framework/ArenaMaster.java b/src/com/garbagemule/MobArena/framework/ArenaMaster.java new file mode 100644 index 0000000..f16ffc7 --- /dev/null +++ b/src/com/garbagemule/MobArena/framework/ArenaMaster.java @@ -0,0 +1,136 @@ +package com.garbagemule.MobArena.framework; + +import java.util.List; +import java.util.Map; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.PlayerInventory; + +import com.garbagemule.MobArena.ArenaClass; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; + +public interface ArenaMaster +{ + /*///////////////////////////////////////////////////////////////////////// + // + // NEW METHODS IN REFACTORING + // + /////////////////////////////////////////////////////////////////////////*/ + + + public MobArena getPlugin(); + + public boolean isEnabled(); + + public void setEnabled(boolean value); + + public boolean notifyOnUpdates(); + + public Arena getSelectedArena(); + + public void setSelectedArena(Arena arena); + + public List getArenas(); + + public Map getClasses(); + + public void addPlayer(Player p, Arena arena); + + public Arena removePlayer(Player p); + + public void resetArenaMap(); + + + + /*///////////////////////////////////////////////////////////////////////// + // + // Getters + // + /////////////////////////////////////////////////////////////////////////*/ + + + public List getEnabledArenas(); + + public List getPermittedArenas(Player p); + + public List getEnabledAndPermittedArenas(Player p); + + public Arena getArenaAtLocation(Location loc); + + public List getArenasInWorld(World world); + + public List getAllPlayers(); + + public List getAllPlayersInArena(String arenaName); + + public List getAllLivingPlayers(); + + public List getLivingPlayersInArena(String arenaName); + + public Arena getArenaWithPlayer(Player p); + + public Arena getArenaWithPlayer(String playerName); + + public Arena getArenaWithSpectator(Player p); + + public Arena getArenaWithMonster(Entity e); + + public Arena getArenaWithPet(Entity e); + + public Arena getArenaWithName(String configName); + + public boolean isAllowed(String command); + + + + /*///////////////////////////////////////////////////////////////////////// + // + // Initialization + // + /////////////////////////////////////////////////////////////////////////*/ + + public void initialize(); + + /** + * Load the global settings. + */ + public void loadSettings(); + + /** + * Load all class-related stuff. + */ + public void loadClasses(); + + public ArenaClass createClassNode(String className, PlayerInventory inv, boolean safe); + + public void removeClassNode(String className); + + public boolean addClassPermission(String className, String perm); + + public boolean removeClassPermission(String className, String perm); + + /** + * Load all arena-related stuff. + */ + public void loadArenas(); + + public Arena createArenaNode(String configName, World world); + + public void removeArenaNode(Arena arena); + + + + /*///////////////////////////////////////////////////////////////////////// + // + // Update and serialization methods + // + /////////////////////////////////////////////////////////////////////////*/ + + public void reloadConfig(); + + public void saveConfig(); +} diff --git a/src/com/garbagemule/MobArena/health/HealthStrategy.java b/src/com/garbagemule/MobArena/health/HealthStrategy.java new file mode 100644 index 0000000..ea5bc12 --- /dev/null +++ b/src/com/garbagemule/MobArena/health/HealthStrategy.java @@ -0,0 +1,13 @@ +package com.garbagemule.MobArena.health; + +import org.bukkit.entity.Player; + +public interface HealthStrategy +{ + /** + * Set the health of a player. + * @param p a player + * @param health amount of health + */ + public void setHealth(Player p, int health); +} diff --git a/src/com/garbagemule/MobArena/health/HealthStrategyHeroes.java b/src/com/garbagemule/MobArena/health/HealthStrategyHeroes.java new file mode 100644 index 0000000..e8a62fb --- /dev/null +++ b/src/com/garbagemule/MobArena/health/HealthStrategyHeroes.java @@ -0,0 +1,21 @@ +package com.garbagemule.MobArena.health; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityRegainHealthEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; + +public class HealthStrategyHeroes implements HealthStrategy +{ + @Override + public void setHealth(Player p, int health) { + int current = p.getHealth(); + int regain = health == 20 ? 20 : health - current; + + try { + EntityRegainHealthEvent event = new EntityRegainHealthEvent(p, regain, RegainReason.CUSTOM); + Bukkit.getPluginManager().callEvent(event); + } + catch (Exception e) {} // Because Bukkit is retarded. + } +} diff --git a/src/com/garbagemule/MobArena/health/HealthStrategyStandard.java b/src/com/garbagemule/MobArena/health/HealthStrategyStandard.java new file mode 100644 index 0000000..7f8aece --- /dev/null +++ b/src/com/garbagemule/MobArena/health/HealthStrategyStandard.java @@ -0,0 +1,11 @@ +package com.garbagemule.MobArena.health; + +import org.bukkit.entity.Player; + +public class HealthStrategyStandard implements HealthStrategy +{ + @Override + public void setHealth(Player p, int health) { + p.setHealth(health); + } +} diff --git a/src/com/garbagemule/MobArena/leaderboards/AbstractLeaderboardColumn.java b/src/com/garbagemule/MobArena/leaderboards/AbstractLeaderboardColumn.java new file mode 100644 index 0000000..fe07abc --- /dev/null +++ b/src/com/garbagemule/MobArena/leaderboards/AbstractLeaderboardColumn.java @@ -0,0 +1,57 @@ +package com.garbagemule.MobArena.leaderboards; + +import java.util.List; + +import org.bukkit.block.Sign; + +import com.garbagemule.MobArena.ArenaPlayerStatistics; + +public abstract class AbstractLeaderboardColumn implements LeaderboardColumn +{ + protected String statname; + private Sign header; + private List signs; + + public AbstractLeaderboardColumn(String statname, Sign header, List signs) { + this.statname = statname; + this.header = header; + this.signs = signs; + } + + public void update(List stats) { + // Make sure the stats will fit on the signs. + int range = Math.min(stats.size(), signs.size()*4); + + for (int i = 0; i < range; i++) { + // Grab the right sign. + Sign s = signs.get(i/4); + + // Call the template method. + String value = getLine(stats.get(i)); + + // And set the line + s.setLine(i % 4, value); + s.update(); + } + } + + public abstract String getLine(ArenaPlayerStatistics stats); + + public void clear() { + for (Sign s : signs) { + s.setLine(0, ""); + s.setLine(1, ""); + s.setLine(2, ""); + s.setLine(3, ""); + s.update(); + } + } + + public Sign getHeader() { + return header; + } + + public List getSigns() { + return signs; + } +} diff --git a/src/com/garbagemule/MobArena/leaderboards/ClassLeaderboardColumn.java b/src/com/garbagemule/MobArena/leaderboards/ClassLeaderboardColumn.java new file mode 100644 index 0000000..83583ee --- /dev/null +++ b/src/com/garbagemule/MobArena/leaderboards/ClassLeaderboardColumn.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.leaderboards; + +import java.util.List; + +import org.bukkit.block.Sign; + +import com.garbagemule.MobArena.ArenaPlayerStatistics; + +public class ClassLeaderboardColumn extends AbstractLeaderboardColumn +{ + public ClassLeaderboardColumn(String statname, Sign header, List signs) { + super(statname, header, signs); + } + + @Override + public String getLine(ArenaPlayerStatistics stats) { + return stats.getClassName(); + } +} diff --git a/src/com/garbagemule/MobArena/leaderboards/IntLeaderboardColumn.java b/src/com/garbagemule/MobArena/leaderboards/IntLeaderboardColumn.java new file mode 100644 index 0000000..0a48d03 --- /dev/null +++ b/src/com/garbagemule/MobArena/leaderboards/IntLeaderboardColumn.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.leaderboards; + +import java.util.List; + +import org.bukkit.block.Sign; + +import com.garbagemule.MobArena.ArenaPlayerStatistics; + +public class IntLeaderboardColumn extends AbstractLeaderboardColumn +{ + public IntLeaderboardColumn(String statname, Sign header, List signs) { + super(statname, header, signs); + } + + @Override + public String getLine(ArenaPlayerStatistics stats) { + return "" + stats.getInt(statname); + } +} diff --git a/src/com/garbagemule/MobArena/leaderboards/Leaderboard.java b/src/com/garbagemule/MobArena/leaderboards/Leaderboard.java index d5eacba..7b286f8 100644 --- a/src/com/garbagemule/MobArena/leaderboards/Leaderboard.java +++ b/src/com/garbagemule/MobArena/leaderboards/Leaderboard.java @@ -4,19 +4,16 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.Sign; -//import org.bukkit.util.config.Configuration; -import com.garbagemule.MobArena.Arena; import com.garbagemule.MobArena.ArenaPlayer; import com.garbagemule.MobArena.ArenaPlayerStatistics; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.Config; +import com.garbagemule.MobArena.framework.Arena; public class Leaderboard { @@ -34,12 +31,12 @@ public class Leaderboard private boolean isValid; /** - * Default constructor. + * Private constructor. * Creates a new leaderboard with no signs or locations or anything. * @param plugin MobArena instance. * @param arena The arena to which this leaderboard belongs. */ - public Leaderboard(MobArena plugin, Arena arena) + private Leaderboard(MobArena plugin, Arena arena) { this.plugin = plugin; this.arena = arena; @@ -47,31 +44,6 @@ public class Leaderboard this.stats = new ArrayList(); } - /** - * Config constructor. - * Used to create a leaderboard from the location persisted in the config-file. - * @param plugin MobArena instance. - * @param arena The arena to which this leaderboard belongs. - * @param config The config-file in which the location is specified. - */ - public Leaderboard(MobArena plugin, Arena arena, Config config) - { - this(plugin, arena); - - // Grab the coords from the config-file. - String coords = config.getString("arenas." + arena.configName() + ".coords.leaderboard", null); - - if (coords != null) - { - // Grab the top left sign. - topLeft = Config.parseLocation(arena.getWorld(), coords); - - // If it is a sign, validate. - if (topLeft.getBlock().getState() instanceof Sign) - isValid = isGridWellFormed(); - } - } - /** * Location constructor. * Used to create a leaderboard on-the-fly from the location from the SignChangeEvent. @@ -83,6 +55,10 @@ public class Leaderboard { this(plugin, arena); + if (topLeft == null) { + return; + } + if (!(topLeft.getBlock().getState() instanceof Sign)) throw new IllegalArgumentException("Block must be a sign!"); @@ -118,7 +94,7 @@ public class Leaderboard public void startTracking() { - trackingId = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, + trackingId = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { public void run() @@ -130,7 +106,7 @@ public class Leaderboard public void stopTracking() { - Bukkit.getServer().getScheduler().cancelTask(trackingId); + plugin.getServer().getScheduler().cancelTask(trackingId); } /** @@ -146,7 +122,7 @@ public class Leaderboard if (!(state instanceof Sign)) { - MobArena.error("Leaderboards for '" + arena.configName() + "' could not be established!"); + plugin.error("Leaderboards for '" + arena.configName() + "' could not be established!"); return false; } @@ -212,8 +188,21 @@ public class Leaderboard } // Create the column. - LeaderboardColumn column = LeaderboardColumn.create(stat.getShortName(), header, signs); - if (column == null) continue; + LeaderboardColumn column = null; + + // Switch on the type of stat + switch (stat) { + case PLAYER_NAME: + column = new PlayerLeaderboardColumn(stat.getShortName(), header, signs); + break; + case CLASS_NAME: + column = new ClassLeaderboardColumn(stat.getShortName(), header, signs); + break; + default: + column = new IntLeaderboardColumn(stat.getShortName(), header, signs); + break; + } + this.boards.add(column); } while ((header = getAdjacentSign(header, direction)) != null); diff --git a/src/com/garbagemule/MobArena/leaderboards/LeaderboardColumn.java b/src/com/garbagemule/MobArena/leaderboards/LeaderboardColumn.java index 756a410..9d0a1e0 100644 --- a/src/com/garbagemule/MobArena/leaderboards/LeaderboardColumn.java +++ b/src/com/garbagemule/MobArena/leaderboards/LeaderboardColumn.java @@ -1,116 +1,43 @@ package com.garbagemule.MobArena.leaderboards; -import java.lang.reflect.Field; import java.util.List; -import org.bukkit.ChatColor; import org.bukkit.block.Sign; import com.garbagemule.MobArena.ArenaPlayerStatistics; -import com.garbagemule.MobArena.util.TextUtils; -public class LeaderboardColumn +public interface LeaderboardColumn { - private Field field; - private boolean isPlayerName; - private Sign header; - private List signs; - - private LeaderboardColumn(String statname, Sign header, List signs) throws Exception - { - this.field = ArenaPlayerStatistics.class.getDeclaredField(statname); - this.isPlayerName = statname.equals("playerName"); - this.header = header; - this.signs = signs; - } + /** + * Update all the signs in this column to the current values + * of the player stat associated with this column. + */ + public void update(List stats); /** - * Safely create a new LeaderboardColumn. - * Avoid the try-catch blocks by creating columns with this method. - * @param statname The name of the stat to track - * @param header The header sign - * @param signs A list of signs - * @return A new LeaderboardColumn, or null + * Get the String representation of the stat in question. + * The line is calculated by simply calling the appropriate + * getter on the ArenaPlayerStatistics object. + * @param stats an ArenaPlayerStatistics object + * @return the String representation of the stat in question */ - public static LeaderboardColumn create(String statname, Sign header, List signs) - { - try - { - return new LeaderboardColumn(statname, header, signs); - } - catch (Exception e) - { - e.printStackTrace(); - return null; - } - } + public String getLine(ArenaPlayerStatistics stats); - public void update(List stats) - { - // Make sure the stats will fit on the signs. - int range = Math.min(stats.size(), signs.size()*4); - - try - { - if (isPlayerName) - { - for (int i = 0; i < range; i++) - { - // Grab the right sign. - Sign s = signs.get(i/4); - - // Get the stat value. - field.setAccessible(true); - ArenaPlayerStatistics aps = stats.get(i); - Object o = field.get(aps); - field.setAccessible(false); - String name = aps.getArenaPlayer().isDead() ? o.toString() : ChatColor.GREEN + o.toString(); - - // Set the value on the right line. - s.setLine(i % 4, TextUtils.truncate(name)); - s.update(); - } - } - else - { - for (int i = 0; i < range; i++) - { - // Grab the right sign. - Sign s = signs.get(i/4); - - // Get the stat value. - field.setAccessible(true); - Object o = field.get(stats.get(i)); - field.setAccessible(false); - - // Set the value on the right line. - s.setLine(i % 4, o.toString()); - s.update(); - } - } - } - catch (Exception e) { e.printStackTrace(); } - } + /** + * Clear the text on all the signs in the column. + */ + public void clear(); - public void clear() - { - for (Sign s : signs) - { - s.setLine(0, ""); - s.setLine(1, ""); - s.setLine(2, ""); - s.setLine(3, ""); - s.update(); - } - } + /** + * Get the top sign of the column. + * The top sign displays the stat name. + * @return the top sign of the column + */ + public Sign getHeader(); - public Sign getHeader() - { - return header; - } - - public List getSigns() - { - return signs; - } + /** + * Get all signs in the column (minus the header). + * @return all signs in the column (minus the header) + */ + public List getSigns(); } diff --git a/src/com/garbagemule/MobArena/leaderboards/PlayerLeaderboardColumn.java b/src/com/garbagemule/MobArena/leaderboards/PlayerLeaderboardColumn.java new file mode 100644 index 0000000..7d7bb59 --- /dev/null +++ b/src/com/garbagemule/MobArena/leaderboards/PlayerLeaderboardColumn.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.leaderboards; + +import java.util.List; + +import org.bukkit.block.Sign; + +import com.garbagemule.MobArena.ArenaPlayerStatistics; + +public class PlayerLeaderboardColumn extends AbstractLeaderboardColumn +{ + public PlayerLeaderboardColumn(String statname, Sign header, List signs) { + super(statname, header, signs); + } + + @Override + public String getLine(ArenaPlayerStatistics stats) { + return stats.getPlayerName(); + } +} diff --git a/src/com/garbagemule/MobArena/leaderboards/Stats.java b/src/com/garbagemule/MobArena/leaderboards/Stats.java index 71c1786..b84f35b 100644 --- a/src/com/garbagemule/MobArena/leaderboards/Stats.java +++ b/src/com/garbagemule/MobArena/leaderboards/Stats.java @@ -1,49 +1,44 @@ package com.garbagemule.MobArena.leaderboards; -import com.garbagemule.MobArena.util.WaveUtils; - public enum Stats { - players("Players", "playerName"), - className("Class", "className"), - kills("Kills", "kills"), - dmgDone("Damage Done", "dmgDone"), - dmgTaken("Damage Taken", "dmgTaken"), - swings("Swings", "swings"), - hits("Hits", "hits"), - lastWave("Last Wave", "lastWave"); + PLAYER_NAME("Players", "playerName"), + CLASS_NAME("Class", "class"), + KILLS("Kills", "kills"), + DAMAGE_DONE("Damage Done", "dmgDone"), + DAMAGE_TAKEN("Damage Taken", "dmgTaken"), + SWINGS("Swings", "swings"), + HITS("Hits", "hits"), + LAST_WAVE("Last Wave", "lastWave"); private String name, shortName; - private Stats(String name, String shortName) - { + private Stats(String name, String shortName) { this.name = name; this.shortName = shortName; } - public String getShortName() - { + public String getShortName() { return shortName; } - public String getFullName() - { + public String getFullName() { return name; } - public static Stats fromString(String name) - { - if (name.equals("class")) - return Stats.className; - return WaveUtils.getEnumFromStringCaseSensitive(Stats.class, name); - } - - public static Stats getByFullName(String name) - { - + public static Stats getByFullName(String name) { for (Stats s : Stats.values()) if (s.name.equals(name)) return s; return null; } + + public static Stats getByShortName(String name) { + for (Stats s : Stats.values()) { + if (s.shortName.equalsIgnoreCase(name)) { + return s; + } + } + return null; + } } diff --git a/src/com/garbagemule/MobArena/listeners/MAGlobalListener.java b/src/com/garbagemule/MobArena/listeners/MAGlobalListener.java new file mode 100644 index 0000000..fd6347c --- /dev/null +++ b/src/com/garbagemule/MobArena/listeners/MAGlobalListener.java @@ -0,0 +1,278 @@ +package com.garbagemule.MobArena.listeners; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; + +import com.garbagemule.MobArena.Messenger; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.framework.ArenaListener.TeleportResponse; +import com.garbagemule.MobArena.leaderboards.Stats; +import com.garbagemule.MobArena.util.VersionChecker; + +/** + * The point of this class is to simply redirect all events to each arena's + * own listener(s). + * This means only one actual listener need be registered in Bukkit, and thus + * less overhead. Of course, this requires a little bit of "hackery" here + * and there. + */ +public class MAGlobalListener implements Listener +{ + private MobArena plugin; + private ArenaMaster am; + + public MAGlobalListener(MobArena plugin, ArenaMaster am) { + this.plugin = plugin; + this.am = am; + } + + + /////////////////////////////////////////////////////////////////////////// + // // + // BLOCK EVENTS // + // // + /////////////////////////////////////////////////////////////////////////// + + + @EventHandler(priority = EventPriority.HIGHEST) + public void blockBreak(BlockBreakEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onBlockBreak(event); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void blockBurn(BlockBurnEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onBlockBurn(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void blockForm(BlockFormEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onBlockForm(event); + } + + @EventHandler(priority = EventPriority.HIGH) + public void blockIgnite(BlockIgniteEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onBlockIgnite(event); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void blockPlace(BlockPlaceEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onBlockPlace(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void signChange(SignChangeEvent event) { + if (!event.getPlayer().hasPermission("mobarena.setup.leaderboards")) { + return; + } + + if (!event.getLine(0).startsWith("[MA]")) { + return; + } + + String text = event.getLine(0).substring((4)); + Arena arena; + Stats stat; + + if ((arena = am.getArenaWithName(text)) != null) { + arena.getEventListener().onSignChange(event); + setSignLines(event, ChatColor.GREEN + "MobArena", ChatColor.YELLOW + arena.arenaName(), ChatColor.AQUA + "Players", "---------------"); + } + else if ((stat = Stats.getByShortName(text)) != null) { + setSignLines(event, ChatColor.GREEN + "", "", ChatColor.AQUA + stat.getFullName(), "---------------"); + Messenger.tellPlayer(event.getPlayer(), "Stat sign created."); + } + } + + private void setSignLines(SignChangeEvent event, String s1, String s2, String s3, String s4) { + event.setLine(0, s1); + event.setLine(1, s2); + event.setLine(2, s3); + event.setLine(3, s4); + } + + + + /////////////////////////////////////////////////////////////////////////// + // // + // ENTITY EVENTS // + // // + /////////////////////////////////////////////////////////////////////////// + + + @EventHandler(priority = EventPriority.HIGHEST) + public void creatureSpawn(CreatureSpawnEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onCreatureSpawn(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityChangeBlock(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void entityCombust(EntityCombustEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityCombust(event); + } + + @EventHandler(priority = EventPriority.LOW) + public void entityDamage(EntityDamageEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityDamage(event); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void entityDeath(EntityDeathEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityDeath(event); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void entityExplode(EntityExplodeEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityExplode(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void entityRegainHealth(EntityRegainHealthEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityRegainHealth(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void entityFoodLevelChange(FoodLevelChangeEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onFoodLevelChange(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void entityTarget(EntityTargetEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onEntityTarget(event); + } + + + + /////////////////////////////////////////////////////////////////////////// + // // + // PLAYER EVENTS // + // // + /////////////////////////////////////////////////////////////////////////// + + + @EventHandler(priority = EventPriority.NORMAL) + public void playerAnimation(PlayerAnimationEvent event) { + if (!am.isEnabled()) return; + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerAnimation(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerBucketEmpty(PlayerBucketEmptyEvent event) { + if (!am.isEnabled()) return; + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerBucketEmpty(event); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void playerCommandPreprocess(PlayerCommandPreprocessEvent event) { + if (!am.isEnabled()) return; + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerCommandPreprocess(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerDropItem(PlayerDropItemEvent event) { + if (!am.isEnabled()) return; + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerDropItem(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerInteract(PlayerInteractEvent event) { + if (!am.isEnabled()) return; + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerInteract(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerJoin(PlayerJoinEvent event) { + if (!am.notifyOnUpdates() || !event.getPlayer().isOp()) return; + + final Player p = event.getPlayer(); + plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, + new Runnable() + { + public void run() + { + if (plugin.getMAConfig().getBoolean("global-settings.update-notification", false) + && !VersionChecker.isLatest(plugin)) { + Messenger.tellPlayer(p, "There is a new version of MobArena available!"); + } + } + }, 60); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerKick(PlayerKickEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerKick(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerQuit(PlayerQuitEvent event) { + for (Arena arena : am.getArenas()) + arena.getEventListener().onPlayerQuit(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerRespawn(PlayerRespawnEvent event) { + for (Arena arena : am.getArenas()) { + if (arena.getEventListener().onPlayerRespawn(event)) { + return; + } + } + + plugin.restoreInventory(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void playerTeleport(PlayerTeleportEvent event) { + if (!am.isEnabled()) return; + + boolean allow = true; + for (Arena arena : am.getArenas()) { + TeleportResponse r = arena.getEventListener().onPlayerTeleport(event); + + // If just one arena allows, uncancel and stop. + switch (r) { + case ALLOW: + event.setCancelled(false); + return; + case REJECT: + allow = false; + break; + } + } + + // Only cancel if at least one arena has rejected the teleport. + if (!allow) { + event.setCancelled(true); + } + } +} diff --git a/src/com/garbagemule/MobArena/listeners/MagicSpellsListener.java b/src/com/garbagemule/MobArena/listeners/MagicSpellsListener.java index 8adfc88..659eb2e 100644 --- a/src/com/garbagemule/MobArena/listeners/MagicSpellsListener.java +++ b/src/com/garbagemule/MobArena/listeners/MagicSpellsListener.java @@ -4,16 +4,18 @@ import java.io.File; import java.util.LinkedList; import java.util.List; -import org.bukkit.util.config.Configuration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.util.FileUtils; -import com.garbagemule.MobArena.waves.Wave.WaveType; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.waves.enums.*; +import com.garbagemule.MobArena.MobArena; import com.nisovin.magicspells.events.SpellCastEvent; -import com.nisovin.magicspells.events.SpellListener; -public class MagicSpellsListener extends SpellListener +public class MagicSpellsListener implements Listener { private MobArena plugin; private List disabled, disabledOnBoss, disabledOnSwarm; @@ -23,40 +25,32 @@ public class MagicSpellsListener extends SpellListener this.plugin = plugin; // Set up the MagicSpells config-file. - File spellFile = FileUtils.extractFile(plugin.getDataFolder(), "magicspells.yml"); - Configuration spellConfig = new Configuration(spellFile); + File spellFile = FileUtils.extractFile(plugin, plugin.getDataFolder(), "magicspells.yml"); + Config spellConfig = new Config(spellFile); spellConfig.load(); setupSpells(spellConfig); } + @EventHandler(priority = EventPriority.NORMAL) public void onSpellCast(SpellCastEvent event) { - Arena arena = plugin.getAM().getArenaWithPlayer(event.getCaster()); + Arena arena = plugin.getArenaMaster().getArenaWithPlayer(event.getCaster()); if (arena == null || !arena.isRunning()) return; String spell = event.getSpell().getName(); WaveType type = (arena.getWave() != null) ? arena.getWave().getType() : null; if (disabled.contains(spell) || - (type == WaveType.BOSS && disabledOnBoss.contains(spell)) || - (type == WaveType.SWARM && disabledOnSwarm.contains(spell))) + (type == WaveType.BOSS && disabledOnBoss.contains(spell)) || + (type == WaveType.SWARM && disabledOnSwarm.contains(spell))) { event.setCancelled(true); + } } - private void setupSpells(Configuration config) + private void setupSpells(Config config) { this.disabled = config.getStringList("disabled-spells", new LinkedList()); - this.disabledOnBoss = config.getStringList("disabled-only-on-bosses", new LinkedList()); - this.disabledOnSwarm = config.getStringList("disabled-only-on-swarms", new LinkedList()); - } - - public void disableSpell(String spell) - { - disabled.add(spell); - } - - public void disableSpellOnBoss(String spell) - { - disabledOnBoss.add(spell); + this.disabledOnBoss = config.getStringList("disabled-on-bosses", new LinkedList()); + this.disabledOnSwarm = config.getStringList("disabled-on-swarms", new LinkedList()); } } diff --git a/src/com/garbagemule/MobArena/listeners/MagicSpellsOldListener.java b/src/com/garbagemule/MobArena/listeners/MagicSpellsOldListener.java deleted file mode 100644 index 1ee7944..0000000 --- a/src/com/garbagemule/MobArena/listeners/MagicSpellsOldListener.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.garbagemule.MobArena.listeners; - -import java.io.File; -import java.util.LinkedList; -import java.util.List; - -import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.FileUtils; -import com.garbagemule.MobArena.waves.Wave.WaveType; -import com.nisovin.MagicSpells.Events.SpellCastEvent; -import com.nisovin.MagicSpells.Events.SpellListener; - -public class MagicSpellsOldListener extends SpellListener -{ - private MobArena plugin; - private List disabled, disabledOnBoss, disabledOnSwarm; - - public MagicSpellsOldListener(MobArena plugin) - { - this.plugin = plugin; - - // Set up the MagicSpells config-file. - File spellFile = FileUtils.extractFile(plugin.getDataFolder(), "magicspells.yml"); - Configuration spellConfig = new Configuration(spellFile); - spellConfig.load(); - setupSpells(spellConfig); - } - - public void onSpellCast(SpellCastEvent event) - { - Arena arena = plugin.getAM().getArenaWithPlayer(event.getCaster()); - if (arena == null || !arena.isRunning()) return; - - String spell = event.getSpell().getName(); - WaveType type = (arena.getWave() != null) ? arena.getWave().getType() : null; - - if (disabled.contains(spell) || - (type == WaveType.BOSS && disabledOnBoss.contains(spell)) || - (type == WaveType.SWARM && disabledOnSwarm.contains(spell))) - event.setCancelled(true); - } - - private void setupSpells(Configuration config) - { - this.disabled = config.getStringList("disabled-spells", new LinkedList()); - this.disabledOnBoss = config.getStringList("disabled-only-on-bosses", new LinkedList()); - this.disabledOnSwarm = config.getStringList("disabled-only-on-swarms", new LinkedList()); - } - - public void disableSpell(String spell) - { - disabled.add(spell); - } - - public void disableSpellOnBoss(String spell) - { - disabledOnBoss.add(spell); - } -} diff --git a/src/com/garbagemule/MobArena/listeners/MASpoutScreenListener.java b/src/com/garbagemule/MobArena/listeners/SpoutScreenListener.java similarity index 64% rename from src/com/garbagemule/MobArena/listeners/MASpoutScreenListener.java rename to src/com/garbagemule/MobArena/listeners/SpoutScreenListener.java index 84d1818..458d596 100644 --- a/src/com/garbagemule/MobArena/listeners/MASpoutScreenListener.java +++ b/src/com/garbagemule/MobArena/listeners/SpoutScreenListener.java @@ -1,30 +1,33 @@ package com.garbagemule.MobArena.listeners; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; import org.getspout.spoutapi.event.screen.ButtonClickEvent; -import org.getspout.spoutapi.event.screen.ScreenListener; import org.getspout.spoutapi.gui.Button; -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.spout.ClassButton; +import com.garbagemule.MobArena.MobArena; -public class MASpoutScreenListener extends ScreenListener +public class SpoutScreenListener implements Listener { private MobArena plugin; - public MASpoutScreenListener(MobArena plugin) + public SpoutScreenListener(MobArena plugin) { this.plugin = plugin; } + @EventHandler(priority = EventPriority.NORMAL) public void onButtonClick(ButtonClickEvent event) { Button b = event.getButton(); if (!(b instanceof ClassButton)) return; Player p = event.getPlayer(); - Arena arena = plugin.getAM().getArenaWithPlayer(p); + Arena arena = plugin.getArenaMaster().getArenaWithPlayer(p); if (arena == null) return; arena.assignClass(p, b.getText()); diff --git a/src/com/garbagemule/MobArena/region/ArenaRegion.java b/src/com/garbagemule/MobArena/region/ArenaRegion.java new file mode 100644 index 0000000..d437976 --- /dev/null +++ b/src/com/garbagemule/MobArena/region/ArenaRegion.java @@ -0,0 +1,517 @@ +package com.garbagemule.MobArena.region; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.Messenger; +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.util.config.ConfigSection; + +public class ArenaRegion +{ + private Arena arena; + private World world; + + private Location p1, p2, l1, l2, arenaWarp, lobbyWarp, specWarp, leaderboard; + private Map spawnpoints, containers; + + private boolean setup, lobbySetup; + + private ConfigSection coords; + private ConfigSection spawns; + private ConfigSection chests; + + public ArenaRegion(ConfigSection coords, Arena arena) { + this.arena = arena; + this.world = arena.getWorld(); + + this.coords = coords; + this.spawns = coords.getConfigSection("spawnpoints"); + this.chests = coords.getConfigSection("containers"); + + reloadAll(); + adjustRegion(); + } + + public void reloadAll() { + reloadRegion(); + reloadWarps(); + reloadLeaderboards(); + reloadSpawnpoints(); + reloadChests(); + + verifyData(); + } + + public void reloadRegion() { + p1 = coords.getLocation("p1", world); + p2 = coords.getLocation("p2", world); + + l1 = coords.getLocation("l1", world); + l2 = coords.getLocation("l2", world); + } + + public void reloadWarps() { + arenaWarp = coords.getLocation("arena", world); + lobbyWarp = coords.getLocation("lobby", world); + specWarp = coords.getLocation("spectator", world); + + leaderboard = coords.getLocation("leaderboard", world); + } + + public void reloadLeaderboards() { + leaderboard = coords.getLocation("leaderboard", world); + } + + public void reloadSpawnpoints() { + spawnpoints = new HashMap(); + Set keys = spawns.getKeys(); + if (keys != null) { + for (String spwn : keys) { + spawnpoints.put(spwn, spawns.getLocation(spwn, world)); + } + } + } + + public void reloadChests() { + containers = new HashMap(); + Set keys = chests.getKeys(); + if (keys != null) { + for (String chst : keys) { + containers.put(chst, chests.getLocation(chst, world)); + } + } + } + + public void verifyData() { + setup = (p1 != null && + p2 != null && + arenaWarp != null && + lobbyWarp != null && + specWarp != null && + !spawnpoints.isEmpty()); + + lobbySetup = (l1 != null && + l2 != null); + } + + public void checkData(MobArena plugin, CommandSender s) { + if (arenaWarp == null) + Messenger.tellPlayer(s, "Missing warp: arena"); + if (lobbyWarp == null) + Messenger.tellPlayer(s, "Missing warp: lobby"); + if (specWarp == null) + Messenger.tellPlayer(s, "Missing warp: spectator"); + if (p1 == null) + Messenger.tellPlayer(s, "Missing region point: p1"); + if (p2 == null) + Messenger.tellPlayer(s, "Missing region point: p2"); + if (spawnpoints.isEmpty()) + Messenger.tellPlayer(s, "Missing spawnpoints"); + if (setup) + Messenger.tellPlayer(s, "Arena is ready to be used!"); + } + + public boolean isDefined() { + return (p1 != null && p2 != null); + } + + public boolean isLobbyDefined() { + return (l1 != null && l2 != null); + } + + public boolean isSetup() { + return setup; + } + + public boolean isLobbySetup() { + return lobbySetup; + } + + public boolean isWarp(Location l) { + return (l.equals(arenaWarp) || + l.equals(lobbyWarp) || + l.equals(specWarp)); + } + + public boolean contains(Location l) { + if (!l.getWorld().getName().equals(world.getName()) || !setup) { + return false; + } + + int x = l.getBlockX(); + int y = l.getBlockY(); + int z = l.getBlockZ(); + + // Check the lobby first. + if (lobbySetup) { + if ((x >= l1.getBlockX() && x <= l2.getBlockX()) && + (z >= l1.getBlockZ() && z <= l2.getBlockZ()) && + (y >= l1.getBlockY() && y <= l2.getBlockY())) + return true; + } + + // Returns false if the location is outside of the region. + return ((x >= p1.getBlockX() && x <= p2.getBlockX()) && + (z >= p1.getBlockZ() && z <= p2.getBlockZ()) && + (y >= p1.getBlockY() && y <= p2.getBlockY())); + } + + public boolean contains(Location l, int radius) { + if (!l.getWorld().getName().equals(world.getName()) || !setup) { + return false; + } + + int x = l.getBlockX(); + int y = l.getBlockY(); + int z = l.getBlockZ(); + + if (lobbySetup) { + if ((x + radius >= l1.getBlockX() && x - radius <= l2.getBlockX()) && + (z + radius >= l1.getBlockZ() && z - radius <= l2.getBlockZ()) && + (y + radius >= l1.getBlockY() && y - radius <= l2.getBlockY())) + return true; + } + + return ((x + radius >= p1.getBlockX() && x - radius <= p2.getBlockX()) && + (z + radius >= p1.getBlockZ() && z - radius <= p2.getBlockZ()) && + (y + radius >= p1.getBlockY() && y - radius <= p2.getBlockY())); + } + + // Region expand + public void expandUp(int amount) { + p2.setY(Math.min(127D, p2.getY() + amount)); + set(RegionPoint.P2, p2); + } + + public void expandDown(int amount) { + p1.setY(Math.max(0D, p1.getY() - amount)); + set(RegionPoint.P1, p1); + } + + public void expandP1(int x, int z) { + p1.setX(p1.getX() - x); + p1.setZ(p1.getZ() - z); + set(RegionPoint.P1, p1); + } + + public void expandP2(int x, int z) { + p2.setX(p2.getX() + x); + p2.setZ(p2.getZ() + z); + set(RegionPoint.P2, p2); + } + + public void expandOut(int amount) { + expandP1(amount, amount); + expandP2(amount, amount); + } + + // Lobby expand + public void expandLobbyUp(int amount) { + l2.setY(Math.min(127D, l2.getY() + amount)); + set(RegionPoint.L2, l2); + } + + public void expandLobbyDown(int amount) { + l1.setY(Math.max(0D, l1.getY() + amount)); + set(RegionPoint.L1, l1); + } + + public void expandL1(int x, int z) { + l1.setX(l1.getX() - x); + l1.setZ(l1.getZ() - z); + set(RegionPoint.L1, l1); + } + + public void expandL2(int x, int z) { + l2.setX(l2.getX() + x); + l2.setZ(l2.getZ() + z); + set(RegionPoint.L2, l2); + } + + public void expandLobbyOut(int amount) { + expandL1(amount, amount); + expandL2(amount, amount); + } + + public void fixRegion() { + fix("p1", "p2"); + } + + public void fixLobbyRegion() { + fix("l1", "l2"); + } + + private void fix(String location1, String location2) { + Location loc1 = coords.getLocation(location1, world); + Location loc2 = coords.getLocation(location2, world); + + if (loc1 == null || loc2 == null) { + return; + } + + if (loc1.getX() > loc2.getX()) { + double tmp = loc1.getX(); + loc1.setX(loc2.getX()); + loc2.setX(tmp); + } + + if (loc1.getZ() > loc2.getZ()) { + double tmp = loc1.getZ(); + loc1.setZ(loc2.getZ()); + loc2.setZ(tmp); + } + + if (loc1.getY() > loc2.getY()) { + double tmp = loc1.getY(); + loc1.setY(loc2.getY()); + loc2.setY(tmp); + } + + if (!arena.getWorld().getName().equals(world.getName())) + arena.setWorld(world); + + coords.set(location1, loc1); + coords.set(location2, loc2); + } + + private void adjustRegion() { + if (!setup) { + return; + } + + // Make sure the arena warp is inside the region. + readjustRegion(arenaWarp); + + // Re-adjust for all spawnpoints and containers. + for (Location spawnpoint : spawnpoints.values()) { + readjustRegion(spawnpoint); + } + + for (Location chest : containers.values()) { + readjustRegion(chest); + } + } + + private void readjustRegion(Location l) { + if (p1 == null || p2 == null) { + return; + } + + int x = l.getBlockX(); + int y = l.getBlockY(); + int z = l.getBlockZ(); + + int p1x = p1.getBlockX(); + int p1y = p1.getBlockY(); + int p1z = p1.getBlockZ(); + + int p2x = p2.getBlockX(); + int p2y = p2.getBlockY(); + int p2z = p2.getBlockZ(); + + if (x <= p1x) { + expandP1(p1x - x + 2, 0); + } + else if (x >= p2x) { + expandP2(x - p2x + 2, 0); + } + + if (y <= p1y) { + expandDown((int) (p1y - y + 2)); + } + else if (y >= p2y) { + expandUp((int) (y - p2y + 2)); + } + + if (z <= p1z) { + expandP1(0, p1z - z + 2); + } + else if (z >= p2z) { + expandP2(0, z - p2z + 2); + } + } + + public List getChunks() { + List result = new ArrayList(); + + if (p1 == null || p2 == null) { + return result; + } + + Chunk c1 = world.getChunkAt(p1); + Chunk c2 = world.getChunkAt(p2); + + for (int i = c1.getX(); i <= c2.getX(); i++) { + for (int j = c1.getZ(); j <= c2.getZ(); j++) { + result.add(world.getChunkAt(i,j)); + } + } + + return result; + } + + public Location getArenaWarp() { + return arenaWarp; + } + + public Location getLobbyWarp() { + return lobbyWarp; + } + + public Location getSpecWarp() { + return specWarp; + } + + public Location getSpawnpoint(String name) { + return spawnpoints.get(name); + } + + public Collection getSpawnpoints() { + return spawnpoints.values(); + } + + public List getSpawnpointList() { + return new ArrayList(spawnpoints.values()); + } + + public Collection getContainers() { + return containers.values(); + } + + public Location getLeaderboard() { + return leaderboard; + } + + public void set(String point, Location loc) { + coords.set(point, loc); + + // Adjust the region to accomodate any bounding box breaking. + if (point.equals("arena") || point.equals("lobby") || point.equals("spectator")) { + readjustRegion(loc); + } + + fixRegion(); + fixLobbyRegion(); + reloadRegion(); + reloadWarps(); + reloadLeaderboards(); + verifyData(); + save(); + } + + public void addSpawn(String name, Location loc) { + spawns.set(name, loc); + readjustRegion(loc); + reloadSpawnpoints(); + verifyData(); + save(); + } + + public boolean removeSpawn(String name) { + if (spawns.getString(name) == null) { + return false; + } + + spawns.set(name, null); + reloadSpawnpoints(); + verifyData(); + save(); + return true; + } + + public void addChest(String name, Location loc) { + chests.set(name, loc); + reloadChests(); + save(); + } + + public boolean removeChest(String name) { + if (chests.getString(name) == null) { + return false; + } + + chests.set(name, null); + reloadChests(); + save(); + return true; + } + + public void save() { + spawns.getParent().save(); + } + + public void showRegion(final Player p) { + if (!isDefined()) { + return; + } + + // Grab all the blocks, and send block change events. + final Map blocks = new HashMap(); + for (Location l : getFramePoints()) { + Block b = l.getBlock(); + blocks.put(l, b.getState()); + p.sendBlockChange(l, 35, (byte) 14); + } + + arena.scheduleTask(new Runnable() { + public void run() { + // If the player isn't online, just forget it. + if (!p.isOnline()) { + return; + } + + // Send block "restore" events. + for (Map.Entry entry : blocks.entrySet()) { + Location l = entry.getKey(); + BlockState b = entry.getValue(); + int id = b.getTypeId(); + byte data = b.getRawData(); + + p.sendBlockChange(l, id, data); + } + } + }, 100); + } + + private List getFramePoints() { + List result = new ArrayList(); + int x1 = p1.getBlockX(); int y1 = p1.getBlockY(); int z1 = p1.getBlockZ(); + int x2 = p2.getBlockX(); int y2 = p2.getBlockY(); int z2 = p2.getBlockZ(); + + for (int i = x1; i <= x2; i++) { + result.add(world.getBlockAt(i, y1, z1).getLocation()); + result.add(world.getBlockAt(i, y1, z2).getLocation()); + result.add(world.getBlockAt(i, y2, z1).getLocation()); + result.add(world.getBlockAt(i, y2, z2).getLocation()); + } + + for (int j = y1; j <= y2; j++) { + result.add(world.getBlockAt(x1, j, z1).getLocation()); + result.add(world.getBlockAt(x1, j, z2).getLocation()); + result.add(world.getBlockAt(x2, j, z1).getLocation()); + result.add(world.getBlockAt(x2, j, z2).getLocation()); + } + + for (int k = z1; k <= z2; k++) { + result.add(world.getBlockAt(x1, y1, k).getLocation()); + result.add(world.getBlockAt(x1, y2, k).getLocation()); + result.add(world.getBlockAt(x2, y1, k).getLocation()); + result.add(world.getBlockAt(x2, y2, k).getLocation()); + } + return result; + } +} diff --git a/src/com/garbagemule/MobArena/region/RegionPoint.java b/src/com/garbagemule/MobArena/region/RegionPoint.java new file mode 100644 index 0000000..f1bdb03 --- /dev/null +++ b/src/com/garbagemule/MobArena/region/RegionPoint.java @@ -0,0 +1,13 @@ +package com.garbagemule.MobArena.region; + +public class RegionPoint +{ + public static final String P1 = "p1"; + public static final String P2 = "p2"; + public static final String L1 = "l1"; + public static final String L2 = "l2"; + public static final String ARENA = "arena"; + public static final String LOBBY = "lobby"; + public static final String SPEC = "spectator"; + public static final String LEADERBOARD = "leaderboard"; +} diff --git a/src/com/garbagemule/MobArena/region/RegionSerializer.java b/src/com/garbagemule/MobArena/region/RegionSerializer.java new file mode 100644 index 0000000..3ad5473 --- /dev/null +++ b/src/com/garbagemule/MobArena/region/RegionSerializer.java @@ -0,0 +1,143 @@ +package com.garbagemule.MobArena.region; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; + +import com.garbagemule.MobArena.framework.Arena; + +public class RegionSerializer +{ + //private UUID worldUID; + private int x1, y1, z1, x2, y2, z2; + private long width, height, length; + + private int[][][] blocks; + private byte[][][] data; + + //private transient World world; + + public RegionSerializer(World world, Location p1, Location p2) { + //this.worldUID = world.getUID(); + + this.x1 = p1.getBlockX(); + this.y1 = p1.getBlockY(); + this.z1 = p1.getBlockZ(); + + this.x2 = p2.getBlockX(); + this.y2 = p2.getBlockY(); + this.z2 = p2.getBlockZ(); + + this.width = (x2 - x1) + 1; + this.height = (y2 - y1) + 1; + this.length = (z2 - z1) + 1; + + int w = (int) width; + int h = (int) height; + int l = (int) length; + + this.blocks = new int[w][h][l]; + this.data = new byte[w][h][l]; + } + + public void serialize(Arena arena) { + Serializer s = new Serializer(arena); + s.start(); + } + + public void deserialize(Arena arena) { + Deserializer d = new Deserializer(arena); + d.start(); + } + + private class Serializer implements Runnable + { + private Arena arena; + private long total; + + public Serializer(Arena arena) { + this.arena = arena; + } + + public void start() { + // Disable the arena while serializing. + arena.setEnabled(false); + + // Start serializing! + total = 0; + arena.scheduleTask(this, 1); + } + + @Override + public void run() { + int y = (int) (total / (width*length)); + int z = (int) ((total % (width*length)) / width); + int x = (int) ((total % (width*length)) % width); + + long max = width*height*length; + int amount = (int) Math.min(20, (max - total)); + + for (int i = 0; i < amount; i++) { + Block b = arena.getWorld().getBlockAt(x,y,z); + blocks[x][y][z] = b.getTypeId(); + data[x][y][z] = b.getData(); + + x = (int) ((x+1) % width); + y = (int) ((y+1) % height); + z = (int) ((z+1) % length); + } + + total += amount; + + if (total == max) { + arena.setEnabled(true); + return; + } + + arena.scheduleTask(this, 1); + } + } + + private class Deserializer implements Runnable + { + private Arena arena; + private long total; + + public Deserializer(Arena arena) { + this.arena = arena; + } + + public void start() { + // Disable the arena while serializing. + arena.setEnabled(false); + + // Start serializing! + total = 0; + arena.scheduleTask(this, 1); + } + + @Override + public void run() { + int y = (int) (total / (width*length)); + int z = (int) ((total % (width*length)) / width); + int x = (int) ((total % (width*length)) % width); + + long max = width*height*length; + int amount = (int) Math.min(20, (max - total)); + + for (int i = 0; i < 20; i++) { + Block b = arena.getWorld().getBlockAt(x,y,z); + b.setTypeIdAndData(blocks[x][y][z], data[x][y][z], false); + } + + total += amount; + + if (total == max) { + arena.setEnabled(true); + return; + } + + arena.scheduleTask(this, 1); + } + } +} diff --git a/src/com/garbagemule/MobArena/repairable/RepairableContainer.java b/src/com/garbagemule/MobArena/repairable/RepairableContainer.java index f4b8245..94c5932 100644 --- a/src/com/garbagemule/MobArena/repairable/RepairableContainer.java +++ b/src/com/garbagemule/MobArena/repairable/RepairableContainer.java @@ -3,54 +3,40 @@ package com.garbagemule.MobArena.repairable; import org.bukkit.block.BlockState; import org.bukkit.block.ContainerBlock; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import com.garbagemule.MobArena.util.InventoryItem; +import com.garbagemule.MobArena.util.inventory.SerializableInventory; public class RepairableContainer extends RepairableBlock { - private InventoryItem[] items; + private SerializableInventory inv; - public RepairableContainer(BlockState state, boolean clear) - { + public RepairableContainer(BlockState state, boolean clear) { super(state); - // Grab the inventory and its contents + // Grab the inventory of the block Inventory inv = ((ContainerBlock) state).getInventory(); - ItemStack[] contents = inv.getContents(); - // Initialize the items array - items = new InventoryItem[contents.length]; - - // Turn every ItemStack into an InventoryItem - for (int i = 0; i < items.length; i++) - items[i] = InventoryItem.parseItemStack(contents[i]); + // Make a SerializableInventory + this.inv = new SerializableInventory(inv); // Clear the inventory if prompted if (clear) inv.clear(); } - public RepairableContainer(BlockState state) - { + public RepairableContainer(BlockState state) { this(state, true); } /** * Repairs the container block by adding all the contents back in. */ - public void repair() - { + public void repair() { super.repair(); // Grab the inventory ContainerBlock cb = (ContainerBlock) getWorld().getBlockAt(getX(),getY(),getZ()).getState(); - Inventory inv = cb.getInventory(); + Inventory chestInv = cb.getInventory(); - // Turn every InventoryItem into an ItemStack - for (int i = 0; i < items.length; i++) - { - InventoryItem item = items[i]; - inv.setItem(i, item != null ? item.toItemStack() : null); - } + SerializableInventory.loadContents(chestInv, this.inv); } } diff --git a/src/com/garbagemule/MobArena/spout/ClassPopup.java b/src/com/garbagemule/MobArena/spout/ClassPopup.java index 08b8d55..6397ece 100644 --- a/src/com/garbagemule/MobArena/spout/ClassPopup.java +++ b/src/com/garbagemule/MobArena/spout/ClassPopup.java @@ -23,6 +23,10 @@ public class ClassPopup extends GenericPopup int buttonsX = (screenWidth - 100) / 2; int buttonsY = (screenHeight - buttonsHeight) / 2 + 20; + // Apparently Spout complains if these aren't set + arenaTitle.setWidth(30); + arenaTitle.setHeight(10); + arenaTitle.setText("Choose your class!"); arenaTitle.setAlign(WidgetAnchor.CENTER_CENTER); arenaTitle.setX((screenWidth - arenaTitle.getWidth()) / 2).setY(buttonsY - 20); diff --git a/src/com/garbagemule/MobArena/spout/Spouty.java b/src/com/garbagemule/MobArena/spout/Spouty.java index 1f5c1f7..7d12bc2 100644 --- a/src/com/garbagemule/MobArena/spout/Spouty.java +++ b/src/com/garbagemule/MobArena/spout/Spouty.java @@ -3,33 +3,22 @@ package com.garbagemule.MobArena.spout; import java.util.LinkedList; import java.util.List; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.Event.Priority; -import org.bukkit.plugin.PluginManager; import org.getspout.spoutapi.SpoutManager; import org.getspout.spoutapi.player.SpoutPlayer; -import com.garbagemule.MobArena.Arena; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.listeners.MASpoutScreenListener; +import com.garbagemule.MobArena.framework.Arena; public class Spouty { - public static void registerEvents(MobArena plugin) - { - PluginManager pm = Bukkit.getServer().getPluginManager(); - pm.registerEvent(Event.Type.CUSTOM_EVENT, new MASpoutScreenListener(plugin), Priority.Normal, plugin); - } - public static void classSelectionScreen(MobArena plugin, Arena arena, Player p) { SpoutPlayer sp = SpoutManager.getPlayer(p); if (sp == null) return; List buttons = new LinkedList(); - for (String s : arena.getClasses()) + for (String s : arena.getClasses().keySet()) buttons.add(new ClassButton(s)); ClassPopup popup = new ClassPopup(plugin, sp, buttons); diff --git a/src/com/garbagemule/MobArena/time/Time.java b/src/com/garbagemule/MobArena/time/Time.java new file mode 100644 index 0000000..06f653b --- /dev/null +++ b/src/com/garbagemule/MobArena/time/Time.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.time; + +public enum Time +{ + DAWN(23000), + SUNRISE(23500), + MORNING(23900), + MIDDAY(6000), + NOON(6000), + DAY(8000), + AFTERNOON(11000), + EVENING(12000), + SUNSET(12600), + DUSK(13300), + NIGHT(14000), + MIDNIGHT(18000); + + private int time; + + private Time(int time) { + this.time = time; + } + + public int getTime() { + return time; + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/time/TimeStrategy.java b/src/com/garbagemule/MobArena/time/TimeStrategy.java new file mode 100644 index 0000000..9a5c77d --- /dev/null +++ b/src/com/garbagemule/MobArena/time/TimeStrategy.java @@ -0,0 +1,24 @@ +package com.garbagemule.MobArena.time; + +import org.bukkit.entity.Player; + +public interface TimeStrategy +{ + /** + * Set the time enum used by setPlayerTime() + * @param time a Time enum + */ + public void setTime(Time time); + + /** + * Set the local client time for the player. + * @param p a player + */ + public void setPlayerTime(Player p); + + /** + * Reset the local client time for the player to the server time + * @param p a player + */ + public void resetPlayerTime(Player p); +} diff --git a/src/com/garbagemule/MobArena/time/TimeStrategyLocked.java b/src/com/garbagemule/MobArena/time/TimeStrategyLocked.java new file mode 100644 index 0000000..3bdce23 --- /dev/null +++ b/src/com/garbagemule/MobArena/time/TimeStrategyLocked.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.time; + +import org.bukkit.entity.Player; + +public class TimeStrategyLocked implements TimeStrategy +{ + private Time time; + + public TimeStrategyLocked(Time time) { + setTime(time); + } + + @Override + public void setTime(Time time) { + this.time = time; + } + + @Override + public void setPlayerTime(Player p) { + p.setPlayerTime(time.getTime(), false); + } + + @Override + public void resetPlayerTime(Player p) { + p.resetPlayerTime(); + } +} diff --git a/src/com/garbagemule/MobArena/time/TimeStrategyNull.java b/src/com/garbagemule/MobArena/time/TimeStrategyNull.java new file mode 100644 index 0000000..7fe0dc0 --- /dev/null +++ b/src/com/garbagemule/MobArena/time/TimeStrategyNull.java @@ -0,0 +1,15 @@ +package com.garbagemule.MobArena.time; + +import org.bukkit.entity.Player; + +public class TimeStrategyNull implements TimeStrategy +{ + @Override + public void setTime(Time time) {} + + @Override + public void setPlayerTime(Player p) {} + + @Override + public void resetPlayerTime(Player p) {} +} diff --git a/src/com/garbagemule/MobArena/util/Config.java b/src/com/garbagemule/MobArena/util/Config.java deleted file mode 100644 index c1275c6..0000000 --- a/src/com/garbagemule/MobArena/util/Config.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.garbagemule.MobArena.util; - -import java.io.File; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; - -public class Config -{ - private YamlConfiguration config; - private File configFile; - - public Config(File configFile) { - this.config = new YamlConfiguration(); - this.configFile = configFile; - - config.options().indent(4); - } - - public boolean load() { - try { - config.load(configFile); - return true; - } - catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public boolean save() { - try { - config.save(configFile); - return true; - } - catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public void setHeader(String header) { - config.options().header(header); - } - - - - /////////////////////////////////////////////////////////////////////////// - // // - // GETTERS // - // // - /////////////////////////////////////////////////////////////////////////// - - public Object getProperty(String path) { - return config.get(path); - } - - public boolean getBoolean(String path) { - return config.getBoolean(path); - } - - public boolean getBoolean(String path, boolean def) { - return config.getBoolean(path, def); - } - - public int getInt(String path) { - return config.getInt(path); - } - - public int getInt(String path, int def) { - return config.getInt(path, def); - } - - public String getString(String path) { - String result = config.getString(path); - return result != null ? result : null; - } - - public String getString(String path, String def) { - return config.getString(path, def); - } - - public Set getKeys(String path) { - if (config.get(path) == null) - return null; - - ConfigurationSection section = config.getConfigurationSection(path); - return section.getKeys(false); - } - - @SuppressWarnings("unchecked") - public List getStringList(String path, List def) { - if (config.get(path) == null) - return def != null ? def : new LinkedList(); - - List list = config.getStringList(path); - return (List) list; - } - - - - /////////////////////////////////////////////////////////////////////////// - // // - // MUTATORS // - // // - /////////////////////////////////////////////////////////////////////////// - - public void set(String path, Object value) { - config.set(path, value); - } - - public void setProperty(String path, Object value) { - this.set(path, value); - } - - public void remove(String path) { - this.set(path, null); - } - - public void removeProperty(String path) { - this.remove(path); - } - - - - /////////////////////////////////////////////////////////////////////////// - // // - // UTILITY METHODS // - // // - /////////////////////////////////////////////////////////////////////////// - - public static Location parseLocation(World world, String coords) - { - String[] parts = coords.split(","); - if (parts.length != 5) - throw new IllegalArgumentException("Input string must contain x, y, z, yaw and pitch"); - - Integer x = getInteger(parts[0]); - Integer y = getInteger(parts[1]); - Integer z = getInteger(parts[2]); - Float yaw = getFloat(parts[3]); - Float pitch = getFloat(parts[4]); - - if (x == null || y == null || z == null || yaw == null || pitch == null) - throw new NullPointerException("Some of the parsed values are null!"); - - return new Location(world, x, y, z, yaw, pitch); - } - - private static Integer getInteger(String s) - { - try - { - return Integer.parseInt(s.trim()); - } - catch (Exception e) {} - - return null; - } - - private static Float getFloat(String s) - { - try - { - return Float.parseFloat(s.trim()); - } - catch (Exception e) {} - - return null; - } - -} diff --git a/src/com/garbagemule/MobArena/util/ConfigUtils.java b/src/com/garbagemule/MobArena/util/ConfigUtils.java deleted file mode 100644 index 469ebc7..0000000 --- a/src/com/garbagemule/MobArena/util/ConfigUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.garbagemule.MobArena.util; - -import org.bukkit.Location; -import org.bukkit.World; - -public class ConfigUtils -{ - public static Location parseLocation(World world, String coords) - { - String[] parts = coords.split(","); - if (parts.length != 5) - throw new IllegalArgumentException("Input string must contain x, y, z, yaw and pitch"); - - Integer x = getInt(parts[0]); - Integer y = getInt(parts[1]); - Integer z = getInt(parts[2]); - Float yaw = getFloat(parts[3]); - Float pitch = getFloat(parts[4]); - - if (x == null || y == null || z == null || yaw == null || pitch == null) - throw new NullPointerException("Some of the parsed values are null!"); - - return new Location(world, x, y, z, yaw, pitch); - } - - private static Integer getInt(String s) - { - try - { - return Integer.parseInt(s.trim()); - } - catch (Exception e) {} - - return null; - } - - private static Float getFloat(String s) - { - try - { - return Float.parseFloat(s.trim()); - } - catch (Exception e) {} - - return null; - } -} diff --git a/src/com/garbagemule/MobArena/util/Delays.java b/src/com/garbagemule/MobArena/util/Delays.java index 68fab16..faf9590 100644 --- a/src/com/garbagemule/MobArena/util/Delays.java +++ b/src/com/garbagemule/MobArena/util/Delays.java @@ -1,20 +1,29 @@ package com.garbagemule.MobArena.util; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; public class Delays { - public static void douse(MobArena plugin, final Player p, long delay) - { - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() - { - public void run() - { - p.setFireTicks(0); + public static void douse(MobArena plugin, final Player p, long delay) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + public void run() { + if (p.isOnline()) { + p.setFireTicks(0); + } } }, delay); } + + public static void revivePlayer(MobArena plugin, final Arena arena, final Player p) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + public void run() { + if (p.isOnline()) { + arena.revivePlayer(p); + } + } + }); + } } diff --git a/src/com/garbagemule/MobArena/util/Enums.java b/src/com/garbagemule/MobArena/util/Enums.java index 63e7051..d3904c9 100644 --- a/src/com/garbagemule/MobArena/util/Enums.java +++ b/src/com/garbagemule/MobArena/util/Enums.java @@ -5,15 +5,12 @@ public class Enums /** * Get the enum value of a string, null if it doesn't exist. */ - public static > T getEnumFromString(Class c, String string) - { - if(c != null && string != null) - { - try - { + public static > T getEnumFromString(Class c, String string) { + if (c != null && string != null) { + try { return Enum.valueOf(c, string.trim().toUpperCase()); } - catch(IllegalArgumentException ex) { } + catch(IllegalArgumentException ex) {} } return null; } @@ -21,15 +18,12 @@ public class Enums /** * Get the enum value of a string, null if it doesn't exist. */ - public static > T getEnumFromStringCaseSensitive(Class c, String string) - { - if(c != null && string != null) - { - try - { + public static > T getEnumFromStringCaseSensitive(Class c, String string) { + if (c != null && string != null) { + try { return Enum.valueOf(c, string); } - catch(IllegalArgumentException ex) { } + catch(IllegalArgumentException ex) {} } return null; } diff --git a/src/com/garbagemule/MobArena/util/FileUtils.java b/src/com/garbagemule/MobArena/util/FileUtils.java index 6205495..25da42f 100644 --- a/src/com/garbagemule/MobArena/util/FileUtils.java +++ b/src/com/garbagemule/MobArena/util/FileUtils.java @@ -11,9 +11,14 @@ import java.net.URLConnection; import java.util.HashSet; import java.util.Set; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + //import org.bukkit.util.config.Configuration; import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.waves.WaveUtils; public class FileUtils { @@ -40,7 +45,7 @@ public class FileUtils * Download all necessary libraries. * @param config The MobArena config-file */ - public static void fetchLibs(Config config) + public static void fetchLibs(MobArena plugin, Config config) { // Get all arenas Set arenas = config.getKeys("arenas"); @@ -59,17 +64,17 @@ public class FileUtils // Download all libraries for (Library lib : libs) - if (!libraryExists(lib)) - fetchLib(lib); + if (!libraryExists(plugin, lib)) + fetchLib(plugin, lib); } /** * Download a given library. * @param lib The Library to download */ - private static synchronized void fetchLib(Library lib) + private static synchronized void fetchLib(MobArena plugin, Library lib) { - MobArena.info("Downloading library '" + lib.filename + "' for log-method '" + lib.name().toLowerCase() + "'..."); + plugin.info("Downloading library '" + lib.filename + "' for log-method '" + lib.name().toLowerCase() + "'..."); URLConnection con = null; InputStream in = null; @@ -87,7 +92,7 @@ public class FileUtils if (lib.backup == null) { e.printStackTrace(); - System.out.println("Connection issues"); + plugin.warning("Connection issues"); return; } @@ -100,14 +105,14 @@ public class FileUtils catch (Exception e2) { e2.printStackTrace(); - System.out.println("Connection issues"); + plugin.warning("Connection issues"); return; } } try { - File libdir = new File(MobArena.dir, "lib"); + File libdir = new File(plugin.getDataFolder(), "lib"); libdir.mkdir(); File file = new File(libdir, lib.filename); @@ -126,14 +131,14 @@ public class FileUtils out.write(buffer, 0, length); // Announce successful download - MobArena.info(lib.filename + " downloaded in " + ((System.currentTimeMillis()-startTime)/1000.0) + " seconds."); + plugin.info(lib.filename + " downloaded in " + ((System.currentTimeMillis()-startTime)/1000.0) + " seconds."); addLibraryToClassLoader(file); } catch (Exception e) { e.printStackTrace(); - MobArena.warning("Couldn't download library: " + lib.filename); + plugin.warning("Couldn't download library: " + lib.filename); } finally { @@ -149,15 +154,12 @@ public class FileUtils } } - private static boolean libraryExists(Library lib) - { - return new File(MobArena.dir + File.separator + "lib", lib.filename).exists(); + private static boolean libraryExists(JavaPlugin plugin, Library lib) { + return new File(plugin.getDataFolder() + File.separator + "lib", lib.filename).exists(); } - private static void addLibraryToClassLoader(File file) - { - try - { + private static void addLibraryToClassLoader(File file) { + try { // Grab the class loader and its addURL method URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader(); Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{ URL.class }); @@ -166,8 +168,7 @@ public class FileUtils // Add the library addURL.invoke(cl, new Object[]{file.toURI().toURL()}); } - catch (Exception e) - { + catch (Exception e) { e.printStackTrace(); } } @@ -176,14 +177,15 @@ public class FileUtils * Create default files from the res/ directory, if they exist. * @param filenames Files to be created. */ - public static void extractDefaults(String... filenames) - { + public static void extractDefaults(MobArena plugin, String... filenames) { + File dir = plugin.getDataFolder(); + if (!dir.exists()) dir.mkdir(); + for (String filename : filenames) - extractFile(MobArena.dir, filename); + extractFile(plugin, dir, filename); } - public static File extractFile(File dir, String filename) - { + public static File extractFile(MobArena plugin, File dir, String filename) { // Skip if file exists File file = new File(dir, filename); if (file.exists()) return file; @@ -192,8 +194,7 @@ public class FileUtils InputStream in = MobArena.class.getResourceAsStream("/res/" + filename); if (in == null) return null; - try - { + try { // Set up an output stream FileOutputStream out = new FileOutputStream(file); byte[] buffer = new byte[8192]; @@ -208,10 +209,30 @@ public class FileUtils return file; } - catch (Exception e) - { + catch (Exception e) { e.printStackTrace(); - MobArena.warning("Problem creating file '" + filename + "'!"); + plugin.warning("Problem creating file '" + filename + "'!"); + } + + return null; + } + + public static YamlConfiguration getConfig(MobArena plugin, String filename) { + InputStream in = MobArena.class.getResourceAsStream("/res/" + filename); + if (in == null) { + plugin.error("Failed to load '" + filename + "', the server must be restarted!"); + return null; + } + + try { + YamlConfiguration result = new YamlConfiguration(); + result.load(in); + + return result; + } + catch (Exception e) { + e.printStackTrace(); + plugin.warning("Couldn't load '" + filename + "' as stream!"); } return null; diff --git a/src/com/garbagemule/MobArena/util/InventoryItem.java b/src/com/garbagemule/MobArena/util/InventoryItem.java deleted file mode 100644 index 38f2757..0000000 --- a/src/com/garbagemule/MobArena/util/InventoryItem.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.garbagemule.MobArena.util; - -import java.io.Serializable; -import java.util.LinkedList; -import java.util.List; - -import org.bukkit.inventory.ItemStack; -import org.bukkit.material.MaterialData; - -public class InventoryItem implements Serializable -{ - private static final long serialVersionUID = 739709220350581510L; - private int id; - private int amount; - private Byte data; - private short durability; - - /** - * Default constructor. - * @param id The data value/type id of the ItemStack - * @param amount The amount of the stack - * @param data The MaterialData (possibly null) of the stack - * @param durability The durability of the stack - */ - public InventoryItem(int id, int amount, Byte data, short durability) - { - this.id = id; - this.amount = amount; - this.data = data; - this.durability = durability; - } - - /** - * ItemStack constructor. - * @param stack The ItemStack to base the InventoryItem off of. - */ - public InventoryItem(ItemStack stack) - { - if (stack == null) - id = -1; - - id = stack.getTypeId(); - amount = stack.getAmount(); - - // In case of "odd" items, don't attempt to get data and durability - if (id < 0) return; - - data = stack.getData() == null ? null : stack.getData().getData(); - durability = stack.getDurability(); - } - - /** - * Static method for turning a (possibly null) InventoryItem into an ItemStack. - * The method is useful if it is unknown whether an InventoryItem object is - * null or not. - * @param item The Inventory item to convert - * @return An ItemStack representation of the InventoryItem, or null - */ - public static ItemStack toItemStack(InventoryItem item) - { - if (item == null) - return null; - - return item.toItemStack(); - } - - public static ItemStack[] toItemStacks(InventoryItem[] items) - { - ItemStack[] result = new ItemStack[items.length]; - - for (int i = 0; i < items.length; i++) - result[i] = items[i].toItemStack(); - - return result; - } - - /** - * Static method for converting an ItemStack to an InventoryItems. - * @param stack The ItemStack to convert - * @return An InventoryItem representation of the ItemStack, or null - */ - public static InventoryItem parseItemStack(ItemStack stack) - { - if (stack == null) - return new InventoryItem(-1, -1, null, (short) 0); - - return new InventoryItem(stack); - } - - /** - * Static method for converting an ItemStack array to an InventoryItem array. - * @param stacks The ItemStack array - * @return An InventoryItem array - */ - public static InventoryItem[] parseItemStacks(ItemStack[] stacks) - { - InventoryItem[] items = new InventoryItem[stacks.length]; - - for (int i = 0; i < items.length; i++) - items[i] = parseItemStack(stacks[i]); - - return items; - } - - /** - * Convert a list of ItemStacks into an array of InventoryItems - * @param stacks List of ItemStacks to convert - * @return An InventoryItem array - */ - public static InventoryItem[] parseItemStacks(List stacks) - { - InventoryItem[] items = new InventoryItem[stacks.size()]; - - for (int i = 0; i < items.length; i++) - items[i] = parseItemStack(stacks.get(i)); - - return items; - } - - /** - * Static method for extracting all InventoryItems from an InventoryItem array. - * @param id The type id of the items to extract - * @param items The InventoryItem array - * @return A list of all InventoryItems removed - */ - public static List extractAllFromArray(int id, InventoryItem[] items) - { - List list = new LinkedList(); - - for (int i = 0; i < items.length; i++) - { - if (items[i].getTypeId() == id) - { - list.add(items[i]); - items[i].setTypeId(-1); - } - } - - return list; - } - - /** - * Static method for removing an InventoryItem from an InventoryItem array. - * @param item The InventoryItem to remove - * @param items The InventoryItem array to remove from - * @return true, if the item was removed successfully, false otherwise - */ - public static boolean removeItemFromArray(InventoryItem item, InventoryItem[] items) - { - // Grab the total amount to remove - int leftToRemove = item.getAmount(); - - for (int i = 0; i < items.length; i++) - { - if (items[i].getTypeId() != item.getTypeId()) - continue; - - // Grab the amount - int amount = items[i].getAmount(); - - // Reduce amount/nullify item - if (amount > leftToRemove) - { - items[i].setAmount(amount - leftToRemove); - leftToRemove = 0; - } - else - { - items[i].setTypeId(-1); - leftToRemove -= amount; - } - - // If nothing left to remove, return true. - if (leftToRemove == 0) - return true; - } - - return false; - } - - /** - * Get the data value/type id of the item - * @return A type id - */ - public int getTypeId() - { - return id; - } - - /** - * Set the data value/type id of the item - * @param id A type id - */ - public void setTypeId(int id) - { - this.id = id; - } - - /** - * Get the amount of the item - * @return An amount - */ - public int getAmount() - { - return amount; - } - - /** - * Set the amount of the item - * @param amount An amount - */ - public void setAmount(int amount) - { - this.amount = amount; - } - - /** - * Get the MaterialData of the item - * @return A MaterialData - */ - public MaterialData getData() - { - return new MaterialData(id, data == null ? (byte) 0 : data); - } - - /** - * Set the MaterialData of the item - * @param data A MaterialData - */ - public void setData(MaterialData data) - { - this.data = data.getData(); - } - - /** - * Get the durability of the item - * @return The item durability - */ - public short getDurability() - { - return durability; - } - - /** - * Set the durability of the item - * @param durability An item durability - */ - public void setDurability(short durability) - { - this.durability = durability; - } - - /** - * Convert this InventoryItem to an ItemStack representation - * @return An ItemStack representation of this InventoryItem, or null if the type id is -1 - */ - public ItemStack toItemStack() - { - if (id == -1) - return null; - - ItemStack stack = new ItemStack(id, amount, durability); - - if (data != null) - stack.setData(getData()); - - return stack; - } -} diff --git a/src/com/garbagemule/MobArena/util/ItemParser.java b/src/com/garbagemule/MobArena/util/ItemParser.java index 979f6af..0dff503 100644 --- a/src/com/garbagemule/MobArena/util/ItemParser.java +++ b/src/com/garbagemule/MobArena/util/ItemParser.java @@ -1,102 +1,223 @@ package com.garbagemule.MobArena.util; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; +import java.util.Map.Entry; import org.bukkit.DyeColor; import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; +import org.bukkit.material.MaterialData; + +import com.garbagemule.MobArena.MobArena; public class ItemParser { - public static List parseItems(String s) - { - List result = new LinkedList(); + public static String parseString(ItemStack... stacks) { + String result = ""; - String[] items = s.split(","); - for (String item : items) - { - ItemStack stack = parseItem(item.trim()); - if (stack != null) - result.add(stack); + // Parse each stack + for (ItemStack stack : stacks) { + if (stack == null || stack.getTypeId() == 0) continue; + + result += ", " + parseString(stack); + } + + // Trim off the leading ', ' if it is there + if (!result.equals("")) { + result = result.substring(2); } return result; } - public static ItemStack parseItem(String item) - { + public static String parseString(ItemStack stack) { + if (stack.getTypeId() == 0) return null; + + // part + String type = stack.getType().toString().toLowerCase(); + + // part + MaterialData md = stack.getData(); + byte data = (md != null ? md.getData() : 0); + + // Take wool into account + if (stack.getType() == Material.WOOL) { + data = (byte) (15 - data); + } + + // part + int amount = stack.getAmount(); + + // Enchantments + String enchantments = ""; + for (Entry entry : stack.getEnchantments().entrySet()) { + int id = entry.getKey().getId(); + int lvl = entry.getValue(); + + // :; + enchantments += "; " + id + ":" + lvl; + } + + // Trim off the leading ';' if it is there + if (!enchantments.equals("")) { + enchantments = enchantments.substring(2); + } + + // + String result = type; + + // (:) + if (data != 0) { + result += ":" + data; + } + + // ((:):) - force if there is data + if (amount > 1 || data != 0) { + result += ":" + amount; + } + + // ((:):) (:(;:(; ... ))) + if (!enchantments.equals("")) { + result += " " + enchantments; + } + + return result; + } + + public static List parseItems(String s) { + if (s == null) { + return new ArrayList(1); + } + + String[] items = s.split(","); + List result = new ArrayList(items.length); + + for (String item : items) { + ItemStack stack = parseItem(item.trim()); + if (stack != null) { + result.add(stack); + } + } + + return result; + } + + public static ItemStack parseItem(String item) { if (item == null || item.equals("")) return null; - String[] parts = item.split(":"); - if (parts.length == 1) - return singleItem(parts[0]); - if (parts.length == 2) - return withAmount(parts[0], parts[1]); - if (parts.length == 3) - return withDataAndAmount(parts[0], parts[1], parts[2]); + // Check if the item has enchantments. + String[] space = item.split(" "); + String[] parts = (space.length == 2 ? space[0].split(":") : item.split(":")); - return null; + ItemStack result = null; + + switch (parts.length) { + case 1: + result = singleItem(parts[0]); + break; + case 2: + result = withAmount(parts[0], parts[1]); + break; + case 3: + result = withDataAndAmount(parts[0], parts[1], parts[2]); + break; + } + + if (space.length == 2) { + addEnchantments(result, space[1]); + } + + return result; } - private static ItemStack singleItem(String item) - { + private static ItemStack singleItem(String item) { + if (item.matches("\\$[1-9][0-9]*")) { + int amount = Integer.parseInt(item.substring(1)); + return new ItemStack(MobArena.ECONOMY_MONEY_ID, amount); + } Material m = getMaterial(item); - return m == null ? null : new ItemStack(m); + return m == null ? null : new ItemStack(m,1); } - private static ItemStack withAmount(String item, String amount) - { + private static ItemStack withAmount(String item, String amount) { Material m = getMaterial(item); int a = getAmount(amount); return m == null ? null : new ItemStack(m,a); } - private static ItemStack withDataAndAmount(String item, String data, String amount) - { + private static ItemStack withDataAndAmount(String item, String data, String amount) { Material m = getMaterial(item.toUpperCase()); - byte d = getData(data); + short d = getData(data, m); int a = getAmount(amount); - if (m == null) + if (m == null) { return null; + } - if (m.getId() == 35) + if (m.getId() == 35) { d = (byte) (15-d); + } - return new ItemStack(m,a,(short)0,d); + return new ItemStack(m,a,d); } - private static Material getMaterial(String item) - { - if (item.matches("[0-9]*")) + private static Material getMaterial(String item) { + if (item.matches("[0-9]*")) { return Material.getMaterial(Integer.parseInt(item)); + } return Material.getMaterial(item.toUpperCase()); } - private static int getAmount(String amount) - { - if (amount.matches("[1-9][0-9]*")) + private static int getAmount(String amount) { + if (amount.matches("[1-9][0-9]*")) { return Integer.parseInt(amount); + } return 1; } - private static byte getData(String data) - { - DyeColor dye = null; - - if (data.matches("[0-9]+")) - { - dye = DyeColor.getByData(Byte.parseByte(data)); - return dye == null ? 0 : dye.getData(); + private static short getData(String data, Material m) { + // If ink sack or wool, parse wisely. + if (m == Material.INK_SACK || m == Material.WOOL) { + // Ink sacks must be offset. + byte offset = (m == Material.INK_SACK ? (byte) 15 : (byte) 0); + + DyeColor dye = (data.matches("[0-9]+")) ? + DyeColor.getByData((byte) Math.abs(offset - Integer.parseInt(data))) : + DyeColor.valueOf(data.toUpperCase()); + + return (short) Math.abs(offset - dye.getData()); } - else - { - dye = Enums.getEnumFromString(DyeColor.class, data); - return dye == null ? 0 : (byte) (15-dye.getData()); + + return Short.parseShort(data); + } + + private static void addEnchantments(ItemStack stack, String list) { + String[] parts = list.split(";"); + + for (String ench : parts) { + addEnchantment(stack, ench.trim()); } } + + private static void addEnchantment(ItemStack stack, String ench) { + String[] parts = ench.split(":"); + if (parts.length != 2 || !(parts[0].matches("[0-9]*") && parts[1].matches("[0-9]*"))) { + return; + } + + int id = Integer.parseInt(parts[0]); + int lvl = Integer.parseInt(parts[1]); + + Enchantment e = Enchantment.getById(id); + if (e == null) {// || !e.canEnchantItem(stack) || lvl > e.getMaxLevel() || lvl < e.getStartLevel()) { + return; + } + //stack.addEnchantment(e, lvl); + stack.addUnsafeEnchantment(e, lvl); + } } diff --git a/src/com/garbagemule/MobArena/util/MutableInt.java b/src/com/garbagemule/MobArena/util/MutableInt.java new file mode 100644 index 0000000..e321403 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/MutableInt.java @@ -0,0 +1,31 @@ +package com.garbagemule.MobArena.util; + +public class MutableInt +{ + private int value; + + public MutableInt(int value) { + this.value = value; + } + + public MutableInt() { + this(0); + } + + public void add(int amount) { + this.value += amount; + } + + public void inc() { + this.value++; + } + + public int value() { + return value; + } + + @Override + public String toString() { + return "" + value; + } +} diff --git a/src/com/garbagemule/MobArena/util/TextUtils.java b/src/com/garbagemule/MobArena/util/TextUtils.java index 8b3dcb2..9e14f2b 100644 --- a/src/com/garbagemule/MobArena/util/TextUtils.java +++ b/src/com/garbagemule/MobArena/util/TextUtils.java @@ -53,4 +53,12 @@ public class TextUtils return s.substring(0, length); } public static String truncate(String s) { return truncate(s, 15); } + + public static String camelCase(String s) { + if (s == null || s.length() < 2) + return null; + + String firstLetter = s.substring(0,1).toUpperCase(); + return firstLetter + s.substring(1).toLowerCase(); + } } diff --git a/src/com/garbagemule/MobArena/util/VersionChecker.java b/src/com/garbagemule/MobArena/util/VersionChecker.java new file mode 100644 index 0000000..b0ee7e9 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/VersionChecker.java @@ -0,0 +1,90 @@ +package com.garbagemule.MobArena.util; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.garbagemule.MobArena.MobArena; + +public class VersionChecker +{ + public static String site = "http://forums.bukkit.org/threads/19144/"; + + /** + * Check if the current plugin version is the latest version. + * @param plugin a MobArena instance + * @return false, if the version is not the latest, true otherwise + */ + public static boolean isLatest(MobArena plugin) { + if (!plugin.getMAConfig().getBoolean("global-settings.update-notification", false)) { + return true; + } + + try { + // Make a URI of the site address + URI baseURI = new URI(site); + + // Open the connection and don't redirect. + HttpURLConnection con = (HttpURLConnection) baseURI.toURL().openConnection(); + con.setConnectTimeout(5000); + con.setInstanceFollowRedirects(false); + + String header = con.getHeaderField("Location"); + + // If something's wrong with the connection... + if (header == null) { + return true; + } + + // Otherwise, grab the location header to get the real URI. + String url = new URI(con.getHeaderField("Location")).toString(); + + // Set up the regex and matcher + Pattern regex = Pattern.compile("v([0-9]+-)*[0-9]+"); + Matcher matcher = regex.matcher(url); + if (!matcher.find()) { + return true; + } + + String thisVersion = plugin.getDescription().getVersion(); + String forumVersion = matcher.group().substring(1).replace("-", "."); + + return isLatest(thisVersion, forumVersion); + } + catch (Exception e) { + e.printStackTrace(); + } + + return true; + } + + /** + * Check if thisVersion is newer than or equal to forumVersion + * @param thisVersion a version string + * @param forumVersion a version string + * @return true, if thisVersion is newer than or equal to forumVersion, false otherwise + */ + public static boolean isLatest(String thisVersion, String forumVersion) { + String[] forumParts = forumVersion.split("\\."); + String[] thisParts = thisVersion.split("\\."); + + int current, forum; + boolean thisIsNewer = false; + + for (int i = 0; i < Math.max(forumParts.length, thisParts.length); i++) { + forum = forumParts.length <= i ? 0 : Integer.parseInt(forumParts[i]); + current = thisParts.length <= i ? 0 : Integer.parseInt(thisParts[i]); + + if (forum < current) { + thisIsNewer = true; + } + + if (forum > current && !thisIsNewer) { + return false; + } + } + + return true; + } +} diff --git a/src/com/garbagemule/MobArena/util/classparsing/ClassIterationStrategy.java b/src/com/garbagemule/MobArena/util/classparsing/ClassIterationStrategy.java deleted file mode 100644 index b0baded..0000000 --- a/src/com/garbagemule/MobArena/util/classparsing/ClassIterationStrategy.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.garbagemule.MobArena.util.classparsing; - -import java.util.Collection; - -public interface ClassIterationStrategy -{ - /** - * Given a package, e.g. com.garbagemule.MobArena.commands.user, the - * concrete strategy will return a collection of qualified class names - * either from a directory or a jar file. - * @param packagePath path from which to extract classes - * @return a list of qualified class names or null - */ - public Collection getClassesFromPackage(String packagePath); -} diff --git a/src/com/garbagemule/MobArena/util/classparsing/ClassParser.java b/src/com/garbagemule/MobArena/util/classparsing/ClassParser.java deleted file mode 100644 index e194614..0000000 --- a/src/com/garbagemule/MobArena/util/classparsing/ClassParser.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.garbagemule.MobArena.util.classparsing; - -import java.util.*; - -public class ClassParser -{ - private ClassIterationStrategy strategy; - - public ClassParser(ClassIterationStrategy strategy) { - this.strategy = strategy; - } - - /** - * Using an iteration strategy provided in the constructor, this method tries - * to iterate through all classes within a given "package". - * @param packagePath a package path, e.g. com.garbagemule.MobArena.commands.user - * @return a list of Class objects - */ - public Collection> getClasses(String packagePath) { - Collection> result = new LinkedList>(); - - for (String classname : strategy.getClassesFromPackage(packagePath)) { - Class c = makeClass(classname); - - if (c != null) { - result.add(c); - } - } - - return result; - } - - /** - * Get a class using its qualified (full package) name. - * @param qualifiedName qualified name of a class - * @return a Class object, or null if the class could not be made. - */ - private static Class makeClass(String qualifiedName) { - try { - return Class.forName(qualifiedName); - } - catch (Exception e) { - return null; - } - } -} diff --git a/src/com/garbagemule/MobArena/util/classparsing/DirClassIterationStrategy.java b/src/com/garbagemule/MobArena/util/classparsing/DirClassIterationStrategy.java deleted file mode 100644 index 177ac31..0000000 --- a/src/com/garbagemule/MobArena/util/classparsing/DirClassIterationStrategy.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.garbagemule.MobArena.util.classparsing; - -import java.io.*; -import java.util.*; - -public class DirClassIterationStrategy implements ClassIterationStrategy -{ - private File baseDir; - - /** - * Dir iterator. The input file must be a directory, whose - * contents are iterated over upon calling the implemented - * method. This dir could be bin/ or build/. - * @param file a directory - * @throws Exception - */ - public DirClassIterationStrategy(File baseDir) throws Exception { - this.baseDir = baseDir; - } - - @Override - public Collection getClassesFromPackage(String packagePath) { - // The resulting list. - Collection result = new LinkedList(); - - // Convert to an actual path. - String path = packageToPath(packagePath); - - // Make a new dir from it. - File dir = new File(baseDir, path); - - for (File f : dir.listFiles()) { - String name = f.getName(); - String absolutePath = f.getAbsolutePath(); - - if (absolutePath.contains(path) && name.endsWith(".class")) { - result.add( packagePath + "." + stripExtension(name) ); - } - } - - return result; - } - - private String stripExtension(String file) { - int dot = file.lastIndexOf("."); - return file.substring(0, dot); - } - - private String packageToPath(String path) { - return path.replace(".", File.separator); - } -} diff --git a/src/com/garbagemule/MobArena/util/classparsing/JarClassIterationStrategy.java b/src/com/garbagemule/MobArena/util/classparsing/JarClassIterationStrategy.java deleted file mode 100644 index f7713b9..0000000 --- a/src/com/garbagemule/MobArena/util/classparsing/JarClassIterationStrategy.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.garbagemule.MobArena.util.classparsing; - -import java.io.*; -import java.util.*; -import java.util.jar.*; - -public class JarClassIterationStrategy implements ClassIterationStrategy -{ - private JarFile jarFile; - - /** - * Jar iterator. The input file is turned into a JarFile object, - * which is later used to extract a list of entries to iterate - * over. The implemented method filters through the entries and - * returns any matches. - * @param file a jar file - * @throws Exception - */ - public JarClassIterationStrategy(File file) throws Exception { - this.jarFile = new JarFile(file); - } - - @Override - public Collection getClassesFromPackage(String packagePath) { - // The resulting list. - Collection result = new LinkedList(); - - // Convert to an actual path. - String path = packageToPath(packagePath); - - // Grab the entries from the jar file. - Enumeration entries = jarFile.entries(); - - /* Iterate through all the entries in the jar file, and - * if a file starts with the package path and ends with - * .class, we have a match, in which case we strip the - * extension and convert to a package representation. */ - while (entries.hasMoreElements()) { - String entry = entries.nextElement().getName(); - - if (entry.startsWith(path) && entry.endsWith(".class")) { - result.add( pathToPackage( stripExtension(entry) ) ); - } - } - - // Return the resulting list. - return result; - } - - private String stripExtension(String file) { - int dot = file.lastIndexOf("."); - return file.substring(0, dot); - } - - private String packageToPath(String path) { - return path.replace(".", "/"); - } - - private String pathToPackage(String path) { - return path.replace("/", "."); - } -} diff --git a/src/com/garbagemule/MobArena/util/config/Config.java b/src/com/garbagemule/MobArena/util/config/Config.java new file mode 100644 index 0000000..41a3bcd --- /dev/null +++ b/src/com/garbagemule/MobArena/util/config/Config.java @@ -0,0 +1,446 @@ +package com.garbagemule.MobArena.util.config; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; + +/** + * Wrapper class for Bukkit's Configuration classes. + * + * The motivation for this class is two-fold: + * + * 1) Provide a means of keeping a reference to the physical disk file + * in the same class as the YamlConfiguration for easy load and save. + * 2) Speed up the access times for the YamlConfiguration by loading all + * values into maps, as to avoid local fields in certain classes. + * + * The specific getters (getInteger, getBoolean, etc.) never call methods on + * the YamlConfiguration. Instead, they interact only with the value maps, + * which speeds up the access times 20-fold. The generic getter (get), does, + * however, interact with the YamlConfiguration (for now). + * + * The mutators (set, remove, etc.) interact with both the YamlConfiguration + * and the value maps for consistency. This means modifications become slightly + * slower, but the difference is neglegible. + * + * @author garbagemule, slipcor + * + * Use this class however you see fit, but please leave this description in, + * as to not unrightfully take credit for our work :) + */ +public class Config +{ + private YamlConfiguration config; + private File configFile; + private Map booleans; + private Map ints; + private Map doubles; + private Map strings; + private Map items; + + /** + * Create a new Config instance that uses the specified file for + * loading and saving. + * @param configFile a YAML file + */ + public Config(File configFile) { + this.config = new YamlConfiguration(); + this.configFile = configFile; + + this.booleans = new HashMap(); + this.ints = new HashMap(); + this.doubles = new HashMap(); + this.strings = new HashMap(); + this.items = new HashMap(); + + this.config.options().indent(4); + } + + /** + * Load the config-file into the YamlConfiguration, and then populate + * the value maps. + * @return true, if the load succeeded, false otherwise. + */ + public boolean load() { + try { + if (!configFile.exists()) { + configFile.createNewFile(); + } + config.load(configFile); + reloadMaps(); + return true; + } + catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * Iterates through all keys in the config-file, and populates the + * value maps. Boolean values are stored in the booleans-map, Strings + * in the strings-map, etc. + */ + public void reloadMaps() { + for (String s : config.getKeys(true)) { + Object o = config.get(s); + + if (o instanceof Boolean) { + booleans.put(s, (Boolean) o); + } else if (o instanceof Integer) { + ints.put(s, (Integer) o); + } else if (o instanceof Double) { + doubles.put(s, (Double) o); + } else if (o instanceof ItemStack) { + items.put(s, (ItemStack) o); + } else if (o instanceof String) { + strings.put(s, (String) o); + } + } + } + + /** + * Save the YamlConfiguration to the config-file. + * @return true, if the save succeeded, false otherwise. + */ + public boolean save() { + try { + config.save(configFile); + load(); // enforce consistency + return true; + } + catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * Get the header of the config-file. + * @return the header + */ + public String getHeader() { + return config.options().header(); + } + + /** + * Set the header of the config-file. + * @param header the header + */ + public void setHeader(String header) { + config.options().header(header); + } + + + + /////////////////////////////////////////////////////////////////////////// + // // + // GETTERS // + // // + /////////////////////////////////////////////////////////////////////////// + + /** + * Get the YamlConfiguration associated with this Config instance. + * Note that changes made directly to the YamlConfiguration will cause an + * inconsistency with the value maps unless reloadMaps() is called. + * @return the YamlConfiguration of this Config instance + */ + public YamlConfiguration getYamlConfiguration() { + return config; + } + + /** + * Grab a section of this Config. + * @param node + * @return + */ + public ConfigSection getConfigSection(String node) { + if (get(node) != null) { + return new ConfigSection(this, node); + } + return null; + } + + /** + * Retrieve a value from the YamlConfiguration. + * @param path the path of the value + * @return the value of the path + */ + public Object get(String path) { + return config.get(path); + } + + /** + * Retrieve a boolean from the value maps. + * @param path the path of the value + * @return the boolean value of the path if the path exists, false otherwise + */ + public boolean getBoolean(String path) { + return getBoolean(path, false); + } + + /** + * Retrieve a boolean from the value maps. + * @param path the path of the value + * @param def a default value to return if the value was not in the map + * @return the boolean value of the path if it exists, def otherwise + */ + public boolean getBoolean(String path, boolean def) { + Boolean result = booleans.get(path); + return (result != null ? result : def); + } + + /** + * Retrieve an int from the value maps. + * @param path the path of the value + * @return the int value of the path if the path exists, 0 otherwise + */ + public int getInt(String path) { + return getInt(path, 0); + } + + /** + * Retrieve an int from the value maps. + * @param path the path of the value + * @param def a default value to return if the value was not in the map + * @return the int value of the path if it exists, def otherwise + */ + public int getInt(String path, int def) { + Integer result = ints.get(path); + return (result != null ? result : def); + } + + /** + * Retrieve a double from the value maps. + * @param path the path of the value + * @return the double value of the path if the path exists, 0D otherwise + */ + public double getDouble(String path) { + return getDouble(path, 0D); + } + + /** + * Retrieve a double from the value maps. + * @param path the path of the value + * @param def a default value to return if the value was not in the map + * @return the double value of the path if it exists, def otherwise + */ + public double getDouble(String path, double def) { + Double result = doubles.get(path); + return (result != null ? result : def); + } + + /** + * Retrieve a string from the value maps. + * @param path the path of the value + * @return the string value of the path if the path exists, null otherwise + */ + public String getString(String path) { + return getString(path, null); + } + + /** + * Retrieve a string from the value maps. + * @param path the path of the value + * @param def a default value to return if the value was not in the map + * @return the string value of the path if it exists, def otherwise + */ + public String getString(String path, String def) { + String result = strings.get(path); + return (result != null ? result : def); + } + + public Location getLocation(String path, World world) { + return getLocation(path, world, null); + } + + public Location getLocation(String path, World world, Location def) { + String string = getString(path); + + if (string == null) + return def; + + return parseLocation(world, string); + } + + public ItemStack getItemStack(String path) { + return getItemStack(path, null); + } + + public ItemStack getItemStack(String path, ItemStack def) { + ItemStack result = items.get(path); + return (result != null ? result : def); + } + + public Set getKeys(String path) { + if (config.get(path) == null) + return null; + + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) + return null; + + return section.getKeys(false); + } + + @SuppressWarnings("unchecked") + public List getStringList(String path, List def) { + if (config.get(path) == null) + return def != null ? def : new ArrayList(); + + List list = config.getStringList(path); + return (List) list; + } + + + + /////////////////////////////////////////////////////////////////////////// + // // + // MUTATORS // + // // + /////////////////////////////////////////////////////////////////////////// + + /** + * Set the value of the given path in both the value maps and the + * YamlConfiguration. Note that this will only properly put the value + * in its relevant value map, if it is of one of the supported types. + * The method can also be used to remove values from their maps and + * the YamlConfiguration by passing null for the value. + * @param path the path on which to set the value + * @param value the value to set + */ + public void set(String path, Object value) { + if (value instanceof Boolean) { + booleans.put(path, (Boolean) value); + } else if (value instanceof Integer) { + ints.put(path, (Integer) value); + } else if (value instanceof Double) { + doubles.put(path, (Double) value); + } else if (value instanceof ItemStack) { + items.put(path, (ItemStack) value); + } else if (value instanceof String) { + strings.put(path, (String) value); + } else if (value instanceof Location) { + value = locationToString((Location) value); + strings.put(path, value.toString()); + } + + if (value == null) { + booleans.remove(path); + ints.remove(path); + doubles.remove(path); + strings.remove(path); + items.remove(path); + } + + config.set(path, value); + } + + /** + * Remove the node at the given path. Uses the set() method with null + * as the value. + * @param path the path of the node to remove + */ + public void remove(String path) { + this.set(path, null); + } + + + + /////////////////////////////////////////////////////////////////////////// + // // + // UTILITY METHODS // + // // + /////////////////////////////////////////////////////////////////////////// + + /** + * Parse an input string on the form "x,y,z" and an input World to create + * a Location. This method will only accept strings of the specified form. + * @param world the World in which the Location exists + * @param coords a string on the form "x,y,z" + * @return a Location in the given world with the given coordinates + */ + public static Location parseSimpleLocation(World world, String coords) { + String[] parts = coords.split(","); + if (parts.length != 3) + throw new IllegalArgumentException("Input string must contain only x, y, and z"); + + Integer x = parseInteger(parts[0]); + Integer y = parseInteger(parts[1]); + Integer z = parseInteger(parts[2]); + + if (x == null || y == null || z == null) + throw new NullPointerException("Some of the parsed values are null!"); + + return new Location(world, x, y, z); + } + + /** + * Parse an input string on the form "x,y,z,yaw,pitch" and an input World + * to create a Location. This method will only accept strings of the + * specified form. + * @param world the World in which the Location exists + * @param coords a string on the form "x,y,z,yaw,pitch" + * @return a Location in the given world with the given coordinates + */ + public static Location parseLocation(World world, String coords) { + String[] parts = coords.split(","); + if (parts.length != 5) + throw new IllegalArgumentException("Input string must contain x, y, z, yaw and pitch"); + + Integer x = parseInteger(parts[0]); + Integer y = parseInteger(parts[1]); + Integer z = parseInteger(parts[2]); + Float yaw = parseFloat(parts[3]); + Float pitch = parseFloat(parts[4]); + + if (x == null || y == null || z == null || yaw == null || pitch == null) + throw new NullPointerException("Some of the parsed values are null!"); + + return new Location(world, x, y, z, yaw, pitch); + } + + /** + * Create a String representation of a Location object. + * @param loc a Location + * @return a String of the form "x,y,z,yaw,pitch" + */ + public static String locationToString(Location loc) { + StringBuffer result = new StringBuffer(); + + result.append(loc.getBlockX() + ","); + result.append(loc.getBlockY() + ","); + result.append(loc.getBlockZ() + ","); + result.append(loc.getYaw() + ","); + result.append(loc.getPitch()); + + return result.toString(); + } + + private static Integer parseInteger(String s) { + try { + return Integer.parseInt(s.trim()); + } + catch (Exception e) { + return null; + } + } + + private static Float parseFloat(String s) { + try { + return Float.parseFloat(s.trim()); + } + catch (Exception e) { + return null; + } + } +} diff --git a/src/com/garbagemule/MobArena/util/config/ConfigSection.java b/src/com/garbagemule/MobArena/util/config/ConfigSection.java new file mode 100644 index 0000000..f155d03 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/config/ConfigSection.java @@ -0,0 +1,97 @@ +package com.garbagemule.MobArena.util.config; + +import java.util.List; +import java.util.Set; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.util.config.Config; + +public class ConfigSection +{ + private Config config; + private String path; + + public ConfigSection(Config config, String path) { + this.config = config; + this.path = (path.endsWith(".") ? path : path + "."); + } + + public Config getParent() { + return config; + } + + public ConfigSection getConfigSection(String path) { + return new ConfigSection(config, this.path + path); + } + + public Object get(String node) { + return config.get(path + node); + } + + public int getInt(String node) { + return config.getInt(path + node); + } + + public int getInt(String node, int def) { + return config.getInt(path + node, def); + } + + public double getDouble(String node) { + return config.getDouble(path + node); + } + + public double getDouble(String node, double def) { + return config.getDouble(path + node, def); + } + + public boolean getBoolean(String node) { + return config.getBoolean(path + node); + } + + public boolean getBoolean(String node, boolean def) { + return config.getBoolean(path + node, def); + } + + public String getString(String node) { + return config.getString(path + node); + } + + public String getString(String node, String def) { + return config.getString(path + node, def); + } + + public Location getLocation(String node, World world) { + return config.getLocation(path + node, world); + } + + public Location getLocation(String node, World world, Location def) { + return config.getLocation(path + node, world, def); + } + + public ItemStack getItemStack(String path) { + return config.getItemStack(path); + } + + public ItemStack getItemStack(String path, ItemStack def) { + return config.getItemStack(path, def); + } + + public Set getKeys() { + return config.getKeys(path); + } + + public Set getKeys(String node) { + return config.getKeys(path + node); + } + + public List getStringList(String node, List def) { + return config.getStringList(path + node, def); + } + + public void set(String node, Object value) { + config.set(path + node, value); + } +} diff --git a/src/com/garbagemule/MobArena/util/config/ConfigUtils.java b/src/com/garbagemule/MobArena/util/config/ConfigUtils.java new file mode 100644 index 0000000..f7627d2 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/config/ConfigUtils.java @@ -0,0 +1,79 @@ +package com.garbagemule.MobArena.util.config; + +import java.util.Set; + +import org.bukkit.configuration.file.YamlConfiguration; + +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.util.FileUtils; + +public class ConfigUtils +{ + public static void addMissingNodes(MobArena plugin, Config config, String path, String filename) { + assertNodes(plugin, config, path, filename, true); + } + + public static void replaceAllNodes(MobArena plugin, Config config, String path, String filename) { + assertNodes(plugin, config, path, filename, false); + } + + private static void assertNodes(MobArena plugin, Config config, String path, String filename, boolean keepOthers) { + // Grab the section that the path is pointing to. + ConfigSection section = config.getConfigSection(path); + + // If null, create the node. + if (section == null) { + config.set(path, ""); + section = config.getConfigSection(path); + } + + // Extract the yml file. + YamlConfiguration ymlConfig = FileUtils.getConfig(plugin, filename); + + // Assert the nodes. + assertNodes(section, ymlConfig, keepOthers); + } + + private static void assertNodes(ConfigSection config, YamlConfiguration ymlConfig, boolean keepOthers) { + if (config == null || ymlConfig == null) return; + + // Grab the default keys. + Set keys = ymlConfig.getKeys(false); + if (keys == null || keys.isEmpty()) return; + + // First ensure that all default nodes exist + for (String key : keys) { + if (config.get(key) == null) { + Object o = ymlConfig.get(key); + config.set(key, o); + } + } + + // If any other nodes in the config should remain, return + if (keepOthers) return; + + // Otherwise, grab all the current nodes. + Set oldKeys = config.getKeys(); + if (oldKeys == null || oldKeys.isEmpty()) return; + + // Remove all nodes that aren't in the defaults. + for (String old : oldKeys) { + if (!keys.contains(old)) { + config.set(old, null); + } + } + } + + public static String waveRewardList(String arena, String type) { + return "arenas." + arena + ".rewards.waves." + type; + } + + public static String waveReward(String arena, String type, int wave) { + return "arenas." + arena + ".rewards.waves." + type + "." + wave; + } + + public static String waveReward(Arena arena, String type, int wave) { + return "arenas." + arena.configName() + ".rewards.waves." + type + "." + wave; + } +} diff --git a/src/com/garbagemule/MobArena/util/data/PlainText.java b/src/com/garbagemule/MobArena/util/data/PlainText.java deleted file mode 100644 index c0cf907..0000000 --- a/src/com/garbagemule/MobArena/util/data/PlainText.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.garbagemule.MobArena.util.data; - -import java.io.File; -import java.io.FileWriter; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.ArenaLog; -import com.garbagemule.MobArena.ArenaPlayer; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.TextUtils; - -public class PlainText -{ - public static void saveSessionData(ArenaLog log) - { - File dir = new File(MobArena.arenaDir, log.getArena().configName()); - if (!dir.exists()) dir.mkdirs(); - - // General information - List toWrite = new LinkedList(); - toWrite.add("Start: " + log.getStartTime()); - toWrite.add("End: " + log.getEndTime()); - toWrite.add("Duration: " + log.getDuration()); - toWrite.add("Last wave: " + log.getLastWave()); - toWrite.add(" "); - - int classLength = longestClassName(log.distribution.keySet()); - int classCount = log.distribution.keySet().size(); - int playerCount = log.players.keySet().size(); - - // Class distribution - toWrite.add("Class Distribution: " + classCount + " classes"); - toWrite.addAll(getClassDistribution(log.distribution, playerCount, classLength)); - toWrite.add(" "); - - // Player data - toWrite.add("Player Data: " + playerCount + " players"); - toWrite.addAll(getPlayerData(log.players, classLength)); - - // Serialize! - serialize(log, dir, "lastsession.txt", toWrite); - } - - public static void updateArenaTotals(ArenaLog log) - { - // Grab the Configuration - //Configuration totals = Totals.getArenaTotals(log.arena); - - // Parse shit - - - // Serialize - - } - - private static void serialize(ArenaLog log, File dir, String filename, List toWrite) - { - try - { - File file = new File(dir, filename); - if (!file.exists()) file.createNewFile(); - - FileWriter fw = new FileWriter(file); - String linebreak = System.getProperty("line.separator"); - for (String s : toWrite) - { - fw.write(s); - fw.write(linebreak); - } - fw.close(); - } - catch (Exception e) - { - e.printStackTrace(); - MobArena.warning("Problem saving session data for arena '" + log.getArena().configName() + "'"); - } - } - - private static List getClassDistribution(Map map, int playerCount, int classLength) - { - List result = new LinkedList(); - - // Add each entry - class: # (percentage%) - for (Map.Entry entry : map.entrySet()) - { - StringBuffer buffy = new StringBuffer(classLength + 15); - - buffy.append("- "); - buffy.append(TextUtils.padRight(entry.getKey() + ":", classLength + 3)); // : - buffy.append(TextUtils.padLeft(entry.getValue().toString(), 2)); // - buffy.append("(" + (entry.getValue()*100/playerCount) + "%)"); // (%) - - result.add(buffy.toString()); - } - - return result; - } - - private static List getPlayerData(Map map, int classLength) - { - List result = new LinkedList(); - String pad = " "; - - // Padding variables; +2 for extra padding - int NAME = 12, CLASS = classLength, WAVE = 4, KILLS = 5, DMGDONE = 7, DMGTAKEN = 8, ACCURACY = 8; - result.add(" " + TextUtils.padRight("NAME", NAME+2) + - TextUtils.padRight("CLASS", CLASS+2) + - TextUtils.padRight("WAVE", WAVE+2) + - TextUtils.padRight("KILLS", KILLS+2) + - TextUtils.padRight("DMGDONE", DMGDONE+2) + - TextUtils.padRight("DMGTAKEN", DMGTAKEN+2) + - TextUtils.padRight("ACCURACY", ACCURACY+2) + - "REWARDS"); - - // Add each player - name class wave dmgdone dmgtaken accuracy rewards - for (Map.Entry entry : map.entrySet()) - { - Player p = entry.getKey(); - ArenaPlayer ap = entry.getValue(); - StringBuffer buffy = new StringBuffer(80); - - String name = (p.getName().length() <= NAME) ? p.getName() : p.getName().substring(0, NAME+1); - buffy.append("- "); - buffy.append(TextUtils.padRight(name, NAME)); buffy.append(pad); - buffy.append(TextUtils.padRight(ap.getClassName(), CLASS)); buffy.append(pad); - buffy.append(TextUtils.padLeft(ap.getStats().lastWave, WAVE)); buffy.append(pad); - buffy.append(TextUtils.padLeft(ap.getStats().kills, KILLS)); buffy.append(pad); - buffy.append(TextUtils.padLeft(ap.getStats().dmgDone, DMGDONE)); buffy.append(pad); - buffy.append(TextUtils.padLeft(ap.getStats().dmgTaken, DMGTAKEN)); buffy.append(pad); - buffy.append(TextUtils.padLeft(((ap.getStats().swings != 0) ? ap.getStats().hits*100/ap.getStats().swings : 0), ACCURACY-1)); buffy.append("%"); buffy.append(pad); - buffy.append(MAUtils.listToString(ap.rewards)); - - result.add(buffy.toString()); - } - - return result; - } - - private static int longestClassName(Collection names) - { - int result = 0; - - for (String c : names) - if (c.length() > result) - result = c.length(); - - return result; - } -} diff --git a/src/com/garbagemule/MobArena/util/data/Totals.java b/src/com/garbagemule/MobArena/util/data/Totals.java deleted file mode 100644 index 087ac8d..0000000 --- a/src/com/garbagemule/MobArena/util/data/Totals.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.garbagemule.MobArena.util.data; - -import java.io.File; - -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.ArenaLog; -import com.garbagemule.MobArena.ArenaPlayer; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.FileUtils; - -public class Totals -{ - private static final String totals_yml = "totals.yml"; - - /** - * Update the totals-file with the session data from the ArenaLog object - * @param log ArenaLog to update with - */ - public static void updateArenaTotals(ArenaLog log) - { - // Grab the configuration - Configuration totals = getArenaTotals(log.arena); - totals.load(); - - // General data - updateInt(totals, "general-info.total-games-played", 1, true); - updateInt(totals, "general-info.most-players", log.players.keySet().size(), false); - updateInt(totals, "general-info.highest-wave-reached", log.lastWave, false); - updateInt(totals, "general-info.total-monsters-killed", countKills(log), true); - updateDuration(totals, "general-info.total-duration", log.getDurationLong(), true); - updateDuration(totals, "general-info.longest-session-duration", log.getDurationLong(), false); - - // Classes - for (String c : log.arena.getClasses()) - { - // Array {kills, dmgDone, dmgTaken} - int[] a = getKillsAndDamageByClass(log, c); - updateInt(totals, "classes." + c + ".kills", a[0], true); - updateInt(totals, "classes." + c + ".damage-done", a[1], true); - updateInt(totals, "classes." + c + ".damage-taken", a[2], true); - updateInt(totals, "classes." + c + ".played", log.distribution.get(c), true); - } - - // Rewards - for (ArenaPlayer ap : log.players.values()) - { - for (ItemStack stack : ap.rewards) - { - boolean money = stack.getTypeId() == MobArena.ECONOMY_MONEY_ID; - updateInt(totals, "rewards." + (money ? "money" : stack.getType().toString().toLowerCase()), stack.getAmount(), true); - } - } - - // Players - for (ArenaPlayer ap : log.players.values()) - { - // Basic values - updateInt(totals,"players." + ap.player.getName() + ".games-played", 1, true); - updateInt(totals,"players." + ap.player.getName() + ".kills", ap.getStats().kills, true); - updateInt(totals,"players." + ap.player.getName() + ".damage-done", ap.getStats().dmgDone, true); - updateInt(totals,"players." + ap.player.getName() + ".damage-taken", ap.getStats().dmgTaken, true); - updateInt(totals,"players." + ap.player.getName() + ".swings", ap.getStats().swings, true); - updateInt(totals,"players." + ap.player.getName() + ".hits", ap.getStats().hits, true); - - // Class count - updateInt(totals,"players." + ap.player.getName() + ".classes." + ap.className,1,true); - } - - // Save everything - totals.save(); - } - - /** - * Get or create a Configuration from the totals-file - * @param arena Arena to get or create a Configuration for - * @return Configuration from the arena's totals-file - */ - public static Configuration getArenaTotals(Arena arena) - { - // Create the folder if it doesn't exist. - File dir = new File(MobArena.arenaDir, arena.configName()); - if (!dir.exists()) dir.mkdirs(); - - File file = new File(dir, totals_yml); - if (!file.exists()) FileUtils.extractFile(dir, totals_yml); - - // Grab the totals-file and return the Configuration - return new Configuration(file); - } - - /** - * Update an integer in a config-file - * @param totals Configuration to alter - * @param node Node to alter - * @param b Integer for comparison - * @param increment If true, the node will be incremented by b, otherwise overwritten if greater - */ - private static void updateInt(Configuration totals, String node, int b, boolean increment) - { - int a = totals.getInt(node, 0); - - if (increment) totals.setProperty(node, a+b); - else if (b > a) totals.setProperty(node, b); - } - - /** - * Update a duration in a config-file - * @param totals Configuration to alter - * @param node Node to alter - * @param b Duration for comparison. This is a java.sql.Timestamp.getTime() long - * @param increment If true, the node will be incremented by b, otherwise overwritten if greater - */ - private static void updateDuration(Configuration totals, String node, long b, boolean increment) - { - long a = MAUtils.parseDuration(totals.getString(node, "0:00:00")); - - if (increment) totals.setProperty(node, MAUtils.getDuration(a+b)); - else if (b > a) totals.setProperty(node, MAUtils.getDuration(b)); - } - - /** - * Get total kills from an arena session - * @param log The ArenaLog to count kills in - * @return Total kills - */ - private static int countKills(ArenaLog log) - { - int kills = 0; - for (ArenaPlayer ap : log.players.values()) - kills += ap.getStats().kills; - return kills; - } - - /** - * Get a (dirty) int-array in the form {kills, damage done, damage taken} for a class - * @param log ArenaLog to count kills, damage done/taken from - * @param className The class to count for - * @return Resulting kills, damage done and damage taken in an int-array - */ - private static int[] getKillsAndDamageByClass(ArenaLog log, String className) - { - int kills = 0, dmgDone = 0, dmgTaken = 0; - for (ArenaPlayer ap : log.players.values()) - { - if (!ap.className.equals(className)) - continue; - - kills += ap.getStats().kills; - dmgDone += ap.getStats().dmgDone; - dmgTaken += ap.getStats().dmgTaken; - } - return new int[]{kills, dmgDone, dmgTaken}; - } -} diff --git a/src/com/garbagemule/MobArena/util/data/XML.java b/src/com/garbagemule/MobArena/util/data/XML.java deleted file mode 100644 index a0502e0..0000000 --- a/src/com/garbagemule/MobArena/util/data/XML.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.garbagemule.MobArena.util.data; - -import java.io.File; -import java.io.FileOutputStream; -import java.util.List; -import java.util.Map; - -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.config.Configuration; -import org.jdom.Attribute; -import org.jdom.Document; -import org.jdom.Element; -//import org.jdom.Namespace; -import org.jdom.output.XMLOutputter; - -import com.garbagemule.MobArena.ArenaLog; -import com.garbagemule.MobArena.ArenaPlayer; -import com.garbagemule.MobArena.MobArena; - -public class XML -{ - public static void saveSessionData(ArenaLog log) - { - File dir = new File(MobArena.arenaDir, log.getArena().configName()); - if (!dir.exists()) dir.mkdirs(); - - // It's here if people want it. - //Namespace w = Namespace.getNamespace("com.garbagemule.MobArena"); - - // General info - Element ge = new Element("general-info"); - ge.addContent(new Element("start-time").addContent(log.getStartTime().toString())); - ge.addContent(new Element("end-time").addContent(log.getEndTime().toString())); - ge.addContent(new Element("duration").addContent(log.getDuration().toString())); - ge.addContent(new Element("last-wave").addContent(log.getLastWave() + "")); - - // Class distribution - Element cd = new Element("class-distribution"); - for (Map.Entry entry : log.distribution.entrySet()) - cd.addContent(new Element(entry.getKey()).addContent(entry.getValue() + "")); - - // Player data - Element pd = new Element("player-data"); - for (Map.Entry entry : log.players.entrySet()) - { - // Name as attribute - Element p = new Element("player").setAttribute(new Attribute("name", entry.getKey().getName())); - ArenaPlayer ap = entry.getValue(); - - p.addContent(new Element("last-wave").addContent(ap.getStats().lastWave + "")); - p.addContent(new Element("kills").addContent(ap.getStats().lastWave + "")); - p.addContent(new Element("damage-done").addContent(ap.getStats().dmgDone + "")); - p.addContent(new Element("damage-taken").addContent(ap.getStats().dmgTaken + "")); - p.addContent(new Element("swings").addContent(ap.getStats().swings + "")); - p.addContent(new Element("hits").addContent(ap.getStats().hits + "")); - - // Rewards - Element rw = new Element("rewards"); - for (ItemStack stack : ap.rewards) - rw.addContent(getReward(stack)); - - p.addContent(rw); - pd.addContent(p); - } - - // Add the main nodes - Element root = new Element("last-session"); - root.addContent(ge); - root.addContent(cd); - root.addContent(pd); - - // Create a new document - Document doc = new Document(); - doc.addContent(root); - - // Serialize! - serialize(log, dir, doc, "lastsession.xml"); - } - - public static void updateArenaTotals(ArenaLog log) - { - Configuration totals = Totals.getArenaTotals(log.arena); - totals.load(); - - // General data - Element gd = new Element("general-info"); - gd.addContent(new Element("total-games-played").addContent(totals.getInt("general-info.total-games-played", 0) + "")); - gd.addContent(new Element("most-players").addContent(totals.getInt("general-info.most-players", 0) + "")); - gd.addContent(new Element("highest-wave-reached").addContent(totals.getInt("general-info.highest-wave-reached", 0) + "")); - gd.addContent(new Element("total-monsters-killed").addContent(totals.getInt("general-info.total-monsters-killed", 0) + "")); - gd.addContent(new Element("total-duration").addContent(totals.getString("general-info.total-duration", "0:00:00"))); - gd.addContent(new Element("longest-session-duration").addContent(totals.getString("general-info.longest-session-duration", "0:00:00"))); - - // Classes - Element cl = new Element("classes"); - for (String c : log.arena.getClasses()) - { - Element e = new Element("class").setAttribute(new Attribute("name", c)); - e.addContent(new Element("kills").addContent(totals.getInt("classes." + c + ".kills", 0) + "")); - e.addContent(new Element("damage-done").addContent(totals.getInt("classes." + c + ".damage-done", 0) + "")); - e.addContent(new Element("damage-taken").addContent(totals.getInt("classes." + c + ".damage-taken", 0) + "")); - cl.addContent(e); - } - - // Rewards - Element rw = new Element("rewards"); - for (ArenaPlayer ap : log.players.values()) - for (ItemStack stack : ap.rewards) - rw.addContent(getReward(stack)); - - // Players - Element pl = new Element("players"); - for (ArenaPlayer ap : log.players.values()) - { - String name = ap.player.getName(); - Element p = new Element("player").setAttribute(new Attribute("name", name)); - p.addContent(new Element("games-played").addContent(totals.getInt("players." + name + ".games-played", 1) + "")); - p.addContent(new Element("kills").addContent(totals.getInt("players." + name + ".kills", 1) + "")); - p.addContent(new Element("damage-done").addContent(totals.getInt("players." + name + ".damage-done", 1) + "")); - p.addContent(new Element("damage-taken").addContent(totals.getInt("players." + name + ".damage-taken", 1) + "")); - p.addContent(new Element("swings").addContent(totals.getInt("players." + name + ".swings", 1) + "")); - p.addContent(new Element("hits").addContent(totals.getInt("players." + name + ".hits", 1) + "")); - - List classes = totals.getKeys("players." + name + ".classes"); - Element pcl = new Element("classes"); - if (classes != null) - { - for (String c : classes) - pcl.addContent(new Element("class").setAttribute(new Attribute("name", c)).addContent(totals.getInt("players." + name + ".classes." + c, 0) + "")); - } - - p.addContent(pcl); - pl.addContent(p); - } - - // Add the main nodes - Element root = new Element("last-session"); - root.addContent(gd); - root.addContent(cl); - root.addContent(rw); - root.addContent(pl); - - // Create a new document - Document doc = new Document(); - doc.addContent(root); - - // Serialize! - File dir = new File(MobArena.arenaDir, log.getArena().configName()); - if (!dir.exists()) dir.mkdirs(); - serialize(log, dir, doc, "totals.xml"); - } - - private static Element getReward(ItemStack stack) - { - boolean money = stack.getTypeId() == MobArena.ECONOMY_MONEY_ID; - - Element result = new Element("reward"); - result.setAttribute(new Attribute("id", stack.getTypeId() + "")); - result.setAttribute(new Attribute("material", money ? "money" : stack.getType().toString().toLowerCase())); - result.setAttribute(new Attribute("data", money || stack.getData() == null ? "0" : stack.getData().toString().toLowerCase())); - result.setAttribute(new Attribute("amount", stack.getAmount() + "")); - - return result; - } - - private static void serialize(ArenaLog log, File dir, Document doc, String filename) - { - try - { - File file = new File(dir, filename); - FileOutputStream fop = new FileOutputStream(file); - XMLOutputter out = new XMLOutputter(); - out.output(doc, fop); - } - catch (Exception e) - { - e.printStackTrace(); - MobArena.warning("Problem saving session data for arena '" + log.getArena().configName() + "'"); - } - } -} diff --git a/src/com/garbagemule/MobArena/util/data/YAML.java b/src/com/garbagemule/MobArena/util/data/YAML.java deleted file mode 100644 index a6d6006..0000000 --- a/src/com/garbagemule/MobArena/util/data/YAML.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.garbagemule.MobArena.util.data; - -import java.io.File; -import java.util.Map; - -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.ArenaLog; -import com.garbagemule.MobArena.ArenaPlayer; -import com.garbagemule.MobArena.MobArena; - -public class YAML -{ - public static void saveSessionData(ArenaLog log) - { - File dir = new File(MobArena.arenaDir, log.getArena().configName()); - if (!dir.exists()) dir.mkdirs(); - - // Make Configuration object - Configuration config = new Configuration(new File(dir, "lastsession.yml")); - config.load(); - - // Reset any existing data - config.setProperty("general-info", null); - config.setProperty("class-distribution", null); - config.setProperty("player-data", null); - - // General information - config.setProperty("general-info.start-time", log.getStartTime()); - config.setProperty("general-info.end-time", log.getEndTime()); - config.setProperty("general-info.duration", log.getDuration()); - config.setProperty("general-info.last-wave", log.getLastWave()); - - // Class distribution - for (Map.Entry entry : log.distribution.entrySet()) - config.setProperty("class-distribution." + entry.getKey(), entry.getValue()); - - // Player data - for (Map.Entry entry : log.players.entrySet()) - { - String p = entry.getKey().getName(); - ArenaPlayer ap = entry.getValue(); - - config.setProperty("player-data." + p + ".last-wave", ap.getStats().lastWave); - config.setProperty("player-data." + p + ".kills", ap.getStats().kills); - config.setProperty("player-data." + p + ".damage-done", ap.getStats().dmgDone); - config.setProperty("player-data." + p + ".damage-taken", ap.getStats().dmgTaken); - config.setProperty("player-data." + p + ".swings", ap.getStats().swings); - config.setProperty("player-data." + p + ".hits", ap.getStats().hits); - for (ItemStack stack : ap.rewards) - { - boolean money = stack.getTypeId() == MobArena.ECONOMY_MONEY_ID; - String path = "player-data." + p + ".rewards." + (money ? "money" : stack.getType().toString().toLowerCase()); - config.setProperty(path, config.getInt(path, 0) + stack.getAmount()); - } - } - - // Save the file - config.save(); - } -} diff --git a/src/com/garbagemule/MobArena/util/inventory/InventoryManager.java b/src/com/garbagemule/MobArena/util/inventory/InventoryManager.java new file mode 100644 index 0000000..f364d9a --- /dev/null +++ b/src/com/garbagemule/MobArena/util/inventory/InventoryManager.java @@ -0,0 +1,198 @@ +package com.garbagemule.MobArena.util.inventory; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; + +public class InventoryManager +{ + private static String EXT = ".inv"; + private File dir; + private Map items, armor; + + public InventoryManager(Arena arena) { + this.dir = new File(arena.getPlugin().getDataFolder(), "inventories"); + this.dir.mkdir(); + + this.items = new HashMap(); + this.armor = new HashMap(); + } + + /** + * Store a player's inventory in memory and in a file. + * @param p a player + * @return true, if the inventory was stored, false if it already existed + */ + public boolean storeInventory(Player p) { + if (items.containsKey(p) && armor.containsKey(p)) { + return false; + } + + // Store the inventory in memory + PlayerInventory inv = p.getInventory(); + ItemStack[] memItems = inv.getContents(); + ItemStack[] memArmor = inv.getArmorContents(); + items.put(p, memItems); + armor.put(p, memArmor); + + // And save it to a file + saveToFile(p); + clearInventory(p); + return true; + } + + /** + * Restore a player's inventory from memory or from a file + * Note that this method is idempotent; calling it multiple times will + * not duplicate player items or anything like that. + * @param p a player + * @return true, if the inventory was restored successfully, false otherwise + */ + public boolean restoreInventory(Player p) { + ItemStack[] memItems = items.remove(p); + ItemStack[] memArmor = armor.remove(p); + + // Restore from inventory if possible + if (memItems != null && memArmor != null) { + PlayerInventory inv = p.getInventory(); + + inv.setContents(memItems); + inv.setArmorContents(memArmor); + + new File(dir, p.getName() + EXT).delete(); + + return true; + } + + // Otherwise, load from file + return loadFromFile(p); + } + + /** + * Clear a player's inventory completely. + * @param p a player + */ + public void clearInventory(Player p) { + PlayerInventory inv = p.getInventory(); + inv.clear(); + inv.setHelmet(null); + inv.setChestplate(null); + inv.setLeggings(null); + inv.setBoots(null); + } + + private boolean saveToFile(Player p) { + File file = new File(dir, p.getName() + EXT); + + // If the file exists, encourage restoring first. + if (file.exists()) { + return false; + } + + try { + SerializableInventory inv = new SerializableInventory(p.getInventory()); + + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + oos.writeObject(inv); + oos.close(); + + return true; + } + catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private boolean loadFromFile(Player p) { + File file = new File(dir, p.getName() + EXT); + + // If the file doesn't exist, we can't restore from it! + if (!file.exists()) { + return false; + } + + try { + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + Object o = ois.readObject(); + ois.close(); + + SerializableInventory inv = (SerializableInventory) o; + SerializableInventory.loadContents(p, inv); + + file.delete(); + + return true; + } + catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static boolean hasEmptyInventory(Player p) { + ItemStack[] inventory = p.getInventory().getContents(); + ItemStack[] armor = p.getInventory().getArmorContents(); + + // For inventory, check for null + for (ItemStack stack : inventory) { + if (stack != null) + return false; + } + + // For armor, check for id 0, or AIR + for (ItemStack stack : armor) { + if (stack.getTypeId() != 0) + return false; + } + + return true; + } + + /** + * Restore a player's inventory from file + * @param plugin MobArena instance + * @param p a player + * @return true, if the inventory was restored successfully, false otherwise + */ + public static boolean restoreFromFile(MobArena plugin, Player p) { + File dir = new File(plugin.getDataFolder(), "inventories"); + + File file = new File(dir, p.getName() + EXT); + + // If the file doesn't exist, we can't restore from it! + if (!file.exists()) { + return false; + } + + try { + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + Object o = ois.readObject(); + ois.close(); + + SerializableInventory inv = (SerializableInventory) o; + SerializableInventory.loadContents(p, inv); + + file.delete(); + + return true; + } + catch (Exception e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/com/garbagemule/MobArena/util/inventory/InventoryUtils.java b/src/com/garbagemule/MobArena/util/inventory/InventoryUtils.java new file mode 100644 index 0000000..3033ab0 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/inventory/InventoryUtils.java @@ -0,0 +1,35 @@ +package com.garbagemule.MobArena.util.inventory; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.inventory.ItemStack; + +public class InventoryUtils +{ + public static SerializableItem parseItemStack(ItemStack stack) { + return SerializableItem.parseSerializableItem(stack); + } + + public static SerializableItem[] parseItemStacks(ItemStack... stacks) { + SerializableItem[] items = new SerializableItem[stacks.length]; + + for (int i = 0; i < items.length; i++) { + items[i] = parseItemStack(stacks[i]); + } + + return items; + } + + public static List extractAll(int id, List items) { + List result = new ArrayList(); + + for (ItemStack stack : items) { + if (stack.getTypeId() == id) { + result.add(stack); + } + } + + return result; + } +} diff --git a/src/com/garbagemule/MobArena/util/inventory/SerializableInventory.java b/src/com/garbagemule/MobArena/util/inventory/SerializableInventory.java new file mode 100644 index 0000000..4ca6912 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/inventory/SerializableInventory.java @@ -0,0 +1,73 @@ +package com.garbagemule.MobArena.util.inventory; + +import java.io.Serializable; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +public class SerializableInventory implements Serializable +{ + private static final long serialVersionUID = 5816986232508437560L; + + private SerializableItem[] items, armor; + + public SerializableInventory(PlayerInventory inv) { + ItemStack[] stackItems = inv.getContents(); + ItemStack[] stackArmor = inv.getArmorContents(); + + this.items = new SerializableItem[stackItems.length]; + this.armor = new SerializableItem[stackArmor.length]; + + for (int i = 0; i < stackItems.length; i++) { + this.items[i] = SerializableItem.parseSerializableItem(stackItems[i]); + } + + for (int i = 0; i < stackArmor.length; i++) { + this.armor[i] = SerializableItem.parseSerializableItem(stackArmor[i]); + } + } + + public SerializableInventory(Inventory inv) { + ItemStack[] stacks = inv.getContents(); + this.items = new SerializableItem[stacks.length]; + + for (int i = 0; i < stacks.length; i++) { + this.items[i] = SerializableItem.parseSerializableItem(stacks[i]); + } + } + + public ItemStack[] getItems() { + ItemStack[] result = new ItemStack[items.length]; + for (int i = 0; i < items.length; i++) { + result[i] = (items[i] == null ? null : items[i].toItemStack()); + } + return result; + } + + public ItemStack[] getArmor() { + ItemStack[] result = new ItemStack[armor.length]; + for (int i = 0; i < armor.length; i++) { + result[i] = (armor[i] == null ? null : armor[i].toItemStack()); + } + return result; + } + + public static void loadContents(Player p, SerializableInventory inv) { + if (inv == null) { + return; + } + + p.getInventory().setContents(inv.getItems()); + p.getInventory().setArmorContents(inv.getArmor()); + } + + public static void loadContents(Inventory chestInv, SerializableInventory inv) { + if (inv == null) { + return; + } + + chestInv.setContents(inv.getItems()); + } +} diff --git a/src/com/garbagemule/MobArena/util/inventory/SerializableItem.java b/src/com/garbagemule/MobArena/util/inventory/SerializableItem.java new file mode 100644 index 0000000..25470d0 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/inventory/SerializableItem.java @@ -0,0 +1,55 @@ +package com.garbagemule.MobArena.util.inventory; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.MaterialData; + +public class SerializableItem implements Serializable +{ + private static final long serialVersionUID = -2855528738291283052L; + + private int id; + private int amount; + private short damage; + private Byte data; + private Map enchantments; + + private SerializableItem(ItemStack stack) { + this.id = stack.getTypeId(); + this.amount = stack.getAmount(); + this.damage = stack.getDurability(); + + MaterialData md = stack.getData(); + this.data = (md == null ? null : md.getData()); + + this.enchantments = new HashMap(); + for (Entry entry : stack.getEnchantments().entrySet()) { + this.enchantments.put(entry.getKey().getId(), entry.getValue()); + } + } + + public ItemStack toItemStack() { + ItemStack stack = new ItemStack(id, amount, damage, data); + + if (!enchantments.isEmpty()) { + for (Entry entry : this.enchantments.entrySet()) { + stack.addUnsafeEnchantment(Enchantment.getById(entry.getKey()), entry.getValue()); + } + } + + return stack; + } + + public static SerializableItem parseSerializableItem(ItemStack stack) { + if (stack == null) { + return null; + } + + return new SerializableItem(stack); + } +} diff --git a/src/com/garbagemule/MobArena/waves/AbstractWave.java b/src/com/garbagemule/MobArena/waves/AbstractWave.java index d42918b..652bb33 100644 --- a/src/com/garbagemule/MobArena/waves/AbstractWave.java +++ b/src/com/garbagemule/MobArena/waves/AbstractWave.java @@ -2,226 +2,125 @@ package com.garbagemule.MobArena.waves; import java.util.List; import java.util.Map; -import java.util.Random; import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Creature; -import org.bukkit.entity.LivingEntity; -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.util.WaveUtils; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.enums.*; public abstract class AbstractWave implements Wave { - private Arena arena; - private World world; - private String waveName; - private int wave, frequency, priority; + private String name; + + private WaveBranch branch; // recurrent, single + private WaveType type; // default, special, swarm, boss + private double healthMultiplier, amountMultiplier; - private WaveBranch branch; - private WaveType type; - private WaveGrowth growth; - /** - * Basic wave constructor. - * @param name The config-file identifier - * @param wave Initial wave number. This is the first wave number this wave can spawn at. - * @param frequency How often the wave can spawn. - * @param priority The priority of the wave. - * @param branch The branch type (single, recurrent) - */ - public AbstractWave(Arena arena, String waveName, int wave, int frequency, int priority, WaveBranch branch) - { - this.arena = arena; - this.world = arena.getWorld(); - this.waveName = waveName; - this.wave = wave; - this.frequency = frequency; - this.priority = priority; - this.branch = branch; - } + private int firstWave, frequency, priority; - // Default recurrent wave constructor - public AbstractWave(Arena arena, String name, int wave, int frequency, int priority) - { - this(arena, name, wave, frequency, priority, WaveBranch.RECURRENT); - } + private List spawnpoints; - // Default single wave constructor - public AbstractWave(Arena arena, String name, int wave) - { - this(arena, name, wave, 0, 0, WaveBranch.SINGLE); - } - - /** - * Check if a wave matches a wave number. - * SINGLE WAVES match, if their wave number is the same as the - * parameter. - * RECURRENT WAVES match, if their wave number subtracted from - * the parameter divides the frequency. The wave number must be - * greater than or equal to the parameter. - * @param wave The wave number to compare - * @return true, if the wave matches the wave number - */ - public boolean matches(int wave) - { - if (branch == WaveBranch.SINGLE) - return this.wave == wave; - - if (branch == WaveBranch.RECURRENT && wave >= this.wave) - return ((wave - this.wave) % frequency == 0); - - return false; - } - - /** - * Spawn an MACreature in the given location. The actual LivingEntity - * spawned is returned. - * @param creature The MACreature to spawn - * @param loc The location to spawn the MACreature in - * @return The resulting LivingEntity - */ - public LivingEntity spawnMonster(MACreature creature, Location loc) - { - // Spawn - LivingEntity e = creature.spawn(getArena(), getWorld(), loc); - if (e == null) return null; - - // Add to collection - getArena().addMonster(e); - - // Boost health - int health = (int) Math.min(150D, e.getHealth() * healthMultiplier); - e.setHealth(Math.max(1, health)); + @Override + public abstract Map getMonstersToSpawn(int wave, int playerCount, Arena arena); - // Grab a random target. - if (e instanceof Creature) - { - Creature c = (Creature) e; - c.setTarget(WaveUtils.getClosestPlayer(getArena(), e)); - } + @Override + public List getSpawnpoints(Arena arena) { + return WaveUtils.getValidSpawnpoints(arena, spawnpoints, arena.getPlayersInArena()); + } + + @Override + public void setSpawnpoints(List spawnpoints) { + this.spawnpoints = spawnpoints; + } + + @Override + public void announce(Arena arena, int wave) { + type.announce(arena, wave); + } - return e; + @Override + public String getName() { + return name; } - /** - * Helper method for spawning a bunch of monsters over a - * collection of spawnpoints. Used by wave types DEFAULT, - * SPECIAL and SWARM. - * @param monsters A collection of monsters to spawn, and how many of each - * @param spawnpoints The spawnpoints to spawn the monsters over - */ - public void spawnAll(Map monsters, List spawnpoints) - { - Random random = new Random(); - int spawnpointCount = spawnpoints.size(); - int index = random.nextInt(spawnpointCount); - - for (Map.Entry entry : monsters.entrySet()) - { - for (int i = 0; i < entry.getValue(); i++) - { - spawnMonster(entry.getKey(), spawnpoints.get(index % spawnpointCount)); - index++; - } - } + @Override + public void setName(String name) { + this.name = name; } - - // GETTERS - public Arena getArena() - { - return arena; - } - - public World getWorld() - { - return world; - } - - public WaveBranch getBranch() - { + + @Override + public WaveBranch getBranch() { return branch; } - public WaveType getType() - { + @Override + public void setBranch(WaveBranch branch) { + this.branch = branch; + } + + @Override + public WaveType getType() { return type; } - - public WaveGrowth getGrowth() - { - return growth; + + @Override + public void setType(WaveType type) { + this.type = type; } - public int getWave() - { - return wave; + @Override + public int getFirstWave() { + return firstWave; } - public int getFrequency() - { + @Override + public void setFirstWave(int firstWave) { + this.firstWave = firstWave; + } + + @Override + public int getFrequency() { return frequency; } - public int getPriority() - { + @Override + public void setFrequency(int frequency) { + this.frequency = frequency; + } + + @Override + public int getPriority() { return priority; } - - public double getHealthMultiplier() - { + + @Override + public void setPriority(int priority) { + this.priority = priority; + } + + @Override + public double getHealthMultiplier() { return healthMultiplier; } - public double getAmountMultiplier() - { + @Override + public void setHealthMultiplier(double healthMultiplier) { + this.healthMultiplier = healthMultiplier; + } + + @Override + public double getAmountMultiplier() { return amountMultiplier; } - public String getName() - { - return waveName; + @Override + public void setAmountMultiplier(double amountMultiplier) { + this.amountMultiplier = amountMultiplier; } - // SETTERS - public void setBranch(WaveBranch branch) - { - this.branch = branch; - } - - public void setType(WaveType type) - { - this.type = type; - } - - public void setGrowth(WaveGrowth growth) - { - this.growth = growth; - } - - public void setHealthMultiplier(double value) - { - healthMultiplier = Math.max(0.1D, value); - } - - public void setAmountMultiplier(double value) - { - amountMultiplier = Math.max(0.1D, value); - } - - // MISC - public String toString() - { - if (branch == WaveBranch.RECURRENT) - return "[Wave type=" + type + - " name=" + waveName + - " branch=" + branch.toString().charAt(0) + - " freq=" + frequency + - " prio=" + priority + "]"; - return "[Wave type=" + type + - " name=" + waveName + - " branch=" + branch.toString().charAt(0) + - " wave=" + wave + "]"; + @Override + public boolean matches(int wave) { + return branch.matches(wave, this); } } diff --git a/src/com/garbagemule/MobArena/waves/BossAbility.java b/src/com/garbagemule/MobArena/waves/BossAbility.java index fc36abb..c7223dd 100644 --- a/src/com/garbagemule/MobArena/waves/BossAbility.java +++ b/src/com/garbagemule/MobArena/waves/BossAbility.java @@ -1,11 +1,11 @@ package com.garbagemule.MobArena.waves; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Random; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Creature; @@ -15,10 +15,9 @@ import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import com.garbagemule.MobArena.Arena; import com.garbagemule.MobArena.MAUtils; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.WaveUtils; +import com.garbagemule.MobArena.framework.Arena; public enum BossAbility @@ -37,6 +36,7 @@ public enum BossAbility Location bLoc = boss.getLocation(); Location loc = bLoc.add(bLoc.getDirection().normalize().multiply(2).toLocation(boss.getWorld(), bLoc.getYaw(), bLoc.getPitch())); Fireball fireball = boss.getWorld().spawn(loc, Fireball.class); + fireball.setFireTicks(100); fireball.setIsIncendiary(false); } }, @@ -45,7 +45,7 @@ public enum BossAbility public void run(Arena arena, LivingEntity boss) { for (Player p : getNearbyPlayers(arena, boss, 5)) - p.setFireTicks(20); + p.setFireTicks(20); } }, LIGHTNINGAURA("Lightning Aura") @@ -66,8 +66,7 @@ public enum BossAbility }, LIVINGBOMB("Living Bomb") { - public void run(final Arena arena, LivingEntity boss) - { + public void run(final Arena arena, LivingEntity boss) { final LivingEntity target = getTarget(boss); if (target == null) return; @@ -75,19 +74,24 @@ public enum BossAbility target.setFireTicks(60); // Create an explosion after 3 seconds - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), - new Runnable() - { - public void run() - { - if (!arena.getLivingPlayers().contains(target)) + arena.getPlugin().getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), + new Runnable() { + public void run() { + if (!arena.inArena((Player) target)) { return; + } + + // If the player put out the fire, don't explode. + if (target.getFireTicks() <= 0) { + return; + } arena.getWorld().createExplosion(target.getLocation(), 2F); - for(Player p : getNearbyPlayers(arena, target, 3)) + for (Player p : getNearbyPlayers(arena, target, 3)) { p.setFireTicks(40); + } } - }, 61); + }, 59); } }, CHAINLIGHTNING("Chain Lightning") @@ -102,12 +106,12 @@ public enum BossAbility private void strikeLightning(final Arena arena, final Player p, final List done) { - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), + arena.getPlugin().getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), new Runnable() { public void run() { - if (!arena.getLivingPlayers().contains(p)) + if (!arena.getPlayersInArena().contains(p)) return; // Smite the target @@ -135,10 +139,32 @@ public enum BossAbility if (target == null) return; Location loc = target.getLocation(); - loc.setYaw(target.getLocation().getYaw() + 45 + MobArena.random.nextInt(270)); + loc.setYaw(loc.getYaw() + 45 + MobArena.random.nextInt(270)); target.teleport(loc); } }, + DISORIENTNEARBY("Disorient Nearby") + { + public void run(Arena arena, LivingEntity boss) + { + for (Player p : getNearbyPlayers(arena, boss, 5)) { + Location loc = p.getLocation(); + loc.setYaw(loc.getYaw() + 45 + MobArena.random.nextInt(270)); + p.teleport(loc); + } + } + }, + DISORIENTDISTANT("Disorient Distant") + { + public void run(Arena arena, LivingEntity boss) + { + for (Player p : getDistantPlayers(arena, boss, 8)) { + Location loc = p.getLocation(); + loc.setYaw(loc.getYaw() + 45 + MobArena.random.nextInt(270)); + p.teleport(loc); + } + } + }, ROOTTARGET("Root Target") { public void run(final Arena arena, LivingEntity boss) @@ -147,22 +173,22 @@ public enum BossAbility if (target == null) return; final Location loc = target.getLocation(); - final int freezeTaskId = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(arena.getPlugin(), + final int freezeTaskId = arena.getPlugin().getServer().getScheduler().scheduleSyncRepeatingTask(arena.getPlugin(), new Runnable() { public void run() { - if (arena.getLivingPlayers().contains(target)) + if (arena.getPlayersInArena().contains(target)) target.teleport(loc); } }, 3, 3); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), + arena.getPlugin().getServer().getScheduler().scheduleSyncDelayedTask(arena.getPlugin(), new Runnable() { public void run() { - Bukkit.getServer().getScheduler().cancelTask(freezeTaskId); + arena.getPlugin().getServer().getScheduler().cancelTask(freezeTaskId); } }, 45); } @@ -171,7 +197,7 @@ public enum BossAbility { public void run(Arena arena, LivingEntity boss) { - List list = arena.getLivingPlayers(); + List list = new ArrayList(arena.getPlayersInArena()); boss.teleport(list.get((new Random()).nextInt(list.size()))); } }, @@ -180,7 +206,7 @@ public enum BossAbility public void run(Arena arena, LivingEntity boss) { // Grab the players and add the boss - List entities = new LinkedList(arena.getArenaPlayers()); + List entities = new ArrayList(arena.getPlayersInArena()); entities.add(boss); // Grab the locations @@ -198,7 +224,7 @@ public enum BossAbility { public void run(Arena arena, LivingEntity boss) { - List players = arena.getLivingPlayers(); + List players = new ArrayList(arena.getPlayersInArena()); Block block = players.get(MobArena.random.nextInt(players.size())).getLocation().getBlock(); if (block.getTypeId() == 0) { @@ -319,7 +345,7 @@ public enum BossAbility { List result = new LinkedList(); for (Entity e : boss.getNearbyEntities(x, x, x)) - if (arena.getLivingPlayers().contains(e)) + if (arena.getPlayersInArena().contains(e)) result.add((Player) e); return result; } @@ -334,19 +360,17 @@ public enum BossAbility protected List getDistantPlayers(Arena arena, Entity boss, int x) { List result = new LinkedList(); - for (Player p : arena.getLivingPlayers()) - if (MAUtils.distanceSquared(p, boss.getLocation()) > (double) (x*x)) + for (Player p : arena.getPlayersInArena()) + if (MAUtils.distanceSquared(arena.getPlugin(), p, boss.getLocation()) > (double) (x*x)) result.add(p); return result; } - public static BossAbility fromString(String string) - { + public static BossAbility fromString(String string) { return WaveUtils.getEnumFromString(BossAbility.class, string.replaceAll("[-_\\.]", "")); } - public String toString() - { + public String toString() { return name; } } diff --git a/src/com/garbagemule/MobArena/waves/BossAbilityThread.java b/src/com/garbagemule/MobArena/waves/BossAbilityThread.java new file mode 100644 index 0000000..47fd489 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/BossAbilityThread.java @@ -0,0 +1,52 @@ +package com.garbagemule.MobArena.waves; + +import java.util.List; +import java.util.Set; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.types.BossWave; + +public class BossAbilityThread implements Runnable +{ + private BossWave wave; + private List abilities; + private Arena arena; + private int counter; + + public BossAbilityThread(BossWave wave, List abilities, Arena arena) { + this.wave = wave; + this.abilities = abilities; + this.arena = arena; + this.counter = 0; + } + + @Override + public void run() { + // If we have no abilities, we can't execute any, so just quit. + if (abilities.isEmpty()) { + return; + } + + // If the arena isn't running or has no players, quit. + if (!arena.isRunning() || arena.getPlayersInArena().isEmpty()) { + return; + } + + // If all bosses are dead, quit. + Set bosses = wave.getMABosses(); + if (bosses.isEmpty()) { + return; + } + + // Get the next ability in the list. + BossAbility ability = abilities.get(counter++ % abilities.size()); + + // And make each boss in this boss wave use it! + for (MABoss boss : bosses) { + ability.run(arena, boss.getEntity()); + } + + // Schedule for another run! + wave.scheduleTask(arena, this, wave.getAbilityInterval()); + } +} diff --git a/src/com/garbagemule/MobArena/waves/BossWave.java b/src/com/garbagemule/MobArena/waves/BossWave.java deleted file mode 100644 index a91c5dd..0000000 --- a/src/com/garbagemule/MobArena/waves/BossWave.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.garbagemule.MobArena.waves; - -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import net.minecraft.server.WorldServer; - -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.entity.CraftEntity; -import org.bukkit.entity.Creature; -import org.bukkit.entity.LivingEntity; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.Msg; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.WaveUtils; - -public class BossWave extends AbstractWave -{ - private MACreature boss; - private LivingEntity bossCreature; - private List abilities; - private Set adds; - private BossHealth bossHealth; - private int healthAmount, abilityTask, abilityInterval; - private boolean lowHealthAnnounced = false, abilityAnnounce; - - // Recurrent - public BossWave(Arena arena, String name, int wave, int frequency, int priority, Config config, String path) - { - super(arena, name, wave, frequency, priority); - load(config, path); - } - - // Single - public BossWave(Arena arena, String name, int wave, Config config, String path) - { - super(arena, name, wave); - load(config, path); - } - - private void load(Config config, String path) - { - setType(WaveType.BOSS); - abilityTask = -1; - abilities = new LinkedList(); - - // Get monster and health - boss = MACreature.fromString(config.getString(path + "monster")); - bossHealth = WaveUtils.getEnumFromString(BossHealth.class, config.getString(path + "health"), BossHealth.MEDIUM); - - // Get abilities - abilityInterval = config.getInt(path + "ability-interval", 3) * 20; - abilityAnnounce = config.getBoolean(path + "ability-announce", true); - String abilities = config.getString(path + "abilities"); - if (abilities != null) - { - for (String a : abilities.split(",")) - { - String ability = a.trim(); - addAbility(BossAbility.fromString(ability)); - } - } - } - - public void spawn(int wave) - { - // Announce spawning - MAUtils.tellAll(getArena(), Msg.WAVE_BOSS, ""+wave); - - // Spawn the boss and set the arena - bossCreature = boss.spawn(getArena(), getWorld(), getArena().getBossSpawnpoint()); - if (bossCreature instanceof Creature) - ((Creature) bossCreature).setTarget(MAUtils.getClosestPlayer(bossCreature, getArena())); - getArena().addMonster(bossCreature); - getArena().setBossWave(this); - - // Set the health stuff - bossCreature.setHealth(bossCreature.getMaxHealth()); - healthAmount = bossHealth.getAmount(getArena().getPlayerCount()); - - startAbilityTasks(); - } - - private void startAbilityTasks() - { - final int abilityCount = abilities.size(); - - // If there are no abilities, don't start the timer. - if (abilityCount == 0) - return; - - abilityTask = Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(getArena().getPlugin(), - new Runnable() - { - private int counter = 0; - - public void run() - { - // Check to see if the boss is still alive. If not, end this boss wave. - if (bossCreature.isDead()) - { - clear(); - return; - } - - // Grab the next ability - BossAbility ability = abilities.get(counter % abilityCount); - - // Announce it - if (abilityAnnounce) - MAUtils.tellAll(getArena(), Msg.WAVE_BOSS_ABILITY, ability.toString()); - - // Activate! - ability.run(getArena(), bossCreature); - - // Increment counter - counter++; - } - }, 100, abilityInterval); - } - - public void cancelAbilityTask() - { - if (abilityTask != -1) - Bukkit.getServer().getScheduler().cancelTask(abilityTask); - } - - public void clear() - { - cancelAbilityTask(); - getArena().setBossWave(null); - - if (bossCreature == null) return; - - CraftEntity ce = (CraftEntity) bossCreature; - CraftWorld cw = (CraftWorld) getWorld(); - WorldServer ws = cw.getHandle(); - Location l = bossCreature.getLocation(); - ws.createExplosion(ce.getHandle(), l.getX(), l.getY() + 1, l.getZ(), 1f, false); - - bossCreature.remove(); - } - - public void addAbility(BossAbility ability) - { - abilities.add(ability); - } - - public void addAdds(Creature creature) - { - adds.add(creature); - } - - public LivingEntity getEntity() - { - return bossCreature; - } - - public int getHealth() - { - return healthAmount; - } - - public void setHealth(int healthAmount) - { - this.healthAmount = healthAmount; - } - - public void subtractHealth(int amount) - { - healthAmount -= amount; - } - - public boolean isLowHealthAnnounced() - { - return lowHealthAnnounced; - } - - public void setLowHealthAnnounced(boolean value) - { - lowHealthAnnounced = value; - } -} diff --git a/src/com/garbagemule/MobArena/waves/DefaultWave.java b/src/com/garbagemule/MobArena/waves/DefaultWave.java deleted file mode 100644 index 34b94c5..0000000 --- a/src/com/garbagemule/MobArena/waves/DefaultWave.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.garbagemule.MobArena.waves; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -import org.bukkit.Location; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.Msg; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.WaveUtils; - -public class DefaultWave extends NormalWave -{ - // Recurrent - public DefaultWave(Arena arena, String name, int wave, int frequency, int priority, Config config, String path) - { - super(arena, name, wave, frequency, priority, config, path); - load(config, path, WaveType.DEFAULT); - } - - // Single - public DefaultWave(Arena arena, String name, int wave, Config config, String path) - { - super(arena, name, wave, config, path); - load(config, path, WaveType.DEFAULT); - } - - /** - * Default waves spawn an amount of random monsters, picked from a - * map of probabilities. The amount to spawn depends on the wave - * number and player count. - */ - public void spawn(int wave) - { - // Announce spawning - MAUtils.tellAll(getArena(), Msg.WAVE_DEFAULT, ""+wave); - - // Get the valid spawnpoints, and initialize counter - List validSpawnpoints = WaveUtils.getValidSpawnpoints(getArena(), getArena().getLivingPlayers()); - - // Initialize the total amount of mobs to spawn - int totalToSpawn = (int) (getGrowth().getAmount(wave, getArena().getPlayerCount()) * getAmountMultiplier()); - - // Spawn all the monsters - spawnAll(getMonstersToSpawn(totalToSpawn), validSpawnpoints); - } - - private Map getMonstersToSpawn(int totalToSpawn) - { - Map result = new HashMap(); - Random random = new Random(); - int randomNumber; - MACreature creature; - - if (totalToSpawn < 1) totalToSpawn = 1; - for (int i = 0; i < totalToSpawn; i++) - { - randomNumber = random.nextInt(getTotalProbability()); - - // Find the monster that corresponds to the random number, and increment its value - for (Map.Entry entry : getProbabilityMap().entrySet()) - { - if (randomNumber > entry.getKey()) continue; - - creature = entry.getValue(); - if (result.containsKey(entry.getValue())) - result.put(creature, result.get(creature) + 1); - else - result.put(creature, 1); - break; - } - } - - return result; - } -} diff --git a/src/com/garbagemule/MobArena/waves/MABoss.java b/src/com/garbagemule/MobArena/waves/MABoss.java new file mode 100644 index 0000000..c598945 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/MABoss.java @@ -0,0 +1,48 @@ +package com.garbagemule.MobArena.waves; + +import org.bukkit.entity.LivingEntity; + +public class MABoss +{ + private LivingEntity entity; + private int health, health25, maxHealth; + private boolean dead, lowHealth; + + public MABoss(LivingEntity entity, int maxHealth) { + this.entity = entity; + this.dead = false; + this.lowHealth = false; + + this.health = this.maxHealth = maxHealth; + this.health25 = maxHealth / 4; + } + + public LivingEntity getEntity() { + return entity; + } + + public int getHealth() { + return health; + } + + public int getMaxHealth() { + return maxHealth; + } + + public void damage(int amount) { + health -= amount; + + if (health <= health25 && !lowHealth) { + lowHealth = true; + //System.out.println("Boss is at 25%!"); + } + + if (health <= 0) { + dead = true; + } + } + + public boolean isDead() { + return dead; + } +} diff --git a/src/com/garbagemule/MobArena/waves/MACreature.java b/src/com/garbagemule/MobArena/waves/MACreature.java index 4d5d6c3..3e0d90d 100644 --- a/src/com/garbagemule/MobArena/waves/MACreature.java +++ b/src/com/garbagemule/MobArena/waves/MACreature.java @@ -2,15 +2,15 @@ package com.garbagemule.MobArena.waves; import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.entity.Creature; import org.bukkit.entity.CreatureType; import org.bukkit.entity.Creeper; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Slime; import org.bukkit.entity.Wolf; -import com.garbagemule.MobArena.Arena; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.util.WaveUtils; +import com.garbagemule.MobArena.framework.Arena; public enum MACreature { @@ -65,29 +65,25 @@ public enum MACreature private CreatureType type; - private MACreature(CreatureType type) - { + private MACreature(CreatureType type) { this.type = type; } - public CreatureType getType() - { + public CreatureType getType() { return type; } - public static MACreature fromString(String string) - { + public static MACreature fromString(String string) { return WaveUtils.getEnumFromString(MACreature.class, string.replaceAll("[-_\\.]", "")); } - public LivingEntity spawn(Arena arena, World world, Location loc) - { + public LivingEntity spawn(Arena arena, World world, Location loc) { LivingEntity e = world.spawnCreature(loc, type); switch (this) { case EXPLODINGSHEEP: - arena.addExplodingSheep(e); + arena.getMonsterManager().addExplodingSheep(e); break; case POWEREDCREEPERS: ((Creeper) e).setPowered(true); @@ -129,6 +125,11 @@ public enum MACreature break; } + if (e instanceof Creature) { + Creature c = (Creature) e; + c.setTarget(WaveUtils.getClosestPlayer(arena, e)); + } + return e; } } diff --git a/src/com/garbagemule/MobArena/waves/NormalWave.java b/src/com/garbagemule/MobArena/waves/NormalWave.java deleted file mode 100644 index 88722d0..0000000 --- a/src/com/garbagemule/MobArena/waves/NormalWave.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.garbagemule.MobArena.waves; - -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.WaveUtils; - -public abstract class NormalWave extends AbstractWave -{ - private int totalProbability = 0; - private Map probabilities = new TreeMap(); - - // Recurrent - public NormalWave(Arena arena, String name, int wave, int frequency, int priority, Config config, String path) - { - super(arena, name, wave, frequency, priority); - } - - // Single - public NormalWave(Arena arena, String name, int wave, Config config, String path) - { - super(arena, name, wave); - } - - /** - * Prepare the wave for spawning by initializing the variables and - * populating the collections needed. - * @param config The config-file - * @param path The absolute path of the wave - * @param type DEFAULT or SPECIAL - */ - public void load(Config config, String path, WaveType type) - { - // Set type and (for DEFAULT) growth. - setType(type); - if (type == WaveType.DEFAULT) - setGrowth(WaveUtils.getEnumFromString(WaveGrowth.class, config.getString(path + "growth"), WaveGrowth.OLD)); - - // Load monsters - int prob; - Set monsters = config.getKeys(path + "monsters"); - if (monsters != null && !monsters.isEmpty()) - { - for (String m : config.getKeys(path + "monsters")) - { - prob = config.getInt(path + "monsters." + m, 1); - if (prob == 0) continue; - - incTotalProbability(prob); - probabilities.put(totalProbability, MACreature.fromString(m)); - } - } - else - { - if (type == WaveType.DEFAULT) - { - probabilities.put(10, MACreature.ZOMBIES); - probabilities.put(20, MACreature.SKELETONS); - probabilities.put(30, MACreature.SPIDERS); - probabilities.put(40, MACreature.CREEPERS); - probabilities.put(50, MACreature.WOLVES); - totalProbability = 50; - } - else if (type == WaveType.SPECIAL) - { - probabilities.put(10, MACreature.POWEREDCREEPERS); - probabilities.put(20, MACreature.ANGRYWOLVES); - probabilities.put(30, MACreature.ZOMBIEPIGMEN); - probabilities.put(40, MACreature.SLIMES); - totalProbability = 40; - } - } - - // Load multipliers - setHealthMultiplier(MAUtils.getDouble(config, path + "health-multiplier", 1D)); - setAmountMultiplier(MAUtils.getDouble(config, path + "amount-multiplier", 1D)); - } - - public int getTotalProbability() - { - return totalProbability; - } - - public void incTotalProbability(int value) - { - totalProbability += value; - } - - public Map getProbabilityMap() - { - return probabilities; - } - - public abstract void spawn(int wave); -} diff --git a/src/com/garbagemule/MobArena/waves/SheepBouncer.java b/src/com/garbagemule/MobArena/waves/SheepBouncer.java new file mode 100644 index 0000000..0895572 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/SheepBouncer.java @@ -0,0 +1,96 @@ +package com.garbagemule.MobArena.waves; + +import java.util.HashSet; +import java.util.Set; + +import net.minecraft.server.WorldServer; + +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import com.garbagemule.MobArena.framework.Arena; + +public class SheepBouncer implements Runnable +{ + public static final int BOUNCE_INTERVAL = 20; + private Arena arena; + private Set sheep; + + public SheepBouncer(Arena arena) { + this.arena = arena; + } + + @Override + public void run() { + // If the arena isn't running or has no players, bail out + if (!arena.isRunning() || arena.getPlayersInArena().isEmpty()) { + return; + } + + // Put all the sheep in a new collection for iteration purposes. + sheep = new HashSet(arena.getMonsterManager().getExplodingSheep()); + + // If there are no sheep, reschedule and return. + if (sheep.isEmpty()) { + arena.scheduleTask(this, BOUNCE_INTERVAL); + return; + } + + for (LivingEntity e : sheep) { + // If an entity is null just ignore it. + if (e == null) { + continue; + } + + // If the sheep is dead, remove it. + if (e.isDead()) { + arena.getMonsterManager().removeMonster(e); + arena.getMonsterManager().removeExplodingSheep(e); + continue; + } + + // Create an explosion if there's a player amongst the nearby entities. + for (Entity entity : e.getNearbyEntities(2D, 2D, 2D)) { + if (entity instanceof Player) { + CraftEntity ce = (CraftEntity) e; + CraftWorld cw = (CraftWorld) e.getWorld(); + WorldServer ws = cw.getHandle(); + + ws.createExplosion(ce.getHandle(), e.getLocation().getX(), e.getLocation().getY() + 1, e.getLocation().getZ(), 2f, false); + e.remove(); + + break; + } + } + + // Otherwise, if it's not already bouncing, BOUNCE! + if (Math.abs(e.getVelocity().getY()) < 1) + e.setVelocity(e.getVelocity().setY(0.5)); + } + + // Reschedule for more bouncy madness! + arena.scheduleTask(this, BOUNCE_INTERVAL); + } + + /*private boolean isTargetNearby(Creature c, LivingEntity t) { + // Null or dead, return false + if (t == null || c == null || c.isDead()) { + return false; + } + + // If the subjects are in different worlds, return false. + if (c.getWorld() != t.getWorld()) { + return false; + } + + // If distance is more than 3 blocks, not close enough. + if (c.getLocation().distanceSquared(t.getLocation()) > 8D) { + return false; + } + + return true; + }*/ +} diff --git a/src/com/garbagemule/MobArena/waves/SpecialWave.java b/src/com/garbagemule/MobArena/waves/SpecialWave.java deleted file mode 100644 index 1bf49d8..0000000 --- a/src/com/garbagemule/MobArena/waves/SpecialWave.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.garbagemule.MobArena.waves; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -import org.bukkit.Location; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.Msg; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.WaveUtils; - -public class SpecialWave extends NormalWave -{ - // Recurrent - public SpecialWave(Arena arena, String name, int wave, int frequency, int priority, Config config, String path) - { - super(arena, name, wave, frequency, priority, config, path); - load(config, path, WaveType.SPECIAL); - } - - // Single - public SpecialWave(Arena arena, String name, int wave, Config config, String path) - { - super(arena, name, wave, config, path); - load(config, path, WaveType.SPECIAL); - } - - public void spawn(int wave) - { - // Announce spawning - MAUtils.tellAll(getArena(), Msg.WAVE_SPECIAL, ""+wave); - - // Get the valid spawnpoints, and initialize counter - List validSpawnpoints = WaveUtils.getValidSpawnpoints(getArena(), getArena().getLivingPlayers()); - - // Strike some lightning! - if (getArena().isLightningEnabled()) - for (Location loc : validSpawnpoints) - getWorld().strikeLightningEffect(loc); - - // Spawn all the monsters - spawnAll(getMonstersToSpawn(getArena().getPlayerCount()), validSpawnpoints); - } - - private Map getMonstersToSpawn(int playerCount) - { - Map result = new HashMap(); - - Random random = new Random(); - int randomNumber = random.nextInt(getTotalProbability()); - for (Map.Entry entry : getProbabilityMap().entrySet()) - { - if (randomNumber > entry.getKey()) continue; - - int amount; - switch (entry.getValue()) - { - case POWEREDCREEPERS: - case ZOMBIEPIGMEN: - case ANGRYWOLVES: amount = playerCount * 2; break; - case SLIMES: amount = playerCount * 4; break; - case GIANTS: - case GHASTS: amount = 2; - default: amount = playerCount + 1; break; - } - - result.put(entry.getValue(), (int) (amount * getAmountMultiplier())); - break; - } - - return result; - } -} diff --git a/src/com/garbagemule/MobArena/waves/SwarmWave.java b/src/com/garbagemule/MobArena/waves/SwarmWave.java deleted file mode 100644 index d23fadc..0000000 --- a/src/com/garbagemule/MobArena/waves/SwarmWave.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.garbagemule.MobArena.waves; - -import java.util.List; - -import org.bukkit.Location; -import org.bukkit.entity.LivingEntity; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.Msg; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.util.WaveUtils; - -public class SwarmWave extends AbstractWave -{ - private MACreature monster; - private SwarmAmount amount; - - // Recurrent - public SwarmWave(Arena arena, String name, int wave, int frequency, int priority, Config config, String path) - { - super(arena, name, wave, frequency, priority); - load(config, path); - } - - // Single - public SwarmWave(Arena arena, String name, int wave, Config config, String path) - { - super(arena, name, wave); - load(config, path); - } - - private void load(Config config, String path) - { - // Set the wave type - setType(WaveType.SWARM); - - // Get the monster type - monster = MACreature.fromString(config.getString(path + "monster")); - - // And the amount - amount = WaveUtils.getEnumFromString(SwarmAmount.class, config.getString(path + "amount"), SwarmAmount.LOW); - - // Load multipliers - setHealthMultiplier(MAUtils.getDouble(config, path + "health-multiplier", 1D)); - setAmountMultiplier(MAUtils.getDouble(config, path + "amount-multiplier", 1D)); - } - - public void spawn(int wave) - { - // Announce spawning - MAUtils.tellAll(getArena(), Msg.WAVE_SWARM, ""+wave); - - // Get the valid spawnpoints, and initialize counter - List validSpawnpoints = WaveUtils.getValidSpawnpoints(getArena(), getArena().getLivingPlayers()); - - // Spawn the hellians! - int toSpawn = (int) (amount.getAmount(getArena().getPlayerCount()) * getAmountMultiplier()); - spawnAll(monster, toSpawn, validSpawnpoints); - } - - public void spawnAll(MACreature monster, int amount, List spawnpoints) - { - int spawnpointCount = spawnpoints.size(); - for (int i = 0; i < amount; i++) - { - LivingEntity e = spawnMonster(monster, spawnpoints.get(i % spawnpointCount)); - if (e == null) continue; - - // Boost health - int health = (int) Math.min(150D, 1 * getHealthMultiplier()); - e.setHealth(Math.max(1, health)); - } - } -} diff --git a/src/com/garbagemule/MobArena/waves/Wave.java b/src/com/garbagemule/MobArena/waves/Wave.java index 49260d8..b7a98c2 100644 --- a/src/com/garbagemule/MobArena/waves/Wave.java +++ b/src/com/garbagemule/MobArena/waves/Wave.java @@ -1,144 +1,136 @@ package com.garbagemule.MobArena.waves; -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.util.WaveUtils; +import java.util.List; +import java.util.Map; + +import org.bukkit.Location; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.enums.*; public interface Wave { - public enum WaveBranch - { - SINGLE, RECURRENT; - } - - public enum WaveType - { - DEFAULT, SPECIAL, SWARM, BOSS; - - public static WaveType fromString(String string) - { - return WaveUtils.getEnumFromString(WaveType.class, string); - } - } - - public enum WaveGrowth - { - OLD(0), SLOW(0.5), MEDIUM(0.65), FAST(0.8), PSYCHO(1.2); - private double exp; - - private WaveGrowth(double exp) - { - this.exp = exp; - } - - public static WaveGrowth fromString(String string) - { - return WaveUtils.getEnumFromString(WaveGrowth.class, string, OLD); - } - - public int getAmount(int wave, int playerCount) - { - if (this == OLD) return wave + playerCount; - - double pc = (double) playerCount; - double w = (double) wave; - - double base = Math.min(Math.ceil(pc/2) + 1, 13); - return (int) ( base * Math.pow(w, exp) ); - } - } - - public enum BossHealth - { - LOW(8), MEDIUM(15), HIGH(25), PSYCHO(40); - private int multiplier; - - private BossHealth(int multiplier) - { - this.multiplier = multiplier; - } - - public int getAmount(int playerCount) - { - return (playerCount + 1) * 20 * multiplier; - } - - public static BossHealth fromString(String string) - { - return WaveUtils.getEnumFromString(BossHealth.class, string); - } - } - - public enum SwarmAmount - { - LOW(10), MEDIUM(20), HIGH(30), PSYCHO(60); - private int multiplier; - - private SwarmAmount(int multiplier) - { - this.multiplier = multiplier; - } - - public int getAmount(int playerCount) - { - return Math.max(1, playerCount / 2) * multiplier; - } - - public static SwarmAmount fromString(String string) - { - return WaveUtils.getEnumFromString(SwarmAmount.class, string); - } - } - /** - * The spawn() method must spawn one or more monsters in - * the arena. The monster count, damage, health, etc. can - * be modified by the wave parameter. - * @param wave Wave number + * Populate a map of which monsters and how many of each this + * wave should spawn, given the wave number, player count and + * an arena object. + * @param wave the current wave number + * @param playerCount the amount of players + * @param arena an Arena + * @return a collection of MACreatures and how many of each to spawn */ - public void spawn(int wave); + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena); /** - * Get the type of wave. - * @return The WaveType of this Wave. + * Get a list of spawnpoints upon which the monsters of this + * wave may be spawned. Note that it is expected that the + * getMonstersToSpawn() method is called first, and that each + * monster is then spawned on a location given in the list + * from this method. + * @param arena an Arena + * @return a list of valid spawnpoints */ - public WaveType getType(); + public List getSpawnpoints(Arena arena); + + /** + * Set the list of spawnpoints on which the monsters of this + * wave may be spawned. If the value is null, all spawnpoints + * of the arena region will be considered. + * @param spawnpoints a list of spawnpoints + */ + public void setSpawnpoints(List spawnpoints); + + /** + * Announce to all players that this wave is spawning. + * @param arena an arena + * @param wave a wave number + */ + public void announce(Arena arena, int wave); + + /** + * Get the wave's name. + * @return The name + */ + public String getName(); + + /** + * Set the name of this wave. + * @param name a name + */ + public void setName(String name); /** * Get the branch type. * @return The WaveBranch of this Wave. */ public WaveBranch getBranch(); + + /** + * Set the type of branch + * @param branch recurrent or single + */ + public void setBranch(WaveBranch branch); /** - * Get the growth rate of this wave. - * @return The growth rate + * Get the type of wave. + * @return a WaveType */ - public WaveGrowth getGrowth(); + public WaveType getType(); + + /** + * Set the type of wave. + * @param type a WaveType + */ + public void setType(WaveType type); /** - * Get the first wave number for this wave - * @return The wave number + * Get the first wave this Wave instance may spawn on. + * @return a wave number */ - public int getWave(); + public int getFirstWave(); + + /** + * Set the first wave this Wave instance may spawn on. + * @param firstWave a wave number + */ + public void setFirstWave(int firstWave); /** * Get the wave's frequency, i.e. wave number "modulo" - * @return The wave's frequency + * @return a frequency */ public int getFrequency(); + /** + * Set the wave's frequency + * @param frequency a frequency + */ + public void setFrequency(int frequency); + /** * Get the wave's priority value. - * @return The priority + * @return a priority */ public int getPriority(); + /** + * Set the wave's priority. + * @param priority a priority + */ + public void setPriority(int priority); + /** * Get the wave's health multiplier. * @return The health multiplier */ public double getHealthMultiplier(); + /** + * Get the wave's health multiplier. + * @param healthMultiplier a double in the range ]0;1] + */ + public void setHealthMultiplier(double healthMultiplier); + /** * Get the wave's amount multiplier. * @return The amount multiplier @@ -146,22 +138,10 @@ public interface Wave public double getAmountMultiplier(); /** - * Get the wave's name. - * @return The name + * Set the wave's amount multiplier. + * @param amountMultiplier a positive double */ - public String getName(); - - /** - * Get the arena of this wave. - * @return The arena - */ - public Arena getArena(); - - /** - * Set the wave's growth - * @param growth How fast the wave will grow - */ - public void setGrowth(WaveGrowth growth); + public void setAmountMultiplier(double amountMultiplier); /** * Check if this wave matches the wave number. diff --git a/src/com/garbagemule/MobArena/waves/WaveManager.java b/src/com/garbagemule/MobArena/waves/WaveManager.java new file mode 100644 index 0000000..590abee --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/WaveManager.java @@ -0,0 +1,137 @@ +package com.garbagemule.MobArena.waves; + +import java.util.SortedSet; +import java.util.TreeSet; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.util.config.Config; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.waves.enums.*; + +public class WaveManager +{ + private Arena arena; + private Config config; + + private Wave defaultWave, currentWave; + private TreeSet recurrentWaves, singleWaves, singleWavesInstance; + + private int wave, finalWave; + + public WaveManager(Arena arena, Config config) { + this.arena = arena; + this.config = config; + this.wave = 0; + this.finalWave = 0; + + reloadWaves(); + } + + public void reload() { + config.load(); + reloadWaves(); + } + + public void save() { + config.save(); + } + + public TreeSet getRecurrentWaves() { + return recurrentWaves; + } + + public void reset() { + wave = 0; + singleWavesInstance = new TreeSet(singleWaves); + } + + public void reloadWaves() { + ConfigSection rConfig = config.getConfigSection("arenas." + arena.configName() + ".waves.recurrent"); + ConfigSection sConfig = config.getConfigSection("arenas." + arena.configName() + ".waves.single"); + + recurrentWaves = WaveParser.parseWaves(arena, rConfig, WaveBranch.RECURRENT); + singleWaves = WaveParser.parseWaves(arena, sConfig, WaveBranch.SINGLE); + + finalWave = config.getInt("arenas." + arena.configName() + ".settings.final-wave", 0); + + if (recurrentWaves.isEmpty()) { + arena.getPlugin().warning(WaveError.NO_RECURRENT_WAVES.format(arena.configName())); + + Wave def = WaveParser.createDefaultWave(); + recurrentWaves.add(def); + } + + defaultWave = recurrentWaves.first(); + } + + /** + * Increment the wave number and get the next Wave to be spawned. + * Note that this method is a mutator. + * @return the next Wave + */ + public Wave next() { + wave++; + + if (!singleWavesInstance.isEmpty() && singleWavesInstance.first().matches(wave)) { + currentWave = singleWavesInstance.pollFirst(); + } + else { + SortedSet matches = getMatchingRecurrentWaves(wave); + currentWave = (matches.isEmpty() ? defaultWave : matches.last()); + } + + return currentWave; + } + + /** + * Get the next Wave to be spawned. This is an accessor and does not + * advance the "counter". Note that the Wave objects, however, are + * mutable. + * @return the next Wave + */ + public Wave getNext() { + int next = wave + 1; + + if (!singleWavesInstance.isEmpty() && singleWavesInstance.first().matches(next)) { + return singleWavesInstance.first(); + } + + SortedSet matches = getMatchingRecurrentWaves(wave); + return (matches.isEmpty() ? defaultWave : matches.last()); + } + + /** + * Get the current wave that's being used. + * Note that the current wave might not have spawned yet. + * @return the current wave + */ + public Wave getCurrent() { + return currentWave; + } + + /** + * Get the current wave number. + * @return the current wave number + */ + public int getWaveNumber() { + return wave; + } + + /** + * Get the final wave number. + * @return the final wave number + */ + public int getFinalWave() { + return finalWave; + } + + private SortedSet getMatchingRecurrentWaves(int wave) { + TreeSet result = new TreeSet(WaveUtils.getRecurrentComparator()); + for (Wave w : recurrentWaves) { + if (w.matches(wave)) { + result.add(w); + } + } + return result; + } +} diff --git a/src/com/garbagemule/MobArena/waves/WaveParser.java b/src/com/garbagemule/MobArena/waves/WaveParser.java new file mode 100644 index 0000000..cb5904a --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/WaveParser.java @@ -0,0 +1,380 @@ +package com.garbagemule.MobArena.waves; + +import java.util.*; + +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.region.ArenaRegion; +import com.garbagemule.MobArena.util.ItemParser; +import com.garbagemule.MobArena.util.config.ConfigSection; +import com.garbagemule.MobArena.waves.enums.*; +import com.garbagemule.MobArena.waves.types.BossWave; +import com.garbagemule.MobArena.waves.types.DefaultWave; +import com.garbagemule.MobArena.waves.types.SpecialWave; +import com.garbagemule.MobArena.waves.types.SupplyWave; +import com.garbagemule.MobArena.waves.types.SwarmWave; +import com.garbagemule.MobArena.waves.types.UpgradeWave; + +public class WaveParser +{ + public static TreeSet parseWaves(Arena arena, ConfigSection config, WaveBranch branch) { + // Grab the plugin for error reporting. + MobArena plugin = arena.getPlugin(); + + // Create a TreeSet with the Comparator for the specific branch. + TreeSet result = new TreeSet(WaveUtils.getComparator(branch)); + + // If the config is null, return the empty set. + if (config == null) { + plugin.warning(WaveError.BRANCH_MISSING.format(branch.toString().toLowerCase(), arena.configName())); + return result; + } + + // If no waves were found, return the empty set. + Set waves = config.getKeys(); + if (waves == null/* || waves.isEmpty()*/) { + plugin.warning(WaveError.BRANCH_MISSING.format(branch.toString().toLowerCase(), arena.configName())); + return result; + } + + // Otherwise, parse each wave in the branch. + for (String wave : waves) { + ConfigSection waveSection = config.getConfigSection(wave); + Wave w = parseWave(arena, wave, waveSection, branch); + + // Only add properly parsed waves. + if (w == null) { + continue; + } + + result.add(w); + } + + return result; + } + + public static Wave parseWave(Arena arena, String name, ConfigSection config, WaveBranch branch) { + // Grab the plugin for error reporting. + MobArena plugin = arena.getPlugin(); + + // Grab the WaveType and verify that it isn't null. + String t = config.getString("type", null); + WaveType type = WaveType.fromString(t); + + if (type == null) { + plugin.warning(WaveError.INVALID_TYPE.format(t, name, arena.configName())); + return null; + } + + // Prepare the result + Wave result = null; + + // Switch on the type of wave. + switch (type) { + case DEFAULT: + result = parseDefaultWave(arena, name, config); + break; + case SPECIAL: + result = parseSpecialWave(arena, name, config); + break; + case SWARM: + result = parseSwarmWave(arena, name, config); + break; + case SUPPLY: + result = parseSupplyWave(arena, name, config); + break; + case UPGRADE: + result = parseUpgradeWave(arena, name, config); + break; + case BOSS: + result = parseBossWave(arena, name, config); + break; + } + + // Check that the result isn't null + if (result == null) { + plugin.warning(WaveError.INVALID_WAVE.format(name, arena.configName())); + return null; + } + + // Grab the branch-specific nodes. + int priority = config.getInt("priority", -1); + int frequency = config.getInt("frequency", -1); + int firstWave = config.getInt("wave", frequency); + + // Get multipliers + double healthMultiplier = config.getDouble("health-multiplier", -1D); + if (healthMultiplier == -1D) { + healthMultiplier = config.getInt("health-multiplier", 1); + } + + double amountMultiplier = config.getDouble("amount-multiplier", -1D); + if (amountMultiplier == -1D) { + amountMultiplier = config.getInt("amount-multiplier", 1); + } + + // Grab the specific spawnpoints if any + List spawnpoints = getSpawnpoints(arena, name, config); + + // Recurrent must have priority + frequency, single must have firstWave + if (branch == WaveBranch.RECURRENT && (priority == -1 || frequency <= 0)) { + plugin.warning(WaveError.RECURRENT_NODES.format(name, arena.configName())); + return null; + } else if (branch == WaveBranch.SINGLE && firstWave <= 0) { + plugin.warning(WaveError.SINGLE_NODES.format(name, arena.configName())); + return null; + } + + // Set the important required values. + result.setName(name); + result.setBranch(branch); + result.setFirstWave(firstWave); + result.setPriority(priority); + result.setFrequency(frequency); + + // And the multipliers. + result.setHealthMultiplier(healthMultiplier); + result.setAmountMultiplier(amountMultiplier); + + // Aaand the spawnpoints + result.setSpawnpoints(spawnpoints); + + return result; + } + + private static Wave parseDefaultWave(Arena arena, String name, ConfigSection config) { + // Grab the monster map. + SortedMap monsters = getMonsterMap(config); + if (monsters == null || monsters.isEmpty()) { + arena.getPlugin().warning(WaveError.MONSTER_MAP_MISSING.format(name, arena.configName())); + return null; + } + + // Create the wave. + DefaultWave result = new DefaultWave(monsters); + + // Grab the WaveGrowth + String grw = config.getString("growth"); + WaveGrowth growth = WaveGrowth.fromString(grw); + result.setGrowth(growth); + + return result; + } + + private static Wave parseSpecialWave(Arena arena, String name, ConfigSection config) { + SortedMap monsters = getMonsterMap(config); + if (monsters == null || monsters.isEmpty()) { + arena.getPlugin().warning(WaveError.MONSTER_MAP_MISSING.format(name, arena.configName())); + return null; + } + + SpecialWave result = new SpecialWave(monsters); + return result; + } + + private static Wave parseSwarmWave(Arena arena, String name, ConfigSection config) { + MACreature monster = getSingleMonster(config); + if (monster == null) { + arena.getPlugin().warning(WaveError.SINGLE_MONSTER_MISSING.format(name, arena.configName())); + return null; + } + + SwarmWave result = new SwarmWave(monster); + + // Grab SwarmAmount + String amnt = config.getString("amount"); + SwarmAmount amount = SwarmAmount.fromString(amnt); + result.setAmount(amount); + + return result; + } + + private static Wave parseSupplyWave(Arena arena, String name, ConfigSection config) { + SortedMap monsters = getMonsterMap(config); + if (monsters == null || monsters.isEmpty()) { + arena.getPlugin().warning(WaveError.MONSTER_MAP_MISSING.format(name, arena.configName())); + return null; + } + + SupplyWave result = new SupplyWave(monsters); + + // Grab the loot. + String loot = config.getString("drops"); + List stacks = ItemParser.parseItems(loot); + result.setDropList(stacks); + + return result; + } + + private static Wave parseUpgradeWave(Arena arena, String name, ConfigSection config) { + Map> classMap = getUpgradeMap(config); + if (classMap == null || classMap.isEmpty()) { + arena.getPlugin().warning(WaveError.UPGRADE_MAP_MISSING.format(name, arena.configName())); + return null; + } + + UpgradeWave result = new UpgradeWave(classMap); + + // Determine if all items should be given + boolean giveAll = config.getBoolean("give-all-items", false); + result.setGiveAll(giveAll); + + return result; + } + + private static Wave parseBossWave(Arena arena, String name, ConfigSection config) { + MACreature monster = getSingleMonster(config); + if (monster == null) { + arena.getPlugin().warning(WaveError.SINGLE_MONSTER_MISSING.format(name, arena.configName())); + return null; + } + + BossWave result = new BossWave(monster); + + // Grab the boss health + String hlth = config.getString("health"); + BossHealth health = BossHealth.fromString(hlth); + result.setHealth(health); + + // And the abilities. + String ablts = config.getString("abilities"); + if (ablts != null) { + String[] parts = ablts.split(","); + for (String ability : parts) { + BossAbility a = BossAbility.fromString(ability.trim()); + if (a == null) { + arena.getPlugin().warning(WaveError.BOSS_ABILITY.format(ability.trim(), name, arena.configName())); + continue; + } + + result.addBossAbility(a); + } + } + + // As well as the ability interval. + int interval = config.getInt("ability-interval", 3) * 20; + result.setAbilityInterval(interval); + + return result; + } + + /** + * Scan the ConfigSection for a "monster" (singular) node, which + * must be exactly a single monster. + * @param config a ConfigSection + * @return an MACreature, if the monster node contains one that is valid + */ + private static MACreature getSingleMonster(ConfigSection config) { + String monster = config.getString("monster"); + if (monster == null) { + return null; + } + + MACreature result = MACreature.fromString(monster); + return result; + } + + /** + * Scan the ConfigSection for a "monsters" (plural) node, which + * must contain a list of at least one "monster: number" node. + * @param config + * @return a "reverse" map of monsters and numbers + */ + private static SortedMap getMonsterMap(ConfigSection config) { + Set monsters = config.getKeys("monsters"); + if (monsters == null || monsters.isEmpty()) { + return null; + } + + // Prepare the map. + SortedMap monsterMap = new TreeMap(); + int sum = 0; + String path = "monsters."; + + // Check all the monsters. + for (String monster : monsters) { + MACreature creature = MACreature.fromString(monster); + if (creature == null) continue; + + int prob = config.getInt(path + monster, 0); + if (prob == 0) continue; + + sum += prob; + monsterMap.put(sum, creature); + } + + return monsterMap; + } + + private static List getSpawnpoints(Arena arena, String name, ConfigSection config) { + MobArena plugin = arena.getPlugin(); + List result = new ArrayList(); + + String spawnString = config.getString("spawnpoints"); + if (spawnString == null) { + return result; + } + + // Split the string by commas + String[] spawns = spawnString.split(","); + + ArenaRegion region = arena.getRegion(); + for (String spawn : spawns) { + Location spawnpoint = region.getSpawnpoint(spawn.trim()); + + if (spawnpoint == null) { + plugin.warning("Spawnpoint '" + spawn + "' in wave '" + name + "' for arena '" + arena.configName() + "' could not be parsed!"); + continue; + } + + result.add(spawnpoint); + } + + return result; + } + + private static Map> getUpgradeMap(ConfigSection config) { + Set classes = config.getKeys("upgrades"); + if (classes == null || classes.isEmpty()) { + return null; + } + + Map> upgrades = new HashMap>(); + String path = "upgrades."; + + for (String className : classes) { + String itemList = config.getString(path + className); + List stacks = ItemParser.parseItems(itemList); + + if (className.equalsIgnoreCase("All")) { + className = "All"; + } + upgrades.put(className.toLowerCase(), stacks); + } + + return upgrades; + } + + public static Wave createDefaultWave() { + SortedMap monsters = new TreeMap(); + monsters.put(10, MACreature.ZOMBIE); + monsters.put(20, MACreature.SKELETON); + monsters.put(30, MACreature.SPIDER); + monsters.put(40, MACreature.SLIMESMALL); + + DefaultWave result = new DefaultWave(monsters); + result.setName("MA_DEFAULT_WAVE"); + result.setBranch(WaveBranch.RECURRENT); + result.setFirstWave(1); + result.setPriority(1); + result.setFrequency(1); + result.setGrowth(WaveGrowth.OLD); + result.setHealthMultiplier(1D); + result.setAmountMultiplier(1D); + + return result; + } +} diff --git a/src/com/garbagemule/MobArena/util/WaveUtils.java b/src/com/garbagemule/MobArena/waves/WaveUtils.java similarity index 77% rename from src/com/garbagemule/MobArena/util/WaveUtils.java rename to src/com/garbagemule/MobArena/waves/WaveUtils.java index d2fc686..f39e2c0 100644 --- a/src/com/garbagemule/MobArena/util/WaveUtils.java +++ b/src/com/garbagemule/MobArena/waves/WaveUtils.java @@ -1,57 +1,55 @@ -package com.garbagemule.MobArena.util; +package com.garbagemule.MobArena.waves; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; -import java.util.Set; -import java.util.TreeSet; import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; -//import org.bukkit.util.config.Configuration; -import com.garbagemule.MobArena.Arena; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.enums.*; import com.garbagemule.MobArena.MAUtils; import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.waves.*; -import com.garbagemule.MobArena.waves.Wave.*; public class WaveUtils { /** * Get all the spawnpoints that have players nearby. - */ - public static List getValidSpawnpoints(Arena arena, Collection players) - { + */ + public static List getValidSpawnpoints(Arena arena, List spawnpoints, Collection players) { + MobArena plugin = arena.getPlugin(); List result = new ArrayList(); - for (Location l : arena.getSpawnpoints()) - { - for (Player p : players) - { - if (MAUtils.distanceSquared(p, l) >= MobArena.MIN_PLAYER_DISTANCE_SQUARED) + // Ensure that we do have some spawnpoints. + if (spawnpoints == null || spawnpoints.isEmpty()) { + spawnpoints = arena.getRegion().getSpawnpointList(); + } + + // Loop through each one and check if any players are in range. + for (Location l : spawnpoints) { + for (Player p : players) { + if (MAUtils.distanceSquared(plugin, p, l) >= MobArena.MIN_PLAYER_DISTANCE_SQUARED) { continue; - + } result.add(l); break; } } - // If no players are in range, just use all the spawnpoints. - if (result.isEmpty()) - { - MobArena.warning("Spawnpoints of arena '" + arena.configName() + "' may be too far apart!"); - return arena.getAllSpawnpoints();//result.addAll(arena.getAllSpawnpoints()); + // If no spawnpoints in range, just return all of them. + if (result.isEmpty()) { + plugin.warning("Spawnpoints of arena '" + arena.configName() + "' may be too far apart!"); + return spawnpoints; } - - // Else, return the valid spawnpoints. return result; } public static Player getClosestPlayer(Arena arena, Entity e) { + MobArena plugin = arena.getPlugin(); // Set up the comparison variable and the result. double dist = 0; double current = Double.POSITIVE_INFINITY; @@ -59,11 +57,11 @@ public class WaveUtils /* Iterate through the ArrayList, and update current and result every * time a squared distance smaller than current is found. */ - for (Player p : arena.getLivingPlayers()) + for (Player p : arena.getPlayersInArena()) { if (!arena.getWorld().equals(p.getWorld())) { - MobArena.info("Player '" + p.getName() + "' is not in the right world. Kicking..."); + plugin.info("Player '" + p.getName() + "' is not in the right world. Kicking..."); p.kickPlayer("[MobArena] Cheater! (Warped out of the arena world.)"); continue; } @@ -81,8 +79,9 @@ public class WaveUtils /** * Grab and process all the waves in the config-file for the arena. */ - public static TreeSet getWaves(Arena arena, Config config, WaveBranch branch) + /*public static TreeSet getWaves(Arena arena, Config config, WaveBranch branch) { + MobArena plugin = arena.getPlugin(); // Determine the branch type of the wave, and grab the appropriate comparator String b = branch.toString().toLowerCase(); TreeSet result = new TreeSet(getComparator(branch)); @@ -102,14 +101,14 @@ public class WaveUtils wave = getWave(arena, config, path + "." + w + ".", w, branch); if (wave != null) result.add(wave); - else MobArena.warning("Wave '" + w + "' in " + path + " was not added!"); + else plugin.warning("Wave '" + w + "' in " + path + " was not added!"); } } // If there are no waves and the type is 'recurrent', add a couple of auto-generated waves. if (branch == WaveBranch.RECURRENT && (result.isEmpty() || waves == null)) { - MobArena.info("No valid recurrent waves detected for arena '" + arena.configName() + "'. Using defaults..."); + plugin.info("No valid recurrent waves detected for arena '" + arena.configName() + "'. Using defaults..."); DefaultWave def = new DefaultWave(arena, "DEF_WAVE_AUTO", 1, 1, 1, config, path + ".DEF_WAVE_AUTO."); SpecialWave spec = new SpecialWave(arena, "SPEC_WAVE_AUTO", 4, 4, 2, config, path + ".SPEC_WAVE_AUTO."); result.add(def); @@ -117,17 +116,19 @@ public class WaveUtils } return result; - } + }*/ /** * Get a single wave based on the config-file, the path, and branch * @return A Wave object if it is well defined, null otherwise. */ - private static Wave getWave(Arena arena, Config config, String path, String name, WaveBranch branch) + /*private static Wave getWave(Arena arena, Config config, String path, String name, WaveBranch branch) { + MobArena plugin = arena.getPlugin(); + // Grab the wave type, if null or not well defined, return null WaveType type = WaveType.fromString(config.getString(path + "type")); - if (type == null || !isWaveWellDefined(config, path, branch, type)) + if (type == null || !isWaveWellDefined(plugin, config, path, branch, type)) return null; Wave result = null; @@ -160,7 +161,7 @@ public class WaveUtils result = new BossWave(arena, name, wave, config, path); } return result; - } + }*/ @@ -184,7 +185,7 @@ public class WaveUtils * @param type The wave type * @return true, only if the entire wave-node is well-defined. */ - private static boolean isWaveWellDefined(Config config, String path, WaveBranch branch, WaveType type) + /*private static boolean isWaveWellDefined(MobArena plugin, Config config, String path, WaveBranch branch, WaveType type) { // This boolean is used in the "leaf methods" boolean wellDefined = true; @@ -193,7 +194,7 @@ public class WaveUtils int hMulti = MAUtils.getInt(config, path + "health-multiplier"); if (hMulti < 0) { - MobArena.warning("Invalid health multiplier '" + hMulti + "' in " + path); + plugin.warning("Invalid health multiplier '" + hMulti + "' in " + path); wellDefined = false; } @@ -201,7 +202,7 @@ public class WaveUtils int aMulti = MAUtils.getInt(config, path + "amount-multiplier"); if (aMulti < 0) { - MobArena.warning("Invalid amount multiplier '" + aMulti + "' in " + path); + plugin.warning("Invalid amount multiplier '" + aMulti + "' in " + path); wellDefined = false; } @@ -213,12 +214,12 @@ public class WaveUtils int frequency = config.getInt(path + "frequency", 0); if (priority == 0) { - MobArena.warning("Missing 'priority'-node in " + path); + plugin.warning("Missing 'priority'-node in " + path); wellDefined = false; } if (frequency == 0) { - MobArena.warning("Missing 'frequency'-node in " + path); + plugin.warning("Missing 'frequency'-node in " + path); wellDefined = false; } @@ -226,7 +227,7 @@ public class WaveUtils int wave = MAUtils.getInt(config, path + "wave"); if (wave < 0) { - MobArena.warning("'wave' must be greater than 0 in " + path); + plugin.warning("'wave' must be greater than 0 in " + path); wellDefined = false; } } @@ -236,20 +237,20 @@ public class WaveUtils int wave = MAUtils.getInt(config, path + "wave"); if (wave == 0) { - MobArena.warning("Missing 'wave'-node in " + path); + plugin.warning("Missing 'wave'-node in " + path); wellDefined = false; } else if (wave < 0) { - MobArena.warning("'wave' must be greater than 0 in " + path); + plugin.warning("'wave' must be greater than 0 in " + path); wellDefined = false; } } else wellDefined = false; // Passed branch-checks; check type - return isTypeWellDefined(config, path, type, wellDefined); - } + return isTypeWellDefined(plugin, config, path, type, wellDefined); + }*/ /** * Check if a wave type in the config-file is well-defined. @@ -261,19 +262,19 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, only if the entire wave-node is well-defined. */ - private static boolean isTypeWellDefined(Config config, String path, WaveType type, boolean wellDefined) + /*private static boolean isTypeWellDefined(MobArena plugin, Config config, String path, WaveType type, boolean wellDefined) { if (type == WaveType.DEFAULT) - return isDefaultWaveWellDefined(config, path, wellDefined); + return isDefaultWaveWellDefined(plugin, config, path, wellDefined); else if (type == WaveType.SPECIAL) - return isSpecialWaveWellDefined(config, path, wellDefined); + return isSpecialWaveWellDefined(plugin, config, path, wellDefined); else if (type == WaveType.BOSS) - return isBossWaveWellDefined(config, path, wellDefined); + return isBossWaveWellDefined(plugin, config, path, wellDefined); else if (type == WaveType.SWARM) - return isSwarmWaveWellDefined(config, path, wellDefined); + return isSwarmWaveWellDefined(plugin, config, path, wellDefined); return false; - } + }*/ /** * Check if a default wave is well-defined. @@ -284,18 +285,18 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, only if the entire wave-node is well-defined. */ - private static boolean isDefaultWaveWellDefined(Config config, String path, boolean wellDefined) + /*private static boolean isDefaultWaveWellDefined(MobArena plugin, Config config, String path, boolean wellDefined) { // OPTIONAL: Wave growth String growth = config.getString(path + "growth"); if (growth != null && WaveGrowth.fromString(growth) == null) { - MobArena.warning("Invalid wave growth '" + growth + "' in " + path); + plugin.warning("Invalid wave growth '" + growth + "' in " + path); wellDefined = false; } - return isNormalWaveWellDefined(config, path, wellDefined); - } + return isNormalWaveWellDefined(plugin, config, path, wellDefined); + }*/ /** * Check if a special wave is well-defined. @@ -305,10 +306,10 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, only if the entire wave-node is well-defined. */ - private static boolean isSpecialWaveWellDefined(Config config, String path, boolean wellDefined) + /*private static boolean isSpecialWaveWellDefined(MobArena plugin, Config config, String path, boolean wellDefined) { - return isNormalWaveWellDefined(config, path, wellDefined); - } + return isNormalWaveWellDefined(plugin, config, path, wellDefined); + }*/ /** * Check if a default or special wave is well-defined. @@ -320,7 +321,7 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, wellDefined is true. */ - private static boolean isNormalWaveWellDefined(Config config, String path, boolean wellDefined) + /*private static boolean isNormalWaveWellDefined(MobArena plugin, Config config, String path, boolean wellDefined) { // OPTIONAL: Monsters Set monsters = config.getKeys(path + "monsters"); @@ -331,14 +332,14 @@ public class WaveUtils if (MACreature.fromString(monster) != null) continue; - MobArena.warning("Invalid monster type '" + monster + "' in " + path); + plugin.warning("Invalid monster type '" + monster + "' in " + path); wellDefined = false; } } - else MobArena.info("No monsters listed in " + path + ", using defaults..."); + else plugin.info("No monsters listed in " + path + ", using defaults..."); return wellDefined; - } + }*/ /** * Check if a swarm wave is well defined @@ -347,18 +348,18 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, wellDefined is true. */ - private static boolean isSwarmWaveWellDefined(Config config, String path, boolean wellDefined) + /*private static boolean isSwarmWaveWellDefined(MobArena plugin, Config config, String path, boolean wellDefined) { // REQUIRED: Monster type String monster = config.getString(path + "monster"); if (monster == null) { - MobArena.warning("Missing monster type in '" + path); + plugin.warning("Missing monster type in '" + path); wellDefined = false; } else if (MACreature.fromString(monster) == null) { - MobArena.warning("Invalid monster type '" + monster + "' in " + path); + plugin.warning("Invalid monster type '" + monster + "' in " + path); wellDefined = false; } @@ -366,12 +367,12 @@ public class WaveUtils String amount = config.getString(path + "amount"); if (amount != null && SwarmAmount.fromString(amount) == null) { - MobArena.warning("Invalid swarm amount '" + amount + "' in " + path); + plugin.warning("Invalid swarm amount '" + amount + "' in " + path); wellDefined = false; } return wellDefined; - } + }*/ /** * Check if a boss wave is well defined. @@ -380,18 +381,18 @@ public class WaveUtils * @param wellDefined Pass-through boolean for "leaf methods". * @return true, wellDefined is true. */ - private static boolean isBossWaveWellDefined(Config config, String path, boolean wellDefined) + /*private static boolean isBossWaveWellDefined(MobArena plugin, Config config, String path, boolean wellDefined) { // REQUIRED: Monster type String monster = config.getString(path + "monster"); if (monster == null) { - MobArena.warning("Missing monster type in '" + path); + plugin.warning("Missing monster type in '" + path); wellDefined = false; } else if (MACreature.fromString(monster) == null) { - MobArena.warning("Invalid monster type '" + monster + "' in " + path); + plugin.warning("Invalid monster type '" + monster + "' in " + path); wellDefined = false; } @@ -404,7 +405,7 @@ public class WaveUtils if (BossAbility.fromString(ability.trim()) != null) continue; - MobArena.warning("Invalid boss ability '" + ability + "' in " + path); + plugin.warning("Invalid boss ability '" + ability + "' in " + path); wellDefined = false; } } @@ -413,7 +414,7 @@ public class WaveUtils int abilityDelay = config.getInt(path + "ability-interval", 3); if (abilityDelay <= 0) { - MobArena.warning("Boss ability-delay must be greater than 0, " + path); + plugin.warning("Boss ability-delay must be greater than 0, " + path); wellDefined = false; } @@ -426,12 +427,12 @@ public class WaveUtils String health = config.getString(path + "health"); if (health != null && BossHealth.fromString(health) == null) { - MobArena.warning("Invalid boss health '" + health + "' in " + path); + plugin.warning("Invalid boss health '" + health + "' in " + path); wellDefined = false; } return wellDefined; - } + }*/ @@ -466,9 +467,9 @@ public class WaveUtils { public int compare(Wave w1, Wave w2) { - if (w1.getWave() < w2.getWave()) + if (w1.getFirstWave() < w2.getFirstWave()) return -1; - else if (w1.getWave() > w2.getWave()) + else if (w1.getFirstWave() > w2.getFirstWave()) return 1; else return 0; } diff --git a/src/com/garbagemule/MobArena/waves/enums/BossHealth.java b/src/com/garbagemule/MobArena/waves/enums/BossHealth.java new file mode 100644 index 0000000..78ea0b8 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/BossHealth.java @@ -0,0 +1,21 @@ +package com.garbagemule.MobArena.waves.enums; + +import com.garbagemule.MobArena.waves.WaveUtils; + +public enum BossHealth +{ + LOW(8), MEDIUM(15), HIGH(25), PSYCHO(40); + private int multiplier; + + private BossHealth(int multiplier) { + this.multiplier = multiplier; + } + + public int getMax(int playerCount) { + return (playerCount + 1) * 20 * multiplier; + } + + public static BossHealth fromString(String string) { + return WaveUtils.getEnumFromString(BossHealth.class, string, MEDIUM); + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/enums/SwarmAmount.java b/src/com/garbagemule/MobArena/waves/enums/SwarmAmount.java new file mode 100644 index 0000000..81a5a2d --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/SwarmAmount.java @@ -0,0 +1,21 @@ +package com.garbagemule.MobArena.waves.enums; + +import com.garbagemule.MobArena.waves.WaveUtils; + +public enum SwarmAmount +{ + LOW(10), MEDIUM(20), HIGH(30), PSYCHO(60); + private int multiplier; + + private SwarmAmount(int multiplier) { + this.multiplier = multiplier; + } + + public int getAmount(int playerCount) { + return Math.max(1, playerCount / 2) * multiplier; + } + + public static SwarmAmount fromString(String string) { + return WaveUtils.getEnumFromString(SwarmAmount.class, string, LOW); + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/enums/WaveBranch.java b/src/com/garbagemule/MobArena/waves/enums/WaveBranch.java new file mode 100644 index 0000000..eaefcb8 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/WaveBranch.java @@ -0,0 +1,25 @@ +package com.garbagemule.MobArena.waves.enums; + +import com.garbagemule.MobArena.waves.Wave; + +public enum WaveBranch +{ + SINGLE { + @Override + public boolean matches(int wave, Wave w) { + return (w.getFirstWave() == wave); + } + }, + + RECURRENT { + @Override + public boolean matches(int wave, Wave w) { + if (wave < w.getFirstWave()) { + return false; + } + return ((wave - w.getFirstWave()) % w.getFrequency() == 0); + } + }; + + public abstract boolean matches(int wave, Wave w); +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/enums/WaveError.java b/src/com/garbagemule/MobArena/waves/enums/WaveError.java new file mode 100644 index 0000000..6fe785a --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/WaveError.java @@ -0,0 +1,32 @@ +package com.garbagemule.MobArena.waves.enums; + +public enum WaveError +{ + INVALID_WAVE("Wave '%s' for arena '%s' could not be parsed."), + NO_RECURRENT_WAVES("No valid recurrent waves found for arena '%s'. Check the config-file. Using implicit default wave."), + + BRANCH_MISSING("The '%s' branch for arena '%s' is empty. Check the config-file."), + INVALID_TYPE("Invalid type '%s' for wave '%s' in arena '%s'. Skipping..."), + + RECURRENT_NODES("Recurrent wave '%s' in arena '%s' is missing either the 'frequency' or the 'priority' node. Skipping..."), + SINGLE_NODES("Single wave '%s' in arena '%s' is missing the 'wave' node. Skipping..."), + + MONSTER_MAP_MISSING("Missing 'monsters' node for wave '%s' in arena '%s'."), + SINGLE_MONSTER_MISSING("Missing 'monster' node for wave '%s' in arena '%s'."), + + BOSS_ABILITY("Invalid boss ability '%s' for wave '%s' in arena '%s'."), + + SUPPLY_DROPS("Missing 'drops' node for wave '%s' in arena '%s'."), + + UPGRADE_MAP_MISSING("Missing 'upgrades' node for wave '%s' in arena '%s'."); + + private String msg; + + private WaveError(String msg) { + this.msg = msg; + } + + public String format(Object... args) { + return String.format(msg, args); + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/enums/WaveGrowth.java b/src/com/garbagemule/MobArena/waves/enums/WaveGrowth.java new file mode 100644 index 0000000..f80e7af --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/WaveGrowth.java @@ -0,0 +1,27 @@ +package com.garbagemule.MobArena.waves.enums; + +import com.garbagemule.MobArena.waves.WaveUtils; + +public enum WaveGrowth +{ + OLD(0), SLOW(0.5), MEDIUM(0.65), FAST(0.8), PSYCHO(1.2); + private double exp; + + private WaveGrowth(double exp) { + this.exp = exp; + } + + public static WaveGrowth fromString(String string) { + return WaveUtils.getEnumFromString(WaveGrowth.class, string, OLD); + } + + public int getAmount(int wave, int playerCount) { + if (this == OLD) return wave + playerCount; + + double pc = (double) playerCount; + double w = (double) wave; + + double base = Math.min(Math.ceil(pc/2) + 1, 13); + return (int) ( base * Math.pow(w, exp) ); + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/enums/WaveType.java b/src/com/garbagemule/MobArena/waves/enums/WaveType.java new file mode 100644 index 0000000..238b815 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/enums/WaveType.java @@ -0,0 +1,57 @@ +package com.garbagemule.MobArena.waves.enums; + +import com.garbagemule.MobArena.Messenger; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.WaveUtils; + +public enum WaveType +{ + DEFAULT { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_DEFAULT, ""+wave); + } + }, + + SPECIAL { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_SPECIAL, ""+wave); + } + }, + + SWARM { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_SWARM, ""+wave); + } + }, + + SUPPLY { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_SUPPLY, ""+wave); + } + }, + + BOSS { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_BOSS, ""+wave); + } + }, + + UPGRADE { + @Override + public void announce(Arena arena, int wave) { + Messenger.tellAll(arena, Msg.WAVE_UPGRADE, ""+wave); + } + }; + + public abstract void announce(Arena arena, int wave); + + public static WaveType fromString(String string) { + return WaveUtils.getEnumFromString(WaveType.class, string); + } +} \ No newline at end of file diff --git a/src/com/garbagemule/MobArena/waves/types/BossWave.java b/src/com/garbagemule/MobArena/waves/types/BossWave.java new file mode 100644 index 0000000..39ad13e --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/BossWave.java @@ -0,0 +1,97 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.BossAbility; +import com.garbagemule.MobArena.waves.BossAbilityThread; +import com.garbagemule.MobArena.waves.MABoss; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.*; +import com.garbagemule.MobArena.MobArena; + +public class BossWave extends AbstractWave +{ + private MACreature monster; + private Set bosses; + private BossHealth health; + + private List abilities; + private boolean activated; + + private int abilityInterval; + + public BossWave(MACreature monster) { + this.monster = monster; + this.bosses = new HashSet(); + this.abilities = new ArrayList(); + this.activated = false; + this.setType(WaveType.BOSS); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + Map result = new HashMap(); + result.put(monster, 1); + return result; + } + + public int getMaxHealth(int playerCount) { + return health.getMax(playerCount); + } + + public void setHealth(BossHealth health) { + this.health = health; + } + + public void addMABoss(MABoss boss) { + bosses.add(boss); + } + + public Set getMABosses() { + Set result = new HashSet(); + for (MABoss b : bosses) { + if (!b.isDead()) { + result.add(b); + } + } + return result; + } + + public void addBossAbility(BossAbility ability) { + abilities.add(ability); + } + + public int getAbilityInterval() { + return abilityInterval; + } + + public void setAbilityInterval(int abilityInterval) { + this.abilityInterval = abilityInterval; + } + + public void activateAbilities(Arena arena) { + if (activated) { + return; + } + + BossAbilityThread bat = new BossAbilityThread(this, abilities, arena); + scheduleTask(arena, bat, 100); + activated = true; + } + + public void scheduleTask(Arena arena, Runnable r, int delay) { + MobArena plugin = arena.getPlugin(); + + plugin.getServer().getScheduler().scheduleSyncDelayedTask( + plugin, + r, + delay); + } +} diff --git a/src/com/garbagemule/MobArena/waves/types/DefaultWave.java b/src/com/garbagemule/MobArena/waves/types/DefaultWave.java new file mode 100644 index 0000000..802f77c --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/DefaultWave.java @@ -0,0 +1,61 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.*; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.*; + +public class DefaultWave extends AbstractWave +{ + private SortedMap monsterMap; + private WaveGrowth growth; + + public DefaultWave(SortedMap monsterMap) { + this.monsterMap = monsterMap; + this.growth = WaveGrowth.OLD; + this.setType(WaveType.DEFAULT); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + // Get the amount of monsters to spawn. + int toSpawn = (int) Math.max(1D, growth.getAmount(wave, playerCount) * super.getAmountMultiplier()); + + // Grab the total probability sum. + int total = monsterMap.lastKey(); + + // Random number generator. + Random random = new Random(); + + // Prepare the monster map. + Map monsters = new HashMap(); + + // Generate some random amounts. + for (int i = 0; i < toSpawn; i++) { + int value = random.nextInt(total) + 1; + + for (Map.Entry entry : monsterMap.entrySet()) { + if (value > entry.getKey()) { + continue; + } + + Integer current = monsters.get(entry.getValue()); + monsters.put(entry.getValue(), (current == null ? 1 : current + 1)); + break; + } + } + //TODO: Remember amount-multiplier. + // Return the map. + return monsters; + } + + public WaveGrowth getGrowth() { + return growth; + } + + public void setGrowth(WaveGrowth growth) { + this.growth = growth; + } +} diff --git a/src/com/garbagemule/MobArena/waves/types/SpecialWave.java b/src/com/garbagemule/MobArena/waves/types/SpecialWave.java new file mode 100644 index 0000000..052b2bb --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/SpecialWave.java @@ -0,0 +1,51 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.*; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.WaveType; + +public class SpecialWave extends AbstractWave +{ + private SortedMap monsterMap; + + public SpecialWave(SortedMap monsterMap) { + this.monsterMap = monsterMap; + this.setType(WaveType.SPECIAL); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + // Random number. + Random random = new Random(); + int value = random.nextInt(monsterMap.lastKey()); + + // Prepare the monster map. + Map result = new HashMap(); + + for (Map.Entry entry : monsterMap.entrySet()) { + if (value > entry.getKey()) { + continue; + } + + int amount; + switch (entry.getValue()) { + case POWEREDCREEPERS: + case ZOMBIEPIGMEN: + case ANGRYWOLVES: amount = playerCount * 2; break; + case SLIMES: amount = playerCount * 4; break; + case GIANTS: + case GHASTS: amount = 2; + default: amount = playerCount + 1; break; + } + + amount = (int) Math.max(1D, amount * super.getAmountMultiplier()); + result.put(entry.getValue(), amount); + break; + } + + return result; + } +} diff --git a/src/com/garbagemule/MobArena/waves/types/SupplyWave.java b/src/com/garbagemule/MobArena/waves/types/SupplyWave.java new file mode 100644 index 0000000..fef917e --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/SupplyWave.java @@ -0,0 +1,61 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.*; + +import org.bukkit.inventory.ItemStack; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.WaveType; + +public class SupplyWave extends AbstractWave +{ + private SortedMap monsterMap; + private List drops; + + public SupplyWave(SortedMap monsterMap) { + this.monsterMap = monsterMap; + this.setType(WaveType.SUPPLY); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + // Grab the total probability sum. + int total = monsterMap.lastKey(); + + // Random number generator. + Random random = new Random(); + + // Prepare the monster map. + Map monsters = new HashMap(); + + int toSpawn = (int) Math.max(1D, playerCount * super.getAmountMultiplier()); + + // Spawn a monster for each player. + for (int i = 0; i < toSpawn; i++) { + int value = random.nextInt(total) + 1; + + for (Map.Entry entry : monsterMap.entrySet()) { + if (value > entry.getKey()) { + continue; + } + + Integer current = monsters.get(entry.getValue()); + monsters.put(entry.getValue(), (current == null ? 1 : current + 1)); + break; + } + } + + // Return the map. + return monsters; + } + + public List getDropList() { + return drops; + } + + public void setDropList(List drops) { + this.drops = drops; + } +} diff --git a/src/com/garbagemule/MobArena/waves/types/SwarmWave.java b/src/com/garbagemule/MobArena/waves/types/SwarmWave.java new file mode 100644 index 0000000..547a312 --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/SwarmWave.java @@ -0,0 +1,40 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.*; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.*; + +public class SwarmWave extends AbstractWave +{ + private MACreature monster; + private SwarmAmount amount; + + public SwarmWave(MACreature monster) { + this.monster = monster; + this.amount = SwarmAmount.LOW; + this.setType(WaveType.SWARM); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + // Prepare the monster map. + Map result = new HashMap(); + + // Add the monster and the swarm amount. + int toSpawn = (int) Math.max(1D, amount.getAmount(playerCount) * super.getAmountMultiplier()); + result.put(monster, toSpawn); + + return result; + } + + public SwarmAmount getAmount() { + return amount; + } + + public void setAmount(SwarmAmount amount) { + this.amount = amount; + } +} diff --git a/src/com/garbagemule/MobArena/waves/types/UpgradeWave.java b/src/com/garbagemule/MobArena/waves/types/UpgradeWave.java new file mode 100644 index 0000000..eee631a --- /dev/null +++ b/src/com/garbagemule/MobArena/waves/types/UpgradeWave.java @@ -0,0 +1,51 @@ +package com.garbagemule.MobArena.waves.types; + +import java.util.*; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.enums.WaveType; + +public class UpgradeWave extends AbstractWave +{ + private Map> classMap; + private boolean giveAll; + + public UpgradeWave(Map> classMap) { + this.classMap = classMap; + this.setType(WaveType.UPGRADE); + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + return new HashMap(); + } + + public void grantItems(Player p, String className) { + List stacks = classMap.get(className.toLowerCase()); + if (stacks == null || stacks.isEmpty()) return; + + PlayerInventory inv = p.getInventory(); + + // Check if we need to add all of the items or just one. + if (giveAll) { + for (ItemStack stack : stacks) { + inv.addItem(stack); + } + } + // Get a random item from the list. + else { + int index = new Random().nextInt(stacks.size()); + inv.addItem(stacks.get(index)); + } + } + + public void setGiveAll(boolean giveAll) { + this.giveAll = giveAll; + } +} diff --git a/src/com/garbagemule/register/payment/Method.java b/src/com/garbagemule/register/payment/Method.java deleted file mode 100644 index b4c4ff4..0000000 --- a/src/com/garbagemule/register/payment/Method.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.garbagemule.register.payment; - -import org.bukkit.plugin.Plugin; - -/** - * Interface to be implemented by a payment method. - * - * @author Nijikokun (@nijikokun) - * @copyright Copyright (C) 2011 - * @license AOL license - */ -public interface Method { - /** - * Encodes the Plugin into an Object disguised as the Plugin. - * If you want the original Plugin Class you must cast it to the correct - * Plugin, to do so you have to verify the name and or version then cast. - * - *
-     *  if(method.getName().equalsIgnoreCase("iConomy"))
-     *   iConomy plugin = ((iConomy)method.getPlugin());
- * - * @return Object - * @see #getName() - * @see #getVersion() - */ - public Object getPlugin(); - - /** - * Returns the actual name of this method. - * - * @return String Plugin name. - */ - public String getName(); - - /** - * Returns the actual version of this method. - * - * @return String Plugin version. - */ - public String getVersion(); - - /** - * Returns the amount of decimal places that get stored - * NOTE: it will return -1 if there is no rounding - * - * @return int for each decimal place - */ - public int fractionalDigits(); - - /** - * Formats amounts into this payment methods style of currency display. - * - * @param amount Double - * @return String - Formatted Currency Display. - */ - public String format(double amount); - - /** - * Allows the verification of bank API existence in this payment method. - * - * @return boolean - */ - public boolean hasBanks(); - - /** - * Determines the existence of a bank via name. - * - * @param bank Bank name - * @return boolean - * @see #hasBanks - */ - public boolean hasBank(String bank); - - /** - * Determines the existence of an account via name. - * - * @param name Account name - * @return boolean - */ - public boolean hasAccount(String name); - - /** - * Check to see if an account name is tied to a bank. - * - * @param bank Bank name - * @param name Account name - * @return boolean - */ - public boolean hasBankAccount(String bank, String name); - - /** - * Forces an account creation - * - * @param name Account name - * @return boolean - */ - public boolean createAccount(String name); - - /** - * Forces an account creation - * - * @param name Account name - * @param balance Initial account balance - * @return boolean - */ - public boolean createAccount(String name, Double balance); - - /** - * Returns a MethodAccount class for an account name. - * - * @param name Account name - * @return MethodAccount or Null - */ - public MethodAccount getAccount(String name); - - - /** - * Returns a MethodBankAccount class for an account name. - * - * @param bank Bank name - * @param name Account name - * @return MethodBankAccount or Null - */ - public MethodBankAccount getBankAccount(String bank, String name); - - /** - * Checks to verify the compatibility between this Method and a plugin. - * Internal usage only, for the most part. - * - * @param plugin Plugin - * @return boolean - */ - public boolean isCompatible(Plugin plugin); - - /** - * Set Plugin data. - * - * @param plugin Plugin - */ - public void setPlugin(Plugin plugin); - - /** - * Contains Calculator and Balance functions for Accounts. - */ - public interface MethodAccount { - public double balance(); - public boolean set(double amount); - public boolean add(double amount); - public boolean subtract(double amount); - public boolean multiply(double amount); - public boolean divide(double amount); - public boolean hasEnough(double amount); - public boolean hasOver(double amount); - public boolean hasUnder(double amount); - public boolean isNegative(); - public boolean remove(); - - @Override - public String toString(); - } - - /** - * Contains Calculator and Balance functions for Bank Accounts. - */ - public interface MethodBankAccount { - public double balance(); - public String getBankName(); - public int getBankId(); - public boolean set(double amount); - public boolean add(double amount); - public boolean subtract(double amount); - public boolean multiply(double amount); - public boolean divide(double amount); - public boolean hasEnough(double amount); - public boolean hasOver(double amount); - public boolean hasUnder(double amount); - public boolean isNegative(); - public boolean remove(); - - @Override - public String toString(); - } -} diff --git a/src/com/garbagemule/register/payment/Methods.java b/src/com/garbagemule/register/payment/Methods.java deleted file mode 100644 index 41920a8..0000000 --- a/src/com/garbagemule/register/payment/Methods.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.garbagemule.register.payment; - -import java.util.HashSet; -import java.util.Set; - -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginManager; - -/** - * The Methods initializes Methods that utilize the Method interface - * based on a "first come, first served" basis. - * - * Allowing you to check whether a payment method exists or not. - * - * Methods also allows you to set a preferred method of payment before it captures - * payment plugins in the initialization process. - * - * in bukkit.yml: - *
- *  economy:
- *      preferred: "iConomy"
- * 
- * - * @author: Nijikokun (@nijikokun) - * @copyright: Copyright (C) 2011 - * @license: AOL license - */ -public class Methods { - private static String version = null; - private static boolean self = false; - private static Method Method = null; - private static String preferred = ""; - private static Set Methods = new HashSet(); - private static Set Dependencies = new HashSet(); - private static Set Attachables = new HashSet(); - - static { - _init(); - } - - /** - * Implement all methods along with their respective name & class. - */ - private static void _init() { - addMethod("iConomy", new com.garbagemule.register.payment.methods.iCo6()); - addMethod("iConomy", new com.garbagemule.register.payment.methods.iCo5()); - addMethod("iConomy", new com.garbagemule.register.payment.methods.iCo4()); - addMethod("BOSEconomy", new com.garbagemule.register.payment.methods.BOSE6()); - addMethod("BOSEconomy", new com.garbagemule.register.payment.methods.BOSE7()); - addMethod("Essentials", new com.garbagemule.register.payment.methods.EE17()); - addMethod("Currency", new com.garbagemule.register.payment.methods.MCUR()); - Dependencies.add("MultiCurrency"); - } - - /** - * Used by the plugin to setup version - * - * @param v version - */ - public static void setVersion(String v) { - version = v; - } - - /** - * Use to reset methods during disable - */ - public static void reset() { - version = null; - self = false; - Method = null; - preferred = ""; - Attachables.clear(); - } - - /** - * Use to get version of Register plugin - * @return version - */ - public static String getVersion() { - return version; - } - - /** - * Returns an array of payment method names that have been loaded - * through the _init method. - * - * @return Set - Array of payment methods that are loaded. - * @see #setMethod(org.bukkit.plugin.Plugin) - */ - public static Set getDependencies() { - return Dependencies; - } - - /** - * Interprets Plugin class data to verify whether it is compatible with an existing payment - * method to use for payments and other various economic activity. - * - * @param plugin Plugin data from bukkit, Internal Class file. - * @return Method or Null - */ - public static Method createMethod(Plugin plugin) { - for (Method method: Methods) - if (method.isCompatible(plugin)) { - method.setPlugin(plugin); - return method; - } - - return null; - } - - private static void addMethod(String name, Method method) { - Dependencies.add(name); - Methods.add(method); - } - - /** - * Verifies if Register has set a payment method for usage yet. - * - * @return boolean - * @see #setMethod(org.bukkit.plugin.Plugin) - * @see #checkDisabled(org.bukkit.plugin.Plugin) - */ - public static boolean hasMethod() { - return (Method != null); - } - - /** - * Checks Plugin Class against a multitude of checks to verify it's usability - * as a payment method. - * - * @param PluginManager the plugin manager for the server - * @return boolean True on success, False on failure. - */ - public static boolean setMethod(PluginManager manager) { - if (hasMethod()) - return true; - - if (self) { - self = false; - return false; - } - - int count = 0; - boolean match = false; - Plugin plugin = null; - - for (String name : getDependencies()) { - if (hasMethod()) - break; - - plugin = manager.getPlugin(name); - if (plugin == null || !plugin.isEnabled()) - continue; - - Method current = createMethod(plugin); - if (current == null) - continue; - - if (preferred.isEmpty()) - Method = current; - else - Attachables.add(current); - } - - if (!preferred.isEmpty()) { - do { - if (hasMethod()) - match = true; - else { - for (Method attached : Attachables) { - if (attached == null) - continue; - - if (hasMethod()) { - match = true; break; - } - - if (preferred.isEmpty()) - Method = attached; - - if (count == 0) - if (preferred.equalsIgnoreCase(attached.getName())) - Method = attached; - - else - Method = attached; - } - - count++; - } - } while (!match); - } - - return hasMethod(); - } - - /** - * Sets the preferred economy - * - * @return boolean - */ - public static boolean setPreferred(String check) { - if (getDependencies().contains(check)) { - preferred = check; - return true; - } - - return false; - } - - /** - * Grab the existing and initialized (hopefully) Method Class. - * - * @return Method or Null - */ - public static Method getMethod() { - return Method; - } - - /** - * Verify is a plugin is disabled, only does this if we there is an existing payment - * method initialized in Register. - * - * @param method Plugin data from bukkit, Internal Class file. - * @return boolean - */ - public static boolean checkDisabled(Plugin method) { - if(!hasMethod()) - return true; - - if (Method.isCompatible(method)) - Method = null; - - return (Method == null); - } -} diff --git a/src/com/garbagemule/register/payment/methods/BOSE6.java b/src/com/garbagemule/register/payment/methods/BOSE6.java deleted file mode 100644 index b3e1cbf..0000000 --- a/src/com/garbagemule/register/payment/methods/BOSE6.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; - -import cosine.boseconomy.BOSEconomy; -import org.bukkit.plugin.Plugin; - -/** - * BOSEconomy 6 Implementation of Method - * - * @author Nijikokun (@nijikokun) - * @copyright (c) 2011 - * @license AOL license - */ -@SuppressWarnings("deprecation") -public class BOSE6 implements Method { - private BOSEconomy BOSEconomy; - - public BOSEconomy getPlugin() { - return this.BOSEconomy; - } - - public String getName() { - return "BOSEconomy"; - } - - public String getVersion() { - return "0.6.2"; - } - - public int fractionalDigits() { - return 0; - } - - public String format(double amount) { - String currency = this.BOSEconomy.getMoneyNamePlural(); - - if(amount == 1) - currency = this.BOSEconomy.getMoneyName(); - - return amount + " " + currency; - } - - public boolean hasBanks() { - return true; - } - - public boolean hasBank(String bank) { - return this.BOSEconomy.bankExists(bank); - } - - public boolean hasAccount(String name) { - return this.BOSEconomy.playerRegistered(name, false); - } - - public boolean hasBankAccount(String bank, String name) { - return this.BOSEconomy.isBankOwner(bank, name) - || this.BOSEconomy.isBankMember(bank, name); - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - this.BOSEconomy.registerPlayer(name); - return true; - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - this.BOSEconomy.registerPlayer(name); - this.BOSEconomy.setPlayerMoney(name, balance, false); - return true; - } - - public MethodAccount getAccount(String name) { - if(!hasAccount(name)) - return null; - - return new BOSEAccount(name, this.BOSEconomy); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - if(!hasBankAccount(bank, name)) - return null; - - return new BOSEBankAccount(bank, BOSEconomy); - } - - public boolean isCompatible(Plugin plugin) { - return plugin.getDescription().getName().equalsIgnoreCase("boseconomy") - && plugin instanceof BOSEconomy - && plugin.getDescription().getVersion().equals("0.6.2"); - } - - public void setPlugin(Plugin plugin) { - BOSEconomy = (BOSEconomy) plugin; - } - - public class BOSEAccount implements MethodAccount { - private final String name; - private final BOSEconomy BOSEconomy; - - public BOSEAccount(String name, BOSEconomy bOSEconomy) { - this.name = name; - this.BOSEconomy = bOSEconomy; - } - - public double balance() { - return (double) this.BOSEconomy.getPlayerMoney(this.name); - } - - public boolean set(double amount) { - int IntAmount = (int)Math.ceil(amount); - return this.BOSEconomy.setPlayerMoney(this.name, IntAmount, false); - } - - public boolean add(double amount) { - int IntAmount = (int)Math.ceil(amount); - return this.BOSEconomy.addPlayerMoney(this.name, IntAmount, false); - } - - public boolean subtract(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance - IntAmount), false); - } - - public boolean multiply(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance * IntAmount), false); - } - - public boolean divide(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance / IntAmount), false); - } - - public boolean hasEnough(double amount) { - return (this.balance() >= amount); - } - - public boolean hasOver(double amount) { - return (this.balance() > amount); - } - - public boolean hasUnder(double amount) { - return (this.balance() < amount); - } - - public boolean isNegative() { - return (this.balance() < 0); - } - - public boolean remove() { - return false; - } - } - - public class BOSEBankAccount implements MethodBankAccount { - private final String bank; - private final BOSEconomy BOSEconomy; - - public BOSEBankAccount(String bank, BOSEconomy bOSEconomy) { - this.bank = bank; - this.BOSEconomy = bOSEconomy; - } - - public String getBankName() { - return this.bank; - } - - public int getBankId() { - return -1; - } - - public double balance() { - return (double) this.BOSEconomy.getBankMoney(bank); - } - - public boolean set(double amount) { - int IntAmount = (int)Math.ceil(amount); - return this.BOSEconomy.setBankMoney(bank, IntAmount, true); - } - - public boolean add(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance + IntAmount), false); - } - - public boolean subtract(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance - IntAmount), false); - } - - public boolean multiply(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance * IntAmount), false); - } - - public boolean divide(double amount) { - int IntAmount = (int)Math.ceil(amount); - int balance = (int)this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance / IntAmount), false); - } - - public boolean hasEnough(double amount) { - return (this.balance() >= amount); - } - - public boolean hasOver(double amount) { - return (this.balance() > amount); - } - - public boolean hasUnder(double amount) { - return (this.balance() < amount); - } - - public boolean isNegative() { - return (this.balance() < 0); - } - - public boolean remove() { - return this.BOSEconomy.removeBank(bank); - } - } -} \ No newline at end of file diff --git a/src/com/garbagemule/register/payment/methods/BOSE7.java b/src/com/garbagemule/register/payment/methods/BOSE7.java deleted file mode 100644 index 376eaff..0000000 --- a/src/com/garbagemule/register/payment/methods/BOSE7.java +++ /dev/null @@ -1,223 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; - -import cosine.boseconomy.BOSEconomy; -import org.bukkit.plugin.Plugin; - -/** - * BOSEconomy 7 Implementation of Method - * - * @author Acrobot - * @author Nijikokun (@nijikokun) - * @copyright (c) 2011 - * @license AOL license - */ -public class BOSE7 implements Method { - private BOSEconomy BOSEconomy; - - public BOSEconomy getPlugin() { - return this.BOSEconomy; - } - - public String getName() { - return "BOSEconomy"; - } - - public String getVersion() { - return "0.7.0"; - } - - public int fractionalDigits() { - return this.BOSEconomy.getFractionalDigits(); - } - - public String format(double amount) { - String currency = this.BOSEconomy.getMoneyNamePlural(); - - if(amount == 1) - currency = this.BOSEconomy.getMoneyName(); - - return amount + " " + currency; - } - - public boolean hasBanks() { - return true; - } - - public boolean hasBank(String bank) { - return this.BOSEconomy.bankExists(bank); - } - - public boolean hasAccount(String name) { - return this.BOSEconomy.playerRegistered(name, false); - } - - public boolean hasBankAccount(String bank, String name) { - return this.BOSEconomy.isBankOwner(bank, name) || this.BOSEconomy.isBankMember(bank, name); - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - this.BOSEconomy.registerPlayer(name); - return true; - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - this.BOSEconomy.registerPlayer(name); - this.BOSEconomy.setPlayerMoney(name, balance, false); - return true; - } - - public MethodAccount getAccount(String name) { - if(!hasAccount(name)) - return null; - - return new BOSEAccount(name, this.BOSEconomy); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - if(!hasBankAccount(bank, name)) - return null; - - return new BOSEBankAccount(bank, BOSEconomy); - } - - public boolean isCompatible(Plugin plugin) { - return plugin.getDescription().getName().equalsIgnoreCase("boseconomy") - && plugin instanceof BOSEconomy - && !plugin.getDescription().getVersion().equals("0.6.2"); - } - - public void setPlugin(Plugin plugin) { - BOSEconomy = (BOSEconomy)plugin; - } - - public class BOSEAccount implements MethodAccount { - private String name; - private BOSEconomy BOSEconomy; - - public BOSEAccount(String name, BOSEconomy bOSEconomy) { - this.name = name; - this.BOSEconomy = bOSEconomy; - } - - public double balance() { - return this.BOSEconomy.getPlayerMoneyDouble(this.name); - } - - public boolean set(double amount) { - return this.BOSEconomy.setPlayerMoney(this.name, amount, false); - } - - public boolean add(double amount) { - return this.BOSEconomy.addPlayerMoney(this.name, amount, false); - } - - public boolean subtract(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance - amount), false); - } - - public boolean multiply(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance * amount), false); - } - - public boolean divide(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setPlayerMoney(this.name, (balance / amount), false); - } - - public boolean hasEnough(double amount) { - return (this.balance() >= amount); - } - - public boolean hasOver(double amount) { - return (this.balance() > amount); - } - - public boolean hasUnder(double amount) { - return (this.balance() < amount); - } - - public boolean isNegative() { - return (this.balance() < 0); - } - - public boolean remove() { - return false; - } - } - - public class BOSEBankAccount implements MethodBankAccount { - private String bank; - private BOSEconomy BOSEconomy; - - public BOSEBankAccount(String bank, BOSEconomy bOSEconomy) { - this.bank = bank; - this.BOSEconomy = bOSEconomy; - } - - public String getBankName() { - return this.bank; - } - - public int getBankId() { - return -1; - } - - public double balance() { - return this.BOSEconomy.getBankMoneyDouble(bank); - } - - public boolean set(double amount) { - return this.BOSEconomy.setBankMoney(bank, amount, true); - } - - public boolean add(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance + amount), false); - } - - public boolean subtract(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance - amount), false); - } - - public boolean multiply(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance * amount), false); - } - - public boolean divide(double amount) { - double balance = this.balance(); - return this.BOSEconomy.setBankMoney(bank, (balance / amount), false); - } - - public boolean hasEnough(double amount) { - return (this.balance() >= amount); - } - - public boolean hasOver(double amount) { - return (this.balance() > amount); - } - - public boolean hasUnder(double amount) { - return (this.balance() < amount); - } - - public boolean isNegative() { - return (this.balance() < 0); - } - - public boolean remove() { - return this.BOSEconomy.removeBank(bank); - } - } -} \ No newline at end of file diff --git a/src/com/garbagemule/register/payment/methods/EE17.java b/src/com/garbagemule/register/payment/methods/EE17.java deleted file mode 100644 index ed282e8..0000000 --- a/src/com/garbagemule/register/payment/methods/EE17.java +++ /dev/null @@ -1,223 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; -import com.earth2me.essentials.Essentials; -import com.earth2me.essentials.api.Economy; - -import org.bukkit.plugin.Plugin; - -/** - * Essentials 17 Implementation of Method - * - * @author Nijikokun (@nijikokun) - * @author Snowleo - * @author Acrobot - * @author KHobbits - * @copyright (c) 2011 - * @license AOL license - */ -public class EE17 implements Method { - private Essentials Essentials; - - public Essentials getPlugin() { - return this.Essentials; - } - - public String getName() { - return "Essentials"; - } - - public String getVersion() { - return "2.2"; - } - - public int fractionalDigits() { - return -1; - } - - public String format(double amount) { - return Economy.format(amount); - } - - public boolean hasBanks() { - return false; - } - - public boolean hasBank(String bank) { - return false; - } - - public boolean hasAccount(String name) { - return Economy.playerExists(name); - } - - public boolean hasBankAccount(String bank, String name) { - return false; - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - Economy.createNPC(name); - return true; - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - Economy.createNPC(name); - - try { - Economy.setMoney(name, balance); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public MethodAccount getAccount(String name) { - if(!hasAccount(name)) - return null; - - return new EEcoAccount(name); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - return null; - } - - public boolean isCompatible(Plugin plugin) { - try { Class.forName("com.earth2me.essentials.api.Economy"); } - catch(Exception e) { return false; } - - return plugin.getDescription().getName().equalsIgnoreCase("essentials") - && plugin instanceof Essentials; - } - - public void setPlugin(Plugin plugin) { - Essentials = (Essentials)plugin; - } - - public class EEcoAccount implements MethodAccount { - private String name; - - public EEcoAccount(String name) { - this.name = name; - } - - public double balance() { - Double balance = 0.0; - - try { - balance = Economy.getMoney(this.name); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - } - - return balance; - } - - public boolean set(double amount) { - try { - Economy.setMoney(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public boolean add(double amount) { - try { - Economy.add(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public boolean subtract(double amount) { - try { - Economy.subtract(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public boolean multiply(double amount) { - try { - Economy.multiply(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public boolean divide(double amount) { - try { - Economy.divide(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - return false; - } - - return true; - } - - public boolean hasEnough(double amount) { - try { - return Economy.hasEnough(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - } - - return false; - } - - public boolean hasOver(double amount) { - try { - return Economy.hasMore(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - } - - return false; - } - - public boolean hasUnder(double amount) { - try { - return Economy.hasLess(name, amount); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - } - - return false; - } - - public boolean isNegative() { - try { - return Economy.isNegative(name); - } catch (Exception ex) { - System.out.println("[REGISTER] Error in Essentials Economy: " + ex.getMessage()); - } - - return false; - } - - public boolean remove() { - return false; - } - } -} \ No newline at end of file diff --git a/src/com/garbagemule/register/payment/methods/MCUR.java b/src/com/garbagemule/register/payment/methods/MCUR.java deleted file mode 100644 index c6a758c..0000000 --- a/src/com/garbagemule/register/payment/methods/MCUR.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; - -import me.ashtheking.currency.Currency; -import me.ashtheking.currency.CurrencyList; - -import org.bukkit.plugin.Plugin; - -/** - * MultiCurrency Method implementation. - * - * @author Acrobot - * @copyright (c) 2011 - * @license AOL license - */ -public class MCUR implements Method { - private Currency currencyList; - - public Object getPlugin() { - return this.currencyList; - } - - public String getName() { - return "MultiCurrency"; - } - - public String getVersion() { - return "0.09"; - } - - public int fractionalDigits() { - return -1; - } - - public String format(double amount) { - return amount + " Currency"; - } - - public boolean hasBanks() { - return false; - } - - public boolean hasBank(String bank) { - return false; - } - - public boolean hasAccount(String name) { - return true; - } - - public boolean hasBankAccount(String bank, String name) { - return false; - } - - public boolean createAccount(String name) { - CurrencyList.setValue((String) CurrencyList.maxCurrency(name)[0], name, 0); - return true; - } - - public boolean createAccount(String name, Double balance) { - CurrencyList.setValue((String) CurrencyList.maxCurrency(name)[0], name, balance); - return true; - } - - public MethodAccount getAccount(String name) { - return new MCurrencyAccount(name); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - return null; - } - - public boolean isCompatible(Plugin plugin) { - return (plugin.getDescription().getName().equalsIgnoreCase("Currency") - || plugin.getDescription().getName().equalsIgnoreCase("MultiCurrency")) - && plugin instanceof Currency; - } - - public void setPlugin(Plugin plugin) { - currencyList = (Currency) plugin; - } - - public class MCurrencyAccount implements MethodAccount{ - private String name; - - public MCurrencyAccount(String name) { - this.name = name; - } - - public double balance() { - return CurrencyList.getValue((String) CurrencyList.maxCurrency(name)[0], name); - } - - public boolean set(double amount) { - CurrencyList.setValue((String) CurrencyList.maxCurrency(name)[0], name, amount); - return true; - } - - public boolean add(double amount) { - return CurrencyList.add(name, amount); - } - - public boolean subtract(double amount) { - return CurrencyList.subtract(name, amount); - } - - public boolean multiply(double amount) { - return CurrencyList.multiply(name, amount); - } - - public boolean divide(double amount) { - return CurrencyList.divide(name, amount); - } - - public boolean hasEnough(double amount) { - return CurrencyList.hasEnough(name, amount); - } - - public boolean hasOver(double amount) { - return CurrencyList.hasOver(name, amount); - } - - public boolean hasUnder(double amount) { - return CurrencyList.hasUnder(name, amount); - } - - public boolean isNegative() { - return CurrencyList.isNegative(name); - } - - public boolean remove() { - return CurrencyList.remove(name); - } - } -} diff --git a/src/com/garbagemule/register/payment/methods/iCo4.java b/src/com/garbagemule/register/payment/methods/iCo4.java deleted file mode 100644 index e4a2ede..0000000 --- a/src/com/garbagemule/register/payment/methods/iCo4.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; -import com.nijiko.coelho.iConomy.iConomy; -import com.nijiko.coelho.iConomy.system.Account; - - -import org.bukkit.plugin.Plugin; - -/** - * iConomy 4 Implementation of Method - * - * @author Nijikokun (@nijikokun) - * @copyright (c) 2011 - * @license AOL license - */ -public class iCo4 implements Method { - private iConomy iConomy; - - public iConomy getPlugin() { - return this.iConomy; - } - - public String getName() { - return "iConomy"; - } - - public String getVersion() { - return "4"; - } - - public int fractionalDigits() { - return 2; - } - - public String format(double amount) { - return com.nijiko.coelho.iConomy.iConomy.getBank().format(amount); - } - - public boolean hasBanks() { - return false; - } - - public boolean hasBank(String bank) { - return false; - } - - public boolean hasAccount(String name) { - return com.nijiko.coelho.iConomy.iConomy.getBank().hasAccount(name); - } - - public boolean hasBankAccount(String bank, String name) { - return false; - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - try { - com.nijiko.coelho.iConomy.iConomy.getBank().addAccount(name); - } catch(Exception E) { - return false; - } - - return true; - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - try { - com.nijiko.coelho.iConomy.iConomy.getBank().addAccount(name, balance); - } catch(Exception E) { - return false; - } - - return true; - } - - public MethodAccount getAccount(String name) { - return new iCoAccount(com.nijiko.coelho.iConomy.iConomy.getBank().getAccount(name)); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - return null; - } - - public boolean isCompatible(Plugin plugin) { - return plugin.getDescription().getName().equalsIgnoreCase("iconomy") - && plugin.getClass().getName().equals("com.nijiko.coelho.iConomy.iConomy") - && plugin instanceof iConomy; - } - - public void setPlugin(Plugin plugin) { - iConomy = (iConomy)plugin; - } - - public class iCoAccount implements MethodAccount { - private Account account; - - public iCoAccount(Account account) { - this.account = account; - } - - public Account getiCoAccount() { - return account; - } - - public double balance() { - return this.account.getBalance(); - } - - public boolean set(double amount) { - if(this.account == null) return false; - this.account.setBalance(amount); - return true; - } - - public boolean add(double amount) { - if(this.account == null) return false; - this.account.add(amount); - return true; - } - - public boolean subtract(double amount) { - if(this.account == null) return false; - this.account.subtract(amount); - return true; - } - - public boolean multiply(double amount) { - if(this.account == null) return false; - this.account.multiply(amount); - return true; - } - - public boolean divide(double amount) { - if(this.account == null) return false; - this.account.divide(amount); - return true; - } - - public boolean hasEnough(double amount) { - return this.account.hasEnough(amount); - } - - public boolean hasOver(double amount) { - return this.account.hasOver(amount); - } - - public boolean hasUnder(double amount) { - return (this.balance() < amount); - } - - public boolean isNegative() { - return this.account.isNegative(); - } - - public boolean remove() { - if(this.account == null) return false; - this.account.remove(); - return true; - } - } -} diff --git a/src/com/garbagemule/register/payment/methods/iCo5.java b/src/com/garbagemule/register/payment/methods/iCo5.java deleted file mode 100644 index 77c73bd..0000000 --- a/src/com/garbagemule/register/payment/methods/iCo5.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; -import com.iConomy.iConomy; -import com.iConomy.system.Account; -import com.iConomy.system.BankAccount; -import com.iConomy.system.Holdings; -import com.iConomy.util.Constants; - - -import org.bukkit.plugin.Plugin; - -/** - * iConomy 5 Implementation of Method - * - * @author Nijikokun (@nijikokun) - * @copyright (c) 2011 - * @license AOL license - */ -public class iCo5 implements Method { - private iConomy iConomy; - - public iConomy getPlugin() { - return this.iConomy; - } - - public String getName() { - return "iConomy"; - } - - public String getVersion() { - return "5"; - } - - public int fractionalDigits() { - return 2; - } - - public String format(double amount) { - return com.iConomy.iConomy.format(amount); - } - - public boolean hasBanks() { - return Constants.Banking; - } - - public boolean hasBank(String bank) { - return (hasBanks()) && com.iConomy.iConomy.Banks.exists(bank); - } - - public boolean hasAccount(String name) { - return com.iConomy.iConomy.hasAccount(name); - } - - public boolean hasBankAccount(String bank, String name) { - return (hasBank(bank)) && com.iConomy.iConomy.getBank(bank).hasAccount(name); - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - return com.iConomy.iConomy.Accounts.create(name); - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - if(!com.iConomy.iConomy.Accounts.create(name)) - return false; - - getAccount(name).set(balance); - - return true; - } - - public MethodAccount getAccount(String name) { - return new iCoAccount(com.iConomy.iConomy.getAccount(name)); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - return new iCoBankAccount(com.iConomy.iConomy.getBank(bank).getAccount(name)); - } - - public boolean isCompatible(Plugin plugin) { - return plugin.getDescription().getName().equalsIgnoreCase("iconomy") - && plugin.getClass().getName().equals("com.iConomy.iConomy") - && plugin instanceof iConomy; - } - - public void setPlugin(Plugin plugin) { - iConomy = (iConomy)plugin; - } - - public class iCoAccount implements MethodAccount { - private Account account; - private Holdings holdings; - - public iCoAccount(Account account) { - this.account = account; - this.holdings = account.getHoldings(); - } - - public Account getiCoAccount() { - return account; - } - - public double balance() { - return this.holdings.balance(); - } - - public boolean set(double amount) { - if(this.holdings == null) return false; - this.holdings.set(amount); - return true; - } - - public boolean add(double amount) { - if(this.holdings == null) return false; - this.holdings.add(amount); - return true; - } - - public boolean subtract(double amount) { - if(this.holdings == null) return false; - this.holdings.subtract(amount); - return true; - } - - public boolean multiply(double amount) { - if(this.holdings == null) return false; - this.holdings.multiply(amount); - return true; - } - - public boolean divide(double amount) { - if(this.holdings == null) return false; - this.holdings.divide(amount); - return true; - } - - public boolean hasEnough(double amount) { - return this.holdings.hasEnough(amount); - } - - public boolean hasOver(double amount) { - return this.holdings.hasOver(amount); - } - - public boolean hasUnder(double amount) { - return this.holdings.hasUnder(amount); - } - - public boolean isNegative() { - return this.holdings.isNegative(); - } - - public boolean remove() { - if(this.account == null) return false; - this.account.remove(); - return true; - } - } - - public class iCoBankAccount implements MethodBankAccount { - private BankAccount account; - private Holdings holdings; - - public iCoBankAccount(BankAccount account) { - this.account = account; - this.holdings = account.getHoldings(); - } - - public BankAccount getiCoBankAccount() { - return account; - } - - public String getBankName() { - return this.account.getBankName(); - } - - public int getBankId() { - return this.account.getBankId(); - } - - public double balance() { - return this.holdings.balance(); - } - - public boolean set(double amount) { - if(this.holdings == null) return false; - this.holdings.set(amount); - return true; - } - - public boolean add(double amount) { - if(this.holdings == null) return false; - this.holdings.add(amount); - return true; - } - - public boolean subtract(double amount) { - if(this.holdings == null) return false; - this.holdings.subtract(amount); - return true; - } - - public boolean multiply(double amount) { - if(this.holdings == null) return false; - this.holdings.multiply(amount); - return true; - } - - public boolean divide(double amount) { - if(this.holdings == null) return false; - this.holdings.divide(amount); - return true; - } - - public boolean hasEnough(double amount) { - return this.holdings.hasEnough(amount); - } - - public boolean hasOver(double amount) { - return this.holdings.hasOver(amount); - } - - public boolean hasUnder(double amount) { - return this.holdings.hasUnder(amount); - } - - public boolean isNegative() { - return this.holdings.isNegative(); - } - - public boolean remove() { - if(this.account == null) return false; - this.account.remove(); - return true; - } - } -} \ No newline at end of file diff --git a/src/com/garbagemule/register/payment/methods/iCo6.java b/src/com/garbagemule/register/payment/methods/iCo6.java deleted file mode 100644 index ca013f0..0000000 --- a/src/com/garbagemule/register/payment/methods/iCo6.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.garbagemule.register.payment.methods; - -import com.garbagemule.register.payment.Method; -import com.iCo6.iConomy; -import com.iCo6.system.Account; -import com.iCo6.system.Accounts; -import com.iCo6.system.Holdings; - - -import org.bukkit.plugin.Plugin; - -/** - * iConomy 6 Implementation of Method - * - * @author Nijikokun (@nijikokun) - * @copyright (c) 2011 - * @license AOL license - */ -public class iCo6 implements Method { - private iConomy iConomy; - - public iConomy getPlugin() { - return this.iConomy; - } - - public String getName() { - return "iConomy"; - } - - public String getVersion() { - return "6"; - } - - public int fractionalDigits() { - return 2; - } - - public String format(double amount) { - return com.iCo6.iConomy.format(amount); - } - - public boolean hasBanks() { - return false; - } - - public boolean hasBank(String bank) { - return false; - } - - public boolean hasAccount(String name) { - return (new Accounts()).exists(name); - } - - public boolean hasBankAccount(String bank, String name) { - return false; - } - - public boolean createAccount(String name) { - if(hasAccount(name)) - return false; - - return (new Accounts()).create(name); - } - - public boolean createAccount(String name, Double balance) { - if(hasAccount(name)) - return false; - - return (new Accounts()).create(name, balance); - } - - public MethodAccount getAccount(String name) { - return new iCoAccount((new Accounts()).get(name)); - } - - public MethodBankAccount getBankAccount(String bank, String name) { - return null; - } - - public boolean isCompatible(Plugin plugin) { - return plugin.getDescription().getName().equalsIgnoreCase("iconomy") - && plugin.getClass().getName().equals("com.iCo6.iConomy") - && plugin instanceof iConomy; - } - - public void setPlugin(Plugin plugin) { - iConomy = (iConomy)plugin; - } - - public class iCoAccount implements MethodAccount { - private Account account; - private Holdings holdings; - - public iCoAccount(Account account) { - this.account = account; - this.holdings = account.getHoldings(); - } - - public Account getiCoAccount() { - return account; - } - - public double balance() { - return this.holdings.getBalance(); - } - - public boolean set(double amount) { - if(this.holdings == null) return false; - this.holdings.setBalance(amount); - return true; - } - - public boolean add(double amount) { - if(this.holdings == null) return false; - this.holdings.add(amount); - return true; - } - - public boolean subtract(double amount) { - if(this.holdings == null) return false; - this.holdings.subtract(amount); - return true; - } - - public boolean multiply(double amount) { - if(this.holdings == null) return false; - this.holdings.multiply(amount); - return true; - } - - public boolean divide(double amount) { - if(this.holdings == null) return false; - this.holdings.divide(amount); - return true; - } - - public boolean hasEnough(double amount) { - return this.holdings.hasEnough(amount); - } - - public boolean hasOver(double amount) { - return this.holdings.hasOver(amount); - } - - public boolean hasUnder(double amount) { - return this.holdings.hasUnder(amount); - } - - public boolean isNegative() { - return this.holdings.isNegative(); - } - - public boolean remove() { - if(this.account == null) return false; - this.account.remove(); - return true; - } - } -} diff --git a/test/config.yml b/test/config.yml index 6059fac..7cd8e4d 100644 --- a/test/config.yml +++ b/test/config.yml @@ -76,7 +76,10 @@ arenas: priority: 1 frequency: 1 monsters: - mushroom_cows: 10 + zombies: 30 + skeletons: 18 + spiders: 5 + mushroom_cows: 1 wave: 1 boss1: type: boss diff --git a/test/garbagemule/mobarena/standard/TestNewConfig.java b/test/garbagemule/mobarena/standard/TestNewConfig.java deleted file mode 100644 index 0094471..0000000 --- a/test/garbagemule/mobarena/standard/TestNewConfig.java +++ /dev/null @@ -1,212 +0,0 @@ -package garbagemule.mobarena.standard; - -import static org.junit.Assert.*; - -import java.io.*; -import java.sql.Timestamp; -import java.text.SimpleDateFormat; -import java.util.*; - -import org.apache.commons.io.FileUtils; -import org.bukkit.util.config.Configuration; -import org.junit.*; - -import com.garbagemule.MobArena.util.Config; - -@SuppressWarnings("deprecation") -public class TestNewConfig -{ - private Configuration oldConfig; - private Config newConfig; - private File testFileOld, testFileNew; - - @Before - public void setup() { - testFileOld = makeTestFile("test", "config.yml"); - testFileNew = makeTestFile("test", "config.yml"); - - oldConfig = new Configuration(testFileOld); - newConfig = new Config(testFileNew); - - oldConfig.load(); - newConfig.load(); - } - - @After - public void cleanup() { - testFileOld.delete(); - testFileNew.delete(); - } - - @Test - public void getProperty() { - String path = "arenas.lol.rewards.waves.after.7"; - - Object oldObj = oldConfig.getProperty(path); - Object newObj = newConfig.getProperty(path); - - assertEquals(oldObj, newObj); - } - - @Test - public void getInt() { - String path = "arenas.lol.settings.first-wave-delay"; - int oldInt = oldConfig.getInt(path, -1); - int newInt = newConfig.getInt(path, -2); - - assertEquals(oldInt, newInt); - } - - @Test - public void getString() { - String path = "classes.Knight.items"; - String oldString = oldConfig.getString(path, "old"); - String newString = newConfig.getString(path, "new"); - - assertEquals(oldString, newString); - - path = "classes.Knight.armor"; - oldString = oldConfig.getString(path, "old"); - newString = newConfig.getString(path, "new"); - - assertEquals(oldString, newString); - } - - @Test - public void getBoolean() { - String path = "global-settings.enabled"; - boolean oldBoolean = oldConfig.getBoolean(path, false); - boolean newBoolean = newConfig.getBoolean(path, true); - - assertEquals(oldBoolean, newBoolean); - - // Test with both permutations of the default value. - oldBoolean = oldConfig.getBoolean(path, true); - newBoolean = newConfig.getBoolean(path, false); - - assertEquals(oldBoolean, newBoolean); - } - - @Test - public void getKeys() { - String path = "classes"; - - List oldList = new LinkedList(); - List newList = new LinkedList(); - - oldList.addAll(oldConfig.getKeys(path)); - newList.addAll(newConfig.getKeys(path)); - - // Sort, just in case. - Collections.sort(oldList); - Collections.sort(newList); - - assertEquals(oldList, newList); - } - - @Test - public void getStringList() { - String path = "classes.Knight.permissions"; - - List oldList = oldConfig.getStringList(path, null); - List newList = newConfig.getStringList(path, null); - assertNotNull(oldList); - assertNotNull(newList); - assertFalse(oldList.isEmpty()); - assertFalse(newList.isEmpty()); - - assertEquals(oldList, newList); - - // Empty lists. - path = "classes.Tank.permissions"; - - oldList = oldConfig.getStringList(path, new LinkedList()); - newList = newConfig.getStringList(path, new LinkedList()); - assertNotNull(oldList); - assertNotNull(newList); - assertTrue(oldList.isEmpty()); - assertTrue(newList.isEmpty()); - - assertEquals(oldList, newList); - - // Null values behave the same as empty lists. - oldList = oldConfig.getStringList(path, null); - newList = newConfig.getStringList(path, null); - assertNotNull(oldList); - assertNotNull(newList); - assertTrue(oldList.isEmpty()); - assertTrue(newList.isEmpty()); - - assertEquals(oldList, newList); - - - // Non-existing lists. - path = "classes.Archer.permissions"; - - oldList = oldConfig.getStringList(path, new LinkedList()); - newList = newConfig.getStringList(path, new LinkedList()); - assertNotNull(oldList); - assertNotNull(newList); - assertTrue(oldList.isEmpty()); - assertTrue(newList.isEmpty()); - - assertEquals(oldList, newList); - } - - @Test - public void removeProperty() { - String path = "global-settings.enabled"; - - // Make sure something exists first. - Object oldObj = oldConfig.getProperty(path); - Object newObj = newConfig.getProperty(path); - assertNotNull(oldObj); - assertNotNull(newObj); - - assertEquals(oldObj, newObj); - - // Remove and try again. - oldConfig.removeProperty(path); - newConfig.removeProperty(path); - - // They should now both be null. - oldObj = oldConfig.getProperty(path); - newObj = newConfig.getProperty(path); - assertNull(oldObj); - assertNull(newObj); - - assertEquals(oldObj, newObj); - - // Should be idempotent. - oldConfig.removeProperty(path); - newConfig.removeProperty(path); - - oldObj = oldConfig.getProperty(path); - newObj = newConfig.getProperty(path); - assertNull(oldObj); - assertNull(newObj); - - assertEquals(oldObj, newObj); - } - - private File makeTestFile(String path, String filename) { - // Grab the time - Timestamp timestamp = new Timestamp(new Date().getTime()); - - // Convert to a string - String time = new SimpleDateFormat("MM-dd-yyyy-H_m_s_S").format(timestamp); - - // Make a new file - File testFile = new File(path + File.separator + time + "_" + filename); - - // Start copying the bytes - try { - FileUtils.copyFile(new File(path + File.separator + filename), testFile); - } - catch (Exception e) { - e.printStackTrace(); - } - - return testFile; - } -} diff --git a/test/garbagemule/mobarena/standard/TestUserCommands.java b/test/garbagemule/mobarena/standard/TestUserCommands.java deleted file mode 100644 index a44ed57..0000000 --- a/test/garbagemule/mobarena/standard/TestUserCommands.java +++ /dev/null @@ -1,208 +0,0 @@ -package garbagemule.mobarena.standard; - -import static org.junit.Assert.*; - -import mock.bukkit.*; -import mock.mobarena.*; -import mock.util.MockLogger; - -import org.junit.*; - -import com.garbagemule.MobArena.*; -import com.garbagemule.MobArena.commands.*; - -public class TestUserCommands -{ - private static final String LABEL = "ma"; - - private MockLogger log; - private String expected; - - private MobArenaPlugin plugin; - private ArenaMaster arenaMaster; - private CommandHandler commandHandler; - private MockPlayer player; - - @Before - public void setup() { - // Set up the log.; - log = new MockLogger(); - log.log("Start"); - - // Create a plugin. - plugin = new MockMobArena(); - - // Get the ArenaMaster. - arenaMaster = plugin.getArenaMaster(); - - // Create a player and a command handler. - player = new MockPlayer("garbagemule", log); - commandHandler = new CommandHandler(plugin); - } - - @After - public void cleanup() { - log = null; - plugin = null; - arenaMaster = null; - player = null; - commandHandler = null; - } - - - - /* ********************************************************************* */ - /* - /* JOIN COMMAND - /* - /* ********************************************************************* */ - - // TODO: Expand all tests to check that the player is indeed added and warped. - - @Test - public void expectArguments() { - commandHandler.onCommand(player, null, LABEL, compileArgs()); - expected = MockLogger.compileMsgToPlayer(player, Msg.MISC_NONE); - assertEquals(expected, log.getLastEntry().getValue()); - } - - @Test - public void playerCanJoinExistingArena() { - // Create some arenas. - createArenas("test","test2","test21","test12"); - - // Try to join an existing arena. - commandHandler.onCommand(player, null, LABEL, compileArgs("join","test")); - - // Success! - expected = MockLogger.compileMsgToPlayer(player, Msg.JOIN_PLAYER_JOINED); - assertEquals(expected, log.getLastEntry().getValue()); - - // Verify that the player is now in the lobby. - Arena a = arenaMaster.getArenaWithName("test"); - assertTrue(a.inLobby(player)); - } - - @Test - public void playerCantJoinNonExistingArena() { - // Create some arenas. - createArenas("test","test2","test21","test12"); - - // Try to join one that doesn't exist. - commandHandler.onCommand(player, null, LABEL, compileArgs("join","test1")); - - // Failure! - expected = MockLogger.compileMsgToPlayer(player, Msg.ARENA_DOES_NOT_EXIST); - assertEquals(expected, log.getLastEntry().getValue()); - } - - @Test - public void argsRequiredForMultipleArenas() { - // Create some arenas. - createArenas("test","test2","test21","test12"); - - // Try to join with no arguments. - commandHandler.onCommand(player, null, LABEL, compileArgs("join")); - - // Should join the only existing arena. - expected = MockLogger.compileMsgToPlayer(player, Msg.JOIN_ARG_NEEDED); - assertEquals(expected, log.getLastEntry().getValue()); - } - - @Test - public void noArgsNeededForOneArenaOnly() { - // Create one arena. - createArenas("gjklqwejklewjglkejg"); - - // Try to join with no arguments. - commandHandler.onCommand(player, null, LABEL, compileArgs("join")); - - // Should join the only existing arena. - expected = MockLogger.compileMsgToPlayer(player, Msg.JOIN_PLAYER_JOINED); - assertEquals(expected, log.getLastEntry().getValue()); - - // Verify that the player is now in the lobby. - Arena a = arenaMaster.getArenaWithName("gjklqwejklewjglkejg"); - assertTrue(a.inLobby(player)); - } - - @Test - public void playerCanOnlyJoinPermittedArenas() { - // Create a couple of arenas. - createArenas("yay", "nay"); - - // Give the player no permission for one. - player.setPermission("mobarena.arenas.nay", false); - - // Try to join. - commandHandler.onCommand(player, null, LABEL, compileArgs("join","nay")); - - // No permission. - expected = MockLogger.compileMsgToPlayer(player, Msg.JOIN_ARENA_PERMISSION); - assertEquals(expected, log.getLastEntry().getValue()); - - // Verify that the player didn't join and isn't in the lobby. - Arena a = arenaMaster.getArenaWithName("nay"); - assertFalse(a.inLobby(player)); - } - - - - /* ********************************************************************* */ - /* - /* LIST ARENAS COMMAND - /* - /* ********************************************************************* */ - - @Test - public void shouldListAllArenas() { - // Create some arenas. - createArenas("one", "two", "three", "four"); - - // Try to list them. - commandHandler.onCommand(player, null, LABEL, compileArgs("arenas")); - - // Should show all of them, because player has permission to all per default. - expected = MockLogger.compileMsgToPlayer(player, Msg.MISC_LIST_ARENAS.toString("one, two, three, four")); - assertEquals(expected, log.getLastEntry().getValue()); - } - - @Test - public void shouldListMiscNoneIfNoArenas() { - // Create an arena and remove the permission. - createArenas("one"); - player.setPermission("mobarena.arenas.one", false); - - // Try to list arenas. - commandHandler.onCommand(player, null, LABEL, compileArgs("arenas")); - - // Should display MISC_NONE - String none = Msg.MISC_NONE.toString(); - expected = MockLogger.compileMsgToPlayer(player, Msg.MISC_LIST_ARENAS.toString(none)); - assertEquals(expected, log.getLastEntry().getValue()); - } - - @Test - public void shouldDisplayOnlyPermittedArenas() { - // Create three arenas, remove permissions for one. - createArenas("one", "two", "three"); - player.setPermission("mobarena.arenas.two", false); - - // List arenas. - commandHandler.onCommand(player, null, LABEL, compileArgs("arenas")); - - // Should display one and three. - expected = MockLogger.compileMsgToPlayer(player, Msg.MISC_LIST_ARENAS.toString("one, three")); - assertEquals(expected, log.getLastEntry().getValue()); - } - - private void createArenas(String... args) { - for (String s : args) { - arenaMaster.createArenaNode(s, null); - } - } - - private String[] compileArgs(String... args) { - return args; - } -} diff --git a/test/mock/bukkit/MockPlayer.java b/test/mock/bukkit/MockPlayer.java index 177d59e..22aed64 100644 --- a/test/mock/bukkit/MockPlayer.java +++ b/test/mock/bukkit/MockPlayer.java @@ -9,10 +9,13 @@ import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.entity.*; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.*; import org.bukkit.map.MapView; import org.bukkit.permissions.*; import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; import org.bukkit.util.Vector; public class MockPlayer implements Player @@ -973,4 +976,136 @@ public class MockPlayer implements Player } + @Override + public Player getKiller() + { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean teleport(Location arg0, TeleportCause arg1) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean teleport(Entity arg0, TeleportCause arg1) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public long getFirstPlayed() + { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getLastPlayed() + { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean hasPlayedBefore() + { + // TODO Auto-generated method stub + return false; + } + + @Override + public void playEffect(EntityEffect arg0) { + // TODO Auto-generated method stub + + } + + @Override + public Set getListeningPluginChannels() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void sendPluginMessage(Plugin arg0, String arg1, byte[] arg2) { + // TODO Auto-generated method stub + + } + + //@Override + public boolean getAllowFlight() { + // TODO Auto-generated method stub + return false; + } + + //@Override + public void setAllowFlight(boolean arg0) { + // TODO Auto-generated method stub + + } + + //@Override + public void setBedSpawnLocation(Location arg0) { + // TODO Auto-generated method stub + + } + + @Override + public boolean addPotionEffect(PotionEffect arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean addPotionEffect(PotionEffect arg0, boolean arg1) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean addPotionEffects(Collection arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Collection getActivePotionEffects() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean hasPotionEffect(PotionEffectType arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void removePotionEffect(PotionEffectType arg0) { + // TODO Auto-generated method stub + + } + + @Override + public boolean canSee(Player arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void hidePlayer(Player arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void showPlayer(Player arg0) { + // TODO Auto-generated method stub + + } + } diff --git a/test/mock/mobarena/MockArena.java b/test/mock/mobarena/MockArena.java deleted file mode 100644 index 3255a90..0000000 --- a/test/mock/mobarena/MockArena.java +++ /dev/null @@ -1,540 +0,0 @@ -package mock.mobarena; - -import java.util.Collection; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.entity.Wolf; -//import org.bukkit.util.config.Configuration; - -import com.garbagemule.MobArena.Arena; -import com.garbagemule.MobArena.ArenaPlayer; -import com.garbagemule.MobArena.ArenaPlayerStatistics; -import com.garbagemule.MobArena.MAUtils; -import com.garbagemule.MobArena.MobArena; -import com.garbagemule.MobArena.MobArenaPlugin; -import com.garbagemule.MobArena.Msg; -import com.garbagemule.MobArena.repairable.Repairable; -import com.garbagemule.MobArena.util.Config; -import com.garbagemule.MobArena.waves.BossWave; -import com.garbagemule.MobArena.waves.Wave; - -public class MockArena extends Arena -{ - private MobArenaPlugin plugin; - private String name; - private boolean enabled; - - public MockArena(MobArenaPlugin plugin, String name, World world) { - this.plugin = plugin; - this.name = name; - this.enabled = true; - this.setup = true; - this.running = false; - - this.arenaPlayers = new HashSet(); - this.lobbyPlayers = new HashSet(); - } - - @Override - public boolean startArena() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean endArena() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public void forceStart() - { - // TODO Auto-generated method stub - - } - - @Override - public void forceEnd() - { - // TODO Auto-generated method stub - - } - - @Override - public void playerJoin(Player p, Location loc) { - lobbyPlayers.add(p); - } - - @Override - public void playerReady(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void playerLeave(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void playerDeath(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void playerSpec(Player p, Location loc) - { - // TODO Auto-generated method stub - - } - - @Override - public void playerKill(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void restoreInvAndGiveRewardsDelayed(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void restoreInvAndGiveRewards(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void storePlayerData(Player p, Location loc) - { - // TODO Auto-generated method stub - - } - - @Override - public void storeContainerContents() - { - // TODO Auto-generated method stub - - } - - @Override - public void restoreContainerContents() - { - // TODO Auto-generated method stub - - } - - @Override - public void movePlayerToLobby(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void movePlayerToSpec(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void movePlayerToEntry(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void repairBlocks() - { - // TODO Auto-generated method stub - - } - - @Override - public void queueRepairable(Repairable r) - { - // TODO Auto-generated method stub - - } - - @Override - public void assignClass(Player p, String className) - { - // TODO Auto-generated method stub - - } - - @Override - public void assignRandomClass(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void assignClassPermissions(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void removeClassPermissions(Player p) - { - // TODO Auto-generated method stub - - } - - @Override - public void load(Config config) - { - // TODO Auto-generated method stub - - } - - @Override - public void restoreRegion() - { - // TODO Auto-generated method stub - - } - - @Override - public void serializeConfig() - { - // TODO Auto-generated method stub - - } - - @Override - public void deserializeConfig() - { - // TODO Auto-generated method stub - - } - - @Override - public boolean serializeRegion() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean deserializeRegion() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean inRegion(Location loc) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean inRegionRadius(Location loc, int radius) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean inArena(Player p) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean inLobby(Player p) { - return lobbyPlayers.contains(p); - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public boolean isRunning() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean isPvpEnabled() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean isLightningEnabled() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean isBossWave() - { - // TODO Auto-generated method stub - return false; - } - - @Override - public String configName() { - return name; - } - - @Override - public String arenaName() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public MobArena getPlugin() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public World getWorld() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Wave getWave() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public void setWave(Wave wave) - { - // TODO Auto-generated method stub - - } - - @Override - public void setBossWave(BossWave bossWave) - { - // TODO Auto-generated method stub - - } - - @Override - public Set getClasses() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getAllSpawnpoints() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getSpawnpoints() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Location getBossSpawnpoint() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public int getPlayerCount() - { - // TODO Auto-generated method stub - return 0; - } - - @Override - public void addBlock(Block b) - { - // TODO Auto-generated method stub - - } - - @Override - public void addMonster(LivingEntity e) - { - // TODO Auto-generated method stub - - } - - @Override - public void addExplodingSheep(LivingEntity e) - { - // TODO Auto-generated method stub - - } - - @Override - public List getAllPlayers() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getLivingPlayers() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Set getArenaPlayers() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Collection getArenaPlayerSet() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getArenaPlayerStatistics( - Comparator comparator) - { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getNonreadyPlayers() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Set getMonsters() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public Set getPets() - { - // TODO Auto-generated method stub - return null; - } - - @Override - public void resetIdleTimer() - { - // TODO Auto-generated method stub - - } - - @Override - public void addTrunkAndLeaves(Block b) - { - // TODO Auto-generated method stub - - } - - @Override - public boolean canAfford(Player p) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean takeFee(Player p) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public boolean canJoin(Player p) { - if (!enabled) - plugin.tell(p, Msg.JOIN_ARENA_NOT_ENABLED); - else if (!setup) //|| recurrentWaves.isEmpty()) TODO: Test waves. - plugin.tell(p, Msg.JOIN_ARENA_NOT_SETUP); - else if (edit) - plugin.tell(p, Msg.JOIN_ARENA_EDIT_MODE); - else if (running && (notifyPlayers.contains(p) || notifyPlayers.add(p))) - plugin.tell(p, Msg.JOIN_ARENA_IS_RUNNING); - else if (arenaPlayers.contains(p) || lobbyPlayers.contains(p)) - plugin.tell(p, Msg.JOIN_ALREADY_PLAYING); - else if (!p.hasPermission("mobarena.arenas." + configName())) - plugin.tell(p, Msg.JOIN_ARENA_PERMISSION); - else if (maxPlayers > 0 && lobbyPlayers.size() >= maxPlayers) - plugin.tell(p, Msg.JOIN_PLAYER_LIMIT_REACHED); - else if (joinDistance > 0 && !inRegionRadius(p.getLocation(), joinDistance)) - plugin.tell(p, Msg.JOIN_TOO_FAR); - else if (emptyInvJoin && !MAUtils.hasEmptyInventory(p)) - plugin.tell(p, Msg.JOIN_EMPTY_INV); - else - return true; - - /*else if (!canAfford(p)) - plugin.tell(p, Msg.JOIN_FEE_REQUIRED, MAUtils.listToString(entryFee, plugin));*/ - return false; - } - - @Override - public boolean canSpec(Player p) - { - // TODO Auto-generated method stub - return false; - } - - @Override - public String toString() { - return name; - } -} diff --git a/test/mock/mobarena/MockArenaMaster.java b/test/mock/mobarena/MockArenaMaster.java deleted file mode 100644 index 964c901..0000000 --- a/test/mock/mobarena/MockArenaMaster.java +++ /dev/null @@ -1,232 +0,0 @@ -package mock.mobarena; - -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; - -import com.garbagemule.MobArena.*; - -public class MockArenaMaster extends ArenaMaster -{ - private MobArenaPlugin plugin; - private List arenas; - - public MockArenaMaster(MobArenaPlugin plugin) { - this.plugin = plugin; - this.arenas = new LinkedList(); - } - - @Override - public List getEnabledArenas() { - List result = new LinkedList(); - - for (Arena a : arenas) { - if (a.isEnabled()) - result.add(a); - } - - return result; - } - - @Override - public List getPermittedArenas(Player p) { - List result = new LinkedList(); - - for (Arena a : arenas) { - String perm = "mobarena.arenas." + a.configName(); - - if (!p.isPermissionSet(perm) || p.hasPermission(perm)) - result.add(a); - } - - return result; - } - - @Override - public List getEnabledAndPermittedArenas(Player p) { - List result = new LinkedList(); - - for (Arena a : arenas) { - // We only want enabled arenas. - if (!a.isEnabled()) - continue; - - // And the player must have permission. - String perm = "mobarena.arenas." + a.configName(); - if (!p.isPermissionSet(perm) || p.hasPermission(perm)) - result.add(a); - } - - return result; - } - - @Override - public Arena getArenaAtLocation(Location loc) - { - return null; - } - - @Override - public List getArenasInWorld(World world) - { - return null; - } - - @Override - public List getAllPlayers() - { - return null; - } - - @Override - public List getAllPlayersInArena(String arenaName) - { - return null; - } - - @Override - public List getAllLivingPlayers() - { - return null; - } - - @Override - public List getLivingPlayersInArena(String arenaName) - { - return null; - } - - @Override - public Arena getArenaWithPlayer(Player p) - { - return null; - } - - @Override - public Arena getArenaWithPlayer(String playerName) - { - return null; - } - - @Override - public Arena getArenaWithSpectator(Player p) - { - return null; - } - - @Override - public Arena getArenaWithMonster(Entity e) - { - return null; - } - - @Override - public Arena getArenaWithPet(Entity e) - { - return null; - } - - @Override - public Arena getArenaWithName(String configName) { - return getArenaWithName(this.arenas, configName); - } - - @Override - public Arena getArenaWithName(Collection arenas, String configName) { - for (Arena a : arenas) { - if (a.configName().equals(configName)) - return a; - } - return null; - } - - @Override - public void initialize() - { - - } - - @Override - public void loadSettings() - { - - } - - @Override - public void loadClasses() - { - - } - - @Override - public void loadArenas() - { - - } - - @Override - public Arena createArenaNode(String configName, World world) { - Arena a = new MockArena(plugin, configName, world); - arenas.add(a); - return a; - } - - @Override - public void removeArenaNode(String configName) - { - - } - - @Override - public void update(boolean settings, boolean classes, boolean arenalist) - { - - } - - @Override - public void serializeSettings() - { - - } - - @Override - public void serializeArenas() - { - - } - - @Override - public void deserializeArenas() - { - - } - - @Override - public void updateSettings() - { - - } - - @Override - public void updateClasses() - { - - } - - @Override - public void updateArenas() - { - - } - - @Override - public void updateAll() - { - - } -} diff --git a/test/mock/mobarena/MockMobArena.java b/test/mock/mobarena/MockMobArena.java index 42b7623..2cfc3b8 100644 --- a/test/mock/mobarena/MockMobArena.java +++ b/test/mock/mobarena/MockMobArena.java @@ -1,39 +1,47 @@ package mock.mobarena; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; +import mock.util.MockLogger; -import com.garbagemule.MobArena.ArenaMaster; -import com.garbagemule.MobArena.MobArenaPlugin; -import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.ArenaMaster; +import com.garbagemule.MobArena.util.config.Config; -public class MockMobArena implements MobArenaPlugin +public class MockMobArena //extends MobArena //implements MobArenaPlugin { private ArenaMaster arenaMaster; + private MockLogger log; + private Config config; public MockMobArena() { - this.arenaMaster = new MockArenaMaster(this); + //this.arenaMaster = new MockArenaMaster(this); + this.log = MockLogger.getLogger("MobArenaTest"); + } + + //@Override + public void onEnable() { + //this.config = new Config(new File(getDataFolder(), "config.yml")); + //this.arenaMaster = new ArenaMasterImpl(this); + this.log.log("MockMobArena enabled."); + } + + //@Override + public void onDisable() { + log.log("MockMobArena disabled."); + } + + //@Override + public Config getMAConfig() { + return config; } - public MockMobArena(ArenaMaster arenaMaster) { - this.arenaMaster = arenaMaster; - } - - @Override + //@Override public ArenaMaster getArenaMaster() { return arenaMaster; } - - @Override - public void tell(CommandSender sender, String msg) { - if (sender == null || msg.equals("") || msg.equals(" ")) - return; - - sender.sendMessage(ChatColor.GREEN + "[MobArena] " + ChatColor.WHITE + msg); - } - - @Override - public void tell(CommandSender sender, Msg msg) { - tell(sender, msg.toString()); - } + + //@Override + public void info(String msg) { log.log("[MobArena] " + msg); } + //@Override + public void warning(String msg) { log.log("[MobArena] " + msg); } + //@Override + public void error(String msg) { log.log("[MobArena] " + msg); } } diff --git a/test/mock/util/MockLogger.java b/test/mock/util/MockLogger.java index c2ce56c..c6c3939 100644 --- a/test/mock/util/MockLogger.java +++ b/test/mock/util/MockLogger.java @@ -3,18 +3,38 @@ package mock.util; import java.sql.Timestamp; import java.util.*; import java.util.Map.Entry; +import java.util.logging.Logger; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import com.garbagemule.MobArena.Msg; -public class MockLogger +public class MockLogger extends Logger { - private TreeMap entries; + private static Map loggers = new TreeMap(); + private TreeMap entries = new TreeMap(); - public MockLogger() { + /** + * Private constructor to ensure that all MockLoggers exist within this class as singletons. + */ + /*private MockLogger() { this.entries = new TreeMap(); + }*/ + protected MockLogger(String name, String resourceBundleName) { + super(name, resourceBundleName); + } + + /** + * All MockLogger objects are singletons. + * @param name the name of a MockLogger + * @return the MockLogger with the input name, or a new MockLogger if it didn't already exist. + */ + public static MockLogger getLogger(String name) { + if (!loggers.containsKey(name)) + loggers.put(name, new MockLogger(name, null)); + + return loggers.get(name); } public void log(String msg) {