From 1592d0b2a4dbbf8e352e0fd53ac1e872fddd2b15 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sat, 15 Mar 2025 08:16:56 +0100 Subject: [PATCH] Move files around --- .../solstice/modules/ModuleProvider.java | 127 ++++++ .../solstice/modules/admin/AdminModule.java | 43 +++ .../solstice/modules/afk/AfkModule.java | 361 ++++++++++++++++++ .../afk/commands/ActiveTimeCommand.java | 122 ++++++ .../modules/afk/commands/AfkCommand.java | 33 ++ .../solstice/modules/afk/data/AfkConfig.java | 52 +++ .../solstice/modules/afk/data/AfkLocale.java | 17 + .../modules/afk/data/AfkPlayerData.java | 8 + .../modules/afk/data/AfkServerData.java | 9 + .../modules/afk/data/LeaderboardEntry.java | 35 ++ .../modules/afk/data/PlayerActivityState.java | 23 ++ .../AutoAnnouncementModule.java | 79 ++++ .../data/AutoAnnouncementConfig.java | 31 ++ .../solstice/modules/back/BackModule.java | 44 +++ .../modules/back/commands/BackCommand.java | 52 +++ .../modules/back/data/BackLocale.java | 10 + .../solstice/modules/ban/BanModule.java | 25 ++ .../modules/ban/commands/BanCommand.java | 92 +++++ .../modules/ban/commands/TempBanCommand.java | 57 +++ .../modules/ban/commands/UnbanCommand.java | 69 ++++ .../solstice/modules/ban/data/BanLocale.java | 10 + .../ban/formatters/BanMessageFormatter.java | 33 ++ .../modules/broadcast/BroadcastModule.java | 23 ++ .../broadcast/commands/BroadcastCommand.java | 50 +++ .../commands/PlainBroadcastCommand.java | 42 ++ .../broadcast/data/BroadcastConfig.java | 10 + .../modules/commandSpy/CommandSpyModule.java | 61 +++ .../commandSpy/data/CommandSpyConfig.java | 15 + .../commandSpy/data/CommandSpyLocale.java | 9 + .../modules/customName/CustomNameModule.java | 98 +++++ .../customName/commands/NicknameCommand.java | 77 ++++ .../customName/data/CustomNameConfig.java | 20 + .../customName/data/CustomNamePlayerData.java | 7 + .../modules/enderchest/EnderChestModule.java | 21 + .../commands/EnderChestCommand.java | 120 ++++++ .../enderchest/data/EnderChestLocale.java | 11 + .../experiments/ExperimentsModule.java | 31 ++ .../experiments/commands/FlagsCommand.java | 59 +++ .../experiments/commands/TimeSpanCommand.java | 42 ++ .../modules/extinguish/ExtinguishModule.java | 17 + .../commands/ExtinguishCommand.java | 56 +++ .../solstice/modules/feed/FeedModule.java | 17 + .../modules/feed/commands/FeedCommand.java | 61 +++ .../solstice/modules/fly/FlyModule.java | 35 ++ .../modules/fly/commands/FlyCommand.java | 81 ++++ .../solstice/modules/fly/data/FlyLocale.java | 12 + .../modules/fly/data/FlyPlayerData.java | 5 + .../solstice/modules/god/GodModule.java | 35 ++ .../modules/god/commands/GodCommand.java | 81 ++++ .../solstice/modules/god/data/GodLocale.java | 12 + .../modules/god/data/GodPlayerData.java | 5 + .../solstice/modules/hat/HatModule.java | 58 +++ .../modules/hat/commands/HatCommand.java | 63 +++ .../solstice/modules/hat/data/HatConfig.java | 18 + .../solstice/modules/hat/data/HatLocale.java | 11 + .../solstice/modules/heal/HealModule.java | 17 + .../modules/heal/commands/HealCommand.java | 66 ++++ .../solstice/modules/helpOp/HelpOpModule.java | 21 + .../helpOp/commands/HelpOpCommand.java | 64 ++++ .../modules/helpOp/data/HelpOpLocale.java | 10 + .../solstice/modules/home/HomeModule.java | 39 ++ .../home/commands/DeleteHomeCommand.java | 74 ++++ .../modules/home/commands/HomeCommand.java | 79 ++++ .../home/commands/HomeOtherCommand.java | 76 ++++ .../modules/home/commands/HomesCommand.java | 133 +++++++ .../modules/home/commands/SetHomeCommand.java | 109 ++++++ .../modules/home/data/HomeConfig.java | 16 + .../modules/home/data/HomeLocale.java | 24 ++ .../modules/home/data/HomePlayerData.java | 11 + .../solstice/modules/ignite/IgniteModule.java | 17 + .../ignite/commands/IgniteCommand.java | 70 ++++ .../solstice/modules/ignore/IgnoreModule.java | 42 ++ .../ignore/commands/IgnoreCommand.java | 76 ++++ .../ignore/commands/IgnoreListCommand.java | 79 ++++ .../modules/ignore/data/IgnoreLocale.java | 16 + .../modules/ignore/data/IgnorePlayerData.java | 8 + .../solstice/modules/info/InfoModule.java | 135 +++++++ .../modules/info/commands/InfoCommand.java | 95 +++++ .../modules/info/commands/MotdCommand.java | 34 ++ .../modules/info/commands/RulesCommand.java | 33 ++ .../modules/info/data/InfoConfig.java | 10 + .../modules/info/data/InfoLocale.java | 14 + .../modules/inventorySee/ImmutableSlot.java | 28 ++ .../inventorySee/InventorySeeModule.java | 21 + .../commands/InventorySeeCommand.java | 215 +++++++++++ .../inventorySee/data/InventorySeeLocale.java | 14 + .../solstice/modules/item/ItemModule.java | 26 ++ .../item/commands/ItemLoreCommand.java | 75 ++++ .../item/commands/ItemNameCommand.java | 64 ++++ .../modules/item/commands/MoreCommand.java | 40 ++ .../modules/item/commands/RepairCommand.java | 54 +++ .../modules/item/data/ItemLocale.java | 17 + .../solstice/modules/jail/JailModule.java | 230 +++++++++++ .../jail/commands/CheckJailCommand.java | 103 +++++ .../modules/jail/commands/JailCommand.java | 132 +++++++ .../modules/jail/commands/JailsCommand.java | 149 ++++++++ .../modules/jail/commands/UnjailCommand.java | 51 +++ .../modules/jail/data/JailConfig.java | 21 + .../modules/jail/data/JailLocale.java | 49 +++ .../modules/jail/data/JailPlayerData.java | 18 + .../modules/jail/data/JailServerData.java | 10 + .../solstice/modules/kick/KickModule.java | 17 + .../modules/kick/commands/KickCommand.java | 64 ++++ .../solstice/modules/kit/KitInventory.java | 61 +++ .../solstice/modules/kit/KitModule.java | 122 ++++++ .../alexdevs/solstice/modules/kit/Utils.java | 52 +++ .../modules/kit/commands/KitCommand.java | 347 +++++++++++++++++ .../modules/kit/commands/KitsCommand.java | 86 +++++ .../solstice/modules/kit/data/Kit.java | 48 +++ .../solstice/modules/kit/data/KitConfig.java | 10 + .../solstice/modules/kit/data/KitLocale.java | 31 ++ .../modules/kit/data/KitPlayerData.java | 8 + .../modules/kit/data/KitServerData.java | 8 + .../solstice/modules/mail/MailModule.java | 73 ++++ .../modules/mail/commands/MailCommand.java | 189 +++++++++ .../modules/mail/data/MailLocale.java | 24 ++ .../modules/mail/data/MailPlayerData.java | 9 + .../modules/miscellaneous/DummyExplosion.java | 22 ++ .../miscellaneous/MiscellaneousModule.java | 74 ++++ .../commands/EffectsCommand.java | 73 ++++ .../commands/KittyCannonCommand.java | 53 +++ .../miscellaneous/commands/NudgeCommand.java | 69 ++++ .../miscellaneous/commands/RocketCommand.java | 76 ++++ .../miscellaneous/commands/SleepCommand.java | 46 +++ .../miscellaneous/commands/TopCommand.java | 40 ++ .../data/MiscellaneousLocale.java | 12 + .../solstice/modules/mute/MuteModule.java | 46 +++ .../modules/mute/commands/MuteCommand.java | 50 +++ .../modules/mute/commands/UnmuteCommand.java | 48 +++ .../modules/mute/data/MuteLocale.java | 9 + .../modules/mute/data/MutePlayerData.java | 5 + .../solstice/modules/near/NearModule.java | 23 ++ .../modules/near/commands/NearCommand.java | 99 +++++ .../modules/near/data/NearConfig.java | 13 + .../modules/near/data/NearLocale.java | 12 + .../solstice/modules/note/NoteModule.java | 75 ++++ .../modules/note/commands/NotesCommand.java | 205 ++++++++++ .../solstice/modules/note/data/Note.java | 16 + .../modules/note/data/NoteConfig.java | 10 + .../modules/note/data/NoteLocale.java | 23 ++ .../modules/note/data/NotePlayerData.java | 9 + .../notifications/NotificationsModule.java | 110 ++++++ .../commands/NotificationsCommand.java | 208 ++++++++++ .../data/NotificationsConfig.java | 17 + .../data/NotificationsLocale.java | 31 ++ .../data/NotificationsPlayerData.java | 17 + .../data/PlayerNotificationSettings.java | 4 + .../placeholders/PlaceholdersModule.java | 42 ++ .../solstice/modules/powertool/Action.java | 21 + .../modules/powertool/PowerToolModule.java | 154 ++++++++ .../powertool/commands/PowerToolCommand.java | 181 +++++++++ .../powertool/data/PowerToolLocale.java | 16 + .../powertool/data/PowerToolPlayerData.java | 10 + .../modules/restart/RestartModule.java | 219 +++++++++++ .../restart/commands/RestartCommand.java | 97 +++++ .../modules/restart/data/RestartConfig.java | 48 +++ .../modules/restart/data/RestartLocale.java | 11 + .../solstice/modules/rtp/RTPModule.java | 49 +++ .../modules/rtp/commands/RTPCommand.java | 166 ++++++++ .../solstice/modules/rtp/core/Locator.java | 249 ++++++++++++ .../solstice/modules/rtp/data/RTPConfig.java | 88 +++++ .../solstice/modules/rtp/data/RTPLocale.java | 15 + .../solstice/modules/seen/SeenModule.java | 21 + .../modules/seen/commands/SeenCommand.java | 106 +++++ .../modules/seen/data/SeenLocale.java | 32 ++ .../solstice/modules/sign/SignModule.java | 34 ++ .../solstice/modules/skull/SkullModule.java | 47 +++ .../modules/skull/commands/SkullCommand.java | 41 ++ .../solstice/modules/smite/SmiteModule.java | 17 + .../modules/smite/commands/SmiteCommand.java | 91 +++++ .../solstice/modules/spawn/SpawnModule.java | 124 ++++++ .../spawn/commands/FirstSpawnCommand.java | 69 ++++ .../spawn/commands/SetFirstSpawnCommand.java | 51 +++ .../spawn/commands/SetSpawnCommand.java | 47 +++ .../modules/spawn/commands/SpawnCommand.java | 92 +++++ .../modules/spawn/data/SpawnConfig.java | 29 ++ .../modules/spawn/data/SpawnLocale.java | 14 + .../modules/spawn/data/SpawnServerData.java | 11 + .../modules/staffChat/StaffChatModule.java | 69 ++++ .../staffChat/commands/StaffChatCommand.java | 46 +++ .../staffChat/data/StaffChatLocale.java | 11 + .../modules/styling/CustomSentMessage.java | 64 ++++ .../modules/styling/StylingModule.java | 41 ++ .../modules/styling/data/StylingConfig.java | 51 +++ .../formatters/AdvancementFormatter.java | 38 ++ .../styling/formatters/ChatFormatter.java | 36 ++ .../ConnectionActivityFormatter.java | 39 ++ .../styling/formatters/DeathFormatter.java | 24 ++ .../styling/formatters/EmoteFormatter.java | 32 ++ .../solstice/modules/sudo/SudoModule.java | 19 + .../modules/sudo/commands/DoAsCommand.java | 93 +++++ .../modules/sudo/commands/SudoCommand.java | 82 ++++ .../modules/suicide/SuicideModule.java | 17 + .../suicide/commands/SuicideCommand.java | 33 ++ .../modules/tablist/TabListModule.java | 73 ++++ .../modules/tablist/data/TabListConfig.java | 32 ++ .../teleportHere/TeleportHereModule.java | 17 + .../commands/TeleportHereCommand.java | 59 +++ .../TeleportOfflineModule.java | 17 + .../commands/TeleportOfflineCommand.java | 52 +++ .../TeleportPositionModule.java | 16 + .../commands/TeleportPositionCommand.java | 74 ++++ .../TeleportRequestModule.java | 172 +++++++++ .../commands/TeleportAcceptCommand.java | 61 +++ .../commands/TeleportAskCommand.java | 48 +++ .../commands/TeleportAskHereCommand.java | 48 +++ .../commands/TeleportDenyCommand.java | 61 +++ .../modules/teleportRequest/data/Request.java | 37 ++ .../teleportRequest/data/TeleportConfig.java | 10 + .../teleportRequest/data/TeleportLocale.java | 20 + .../solstice/modules/tell/TellModule.java | 143 +++++++ .../modules/tell/commands/ReplyCommand.java | 54 +++ .../modules/tell/commands/TellCommand.java | 59 +++ .../modules/tell/data/TellLocale.java | 13 + .../solstice/modules/timeBar/TimeBar.java | 105 +++++ .../modules/timeBar/TimeBarModule.java | 90 +++++ .../timeBar/commands/TimeBarCommand.java | 134 +++++++ .../solstice/modules/trash/TrashModule.java | 21 + .../modules/trash/commands/TrashCommand.java | 38 ++ .../modules/trash/data/TrashLocale.java | 9 + .../modules/utilities/UtilitiesModule.java | 23 ++ .../utilities/commands/AnvilCommand.java | 42 ++ .../commands/CartographyCommand.java | 42 ++ .../utilities/commands/GrindstoneCommand.java | 42 ++ .../utilities/commands/LoomCommand.java | 41 ++ .../utilities/commands/SmithingCommand.java | 42 ++ .../commands/StonecutterCommand.java | 42 ++ .../utilities/commands/WorkbenchCommand.java | 42 ++ .../VirtualAnvilScreenHandler.java | 17 + .../VirtualCartographyTableScreenHandler.java | 17 + .../VirtualCraftingScreenHandler.java | 17 + .../VirtualGrindstoneScreenHandler.java | 17 + .../VirtualLoomScreenHandler.java | 17 + .../VirtualSmithingScreenHandler.java | 17 + .../VirtualStonecutterScreenHandler.java | 17 + .../solstice/modules/warp/WarpModule.java | 35 ++ .../warp/commands/DeleteWarpCommand.java | 69 ++++ .../modules/warp/commands/SetWarpCommand.java | 57 +++ .../modules/warp/commands/WarpCommand.java | 89 +++++ .../modules/warp/commands/WarpsCommand.java | 78 ++++ .../modules/warp/data/WarpLocale.java | 17 + .../modules/warp/data/WarpServerData.java | 9 + 242 files changed, 13063 insertions(+) create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ModuleProvider.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/admin/AdminModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/AfkModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/commands/ActiveTimeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/commands/AfkCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkServerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/LeaderboardEntry.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/afk/data/PlayerActivityState.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/AutoAnnouncementModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/data/AutoAnnouncementConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/back/BackModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/back/commands/BackCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/back/data/BackLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/BanModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/commands/BanCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/commands/TempBanCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/commands/UnbanCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/data/BanLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ban/formatters/BanMessageFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/broadcast/BroadcastModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/BroadcastCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/PlainBroadcastCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/broadcast/data/BroadcastConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/commandSpy/CommandSpyModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/customName/CustomNameModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/customName/commands/NicknameCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNameConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNamePlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/enderchest/EnderChestModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/enderchest/commands/EnderChestCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/enderchest/data/EnderChestLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/experiments/ExperimentsModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/FlagsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/TimeSpanCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/extinguish/ExtinguishModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/extinguish/commands/ExtinguishCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/feed/FeedModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/feed/commands/FeedCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/fly/FlyModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/fly/commands/FlyCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/god/GodModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/god/commands/GodCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/god/data/GodLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/god/data/GodPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/hat/HatModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/hat/commands/HatCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/heal/HealModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/heal/commands/HealCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/helpOp/HelpOpModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/helpOp/commands/HelpOpCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/helpOp/data/HelpOpLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/HomeModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/commands/DeleteHomeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeOtherCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomesCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/commands/SetHomeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/home/data/HomePlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignite/IgniteModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignite/commands/IgniteCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignore/IgnoreModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreListCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnoreLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnorePlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/InfoModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/commands/InfoCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/commands/MotdCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/commands/RulesCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/inventorySee/ImmutableSlot.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/inventorySee/InventorySeeModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/inventorySee/commands/InventorySeeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/inventorySee/data/InventorySeeLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/ItemModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemLoreCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemNameCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/commands/MoreCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/commands/RepairCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/item/data/ItemLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/JailModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/commands/CheckJailCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/commands/UnjailCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailServerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kick/KickModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kick/commands/KickCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/KitInventory.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/KitModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/Utils.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/data/Kit.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitServerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mail/MailModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mail/commands/MailCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/DummyExplosion.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/MiscellaneousModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/EffectsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/KittyCannonCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/NudgeCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/RocketCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/SleepCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/TopCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/data/MiscellaneousLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mute/MuteModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mute/commands/MuteCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mute/commands/UnmuteCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mute/data/MuteLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/mute/data/MutePlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/near/NearModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/near/commands/NearCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/near/data/NearConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/near/data/NearLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/NoteModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/commands/NotesCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/data/Note.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/note/data/NotePlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/NotificationsModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/commands/NotificationsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/notifications/data/PlayerNotificationSettings.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/placeholders/PlaceholdersModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/powertool/Action.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/powertool/PowerToolModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/powertool/commands/PowerToolCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolPlayerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/restart/RestartModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/restart/commands/RestartCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/rtp/RTPModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/rtp/commands/RTPCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/rtp/core/Locator.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/seen/SeenModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/seen/commands/SeenCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/seen/data/SeenLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/sign/SignModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/skull/SkullModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/skull/commands/SkullCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/smite/SmiteModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/smite/commands/SmiteCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/SpawnModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/FirstSpawnCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetFirstSpawnCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetSpawnCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SpawnCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnServerData.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/staffChat/StaffChatModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/staffChat/commands/StaffChatCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/staffChat/data/StaffChatLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/CustomSentMessage.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/StylingModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/data/StylingConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/AdvancementFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ChatFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ConnectionActivityFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/DeathFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/EmoteFormatter.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/sudo/SudoModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/DoAsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/SudoCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/suicide/SuicideModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/suicide/commands/SuicideCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tablist/TabListModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tablist/data/TabListConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportHere/TeleportHereModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportHere/commands/TeleportHereCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/TeleportOfflineModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/commands/TeleportOfflineCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/TeleportPositionModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/commands/TeleportPositionCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/TeleportRequestModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAcceptCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskHereCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportDenyCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/Request.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportConfig.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tell/TellModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tell/commands/ReplyCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tell/commands/TellCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/tell/data/TellLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBar.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBarModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/timeBar/commands/TimeBarCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/trash/TrashModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/trash/commands/TrashCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/trash/data/TrashLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/UtilitiesModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/AnvilCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/CartographyCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/GrindstoneCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/LoomCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/SmithingCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/StonecutterCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/WorkbenchCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualAnvilScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCartographyTableScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCraftingScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualGrindstoneScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualLoomScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualSmithingScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualStonecutterScreenHandler.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/WarpModule.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/commands/DeleteWarpCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/commands/SetWarpCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpsCommand.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpLocale.java create mode 100644 common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpServerData.java diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ModuleProvider.java b/common/src/main/java/me/alexdevs/solstice/modules/ModuleProvider.java new file mode 100644 index 0000000..0138b6c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ModuleProvider.java @@ -0,0 +1,127 @@ +package me.alexdevs.solstice.modules; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.module.ModuleEntrypoint; +import me.alexdevs.solstice.modules.admin.AdminModule; +import me.alexdevs.solstice.modules.afk.AfkModule; +import me.alexdevs.solstice.modules.autoAnnouncement.AutoAnnouncementModule; +import me.alexdevs.solstice.modules.item.ItemModule; +import me.alexdevs.solstice.modules.jail.JailModule; +import me.alexdevs.solstice.modules.kit.KitModule; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import me.alexdevs.solstice.modules.note.NoteModule; +import me.alexdevs.solstice.modules.notifications.NotificationsModule; +import me.alexdevs.solstice.modules.placeholders.PlaceholdersModule; +import me.alexdevs.solstice.modules.powertool.PowerToolModule; +import me.alexdevs.solstice.modules.restart.RestartModule; +import me.alexdevs.solstice.modules.back.BackModule; +import me.alexdevs.solstice.modules.ban.BanModule; +import me.alexdevs.solstice.modules.broadcast.BroadcastModule; +import me.alexdevs.solstice.modules.commandSpy.CommandSpyModule; +import me.alexdevs.solstice.modules.customName.CustomNameModule; +import me.alexdevs.solstice.modules.enderchest.EnderChestModule; +import me.alexdevs.solstice.modules.experiments.ExperimentsModule; +import me.alexdevs.solstice.modules.extinguish.ExtinguishModule; +import me.alexdevs.solstice.modules.feed.FeedModule; +import me.alexdevs.solstice.modules.fly.FlyModule; +import me.alexdevs.solstice.modules.rtp.RTPModule; +import me.alexdevs.solstice.modules.sign.SignModule; +import me.alexdevs.solstice.modules.god.GodModule; +import me.alexdevs.solstice.modules.hat.HatModule; +import me.alexdevs.solstice.modules.heal.HealModule; +import me.alexdevs.solstice.modules.helpOp.HelpOpModule; +import me.alexdevs.solstice.modules.home.HomeModule; +import me.alexdevs.solstice.modules.ignite.IgniteModule; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.info.InfoModule; +import me.alexdevs.solstice.modules.inventorySee.InventorySeeModule; +import me.alexdevs.solstice.modules.kick.KickModule; +import me.alexdevs.solstice.modules.mail.MailModule; +import me.alexdevs.solstice.modules.mute.MuteModule; +import me.alexdevs.solstice.modules.near.NearModule; +import me.alexdevs.solstice.modules.seen.SeenModule; +import me.alexdevs.solstice.modules.skull.SkullModule; +import me.alexdevs.solstice.modules.smite.SmiteModule; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import me.alexdevs.solstice.modules.staffChat.StaffChatModule; +import me.alexdevs.solstice.modules.styling.StylingModule; +import me.alexdevs.solstice.modules.sudo.SudoModule; +import me.alexdevs.solstice.modules.suicide.SuicideModule; +import me.alexdevs.solstice.modules.tablist.TabListModule; +import me.alexdevs.solstice.modules.teleportHere.TeleportHereModule; +import me.alexdevs.solstice.modules.teleportOffline.TeleportOfflineModule; +import me.alexdevs.solstice.modules.teleportPosition.TeleportPositionModule; +import me.alexdevs.solstice.modules.teleportRequest.TeleportRequestModule; +import me.alexdevs.solstice.modules.tell.TellModule; +import me.alexdevs.solstice.modules.timeBar.TimeBarModule; +import me.alexdevs.solstice.modules.trash.TrashModule; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.warp.WarpModule; + +import java.util.HashSet; +import java.util.List; + +public class ModuleProvider implements ModuleEntrypoint { + private static final List modules = List.of( + new AdminModule(), + new AfkModule(), + new AutoAnnouncementModule(), + new RestartModule(), + new BackModule(), + new BanModule(), + new BroadcastModule(), + new CommandSpyModule(), + new CustomNameModule(), + new EnderChestModule(), + new ExperimentsModule(), + new ExtinguishModule(), + new FeedModule(), + new FlyModule(), + new SignModule(), + new GodModule(), + new HatModule(), + new HealModule(), + new HelpOpModule(), + new HomeModule(), + new IgniteModule(), + new IgnoreModule(), + new InfoModule(), + new InventorySeeModule(), + new ItemModule(), + new JailModule(), + new KickModule(), + new KitModule(), + new MailModule(), + new MiscellaneousModule(), + new MuteModule(), + new NearModule(), + new NoteModule(), + new NotificationsModule(), + new PlaceholdersModule(), + new PowerToolModule(), + new RTPModule(), + new SeenModule(), + new SkullModule(), + new SmiteModule(), + new SpawnModule(), + new StaffChatModule(), + new StylingModule(), + new SudoModule(), + new SuicideModule(), + new TabListModule(), + new TeleportHereModule(), + new TeleportOfflineModule(), + new TeleportPositionModule(), + new TeleportRequestModule(), + new TellModule(), + new TimeBarModule(), + new TrashModule(), + new UtilitiesModule(), + new WarpModule() + ); + + @Override + public HashSet register() { + return new HashSet<>(modules); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/admin/AdminModule.java b/common/src/main/java/me/alexdevs/solstice/modules/admin/AdminModule.java new file mode 100644 index 0000000..06b37da --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/admin/AdminModule.java @@ -0,0 +1,43 @@ +package me.alexdevs.solstice.modules.admin; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.PlayerConnectionEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.lucko.fabric.api.permissions.v0.Permissions; + +public class AdminModule extends ModuleBase { + public static final String ID = "admin"; + + public AdminModule() { + super(ID); + } + + @Override + public void init() { + PlayerConnectionEvents.WHITELIST_BYPASS.register(profile -> { + try { + return Permissions.check(profile, getWhitelistBypassPermission(), false).get(); + } catch (Exception e) { + Solstice.LOGGER.error("Error checking whitelist bypass for profile {}", profile.getId(), e); + } + return false; + }); + + PlayerConnectionEvents.FULL_SERVER_BYPASS.register(profile -> { + try { + return Permissions.check(profile, getFullServerBypassPermission(), false).get(); + } catch (Exception e) { + Solstice.LOGGER.error("Error checking full server bypass for profile {}", profile.getId(), e); + } + return false; + }); + } + + public String getWhitelistBypassPermission() { + return getPermissionNode("bypass.whitelist"); + } + + public String getFullServerBypassPermission() { + return getPermissionNode("bypass.fullserver"); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/AfkModule.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/AfkModule.java new file mode 100644 index 0000000..0a7e20b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/AfkModule.java @@ -0,0 +1,361 @@ +package me.alexdevs.solstice.modules.afk; + +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.placeholders.api.PlaceholderResult; +import eu.pb4.placeholders.api.Placeholders; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.events.CommandEvents; +import me.alexdevs.solstice.api.events.PlayerActivityEvents; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.modules.afk.commands.ActiveTimeCommand; +import me.alexdevs.solstice.modules.afk.commands.AfkCommand; +import me.alexdevs.solstice.modules.afk.data.*; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.player.*; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +public class AfkModule extends ModuleBase.Toggleable { + public static final String ID = "afk"; + + public static final double sprintSpeed = 0.280617; + public static final double walkSpeed = 0.215859; + public static final double sneakSpeed = 0.0841; + + public static final int LEADERBOARD_SIZE = 10; + + public enum AfkTriggerReason { + MANUAL, + MOVEMENT, + LOOK_CHANGE, + CHAT_MESSAGE, + COMMAND, + BLOCK_ATTACK, + BLOCK_INTERACT, + ENTITY_ATTACK, + ENTITY_INTERACT, + ITEM_USE, + } + + private final Map activities = new ConcurrentHashMap<>(); + + public AfkModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, AfkConfig.class, AfkConfig::new); + Solstice.localeManager.registerModule(ID, AfkLocale.MODULE); + Solstice.playerData.registerData(ID, AfkPlayerData.class, AfkPlayerData::new); + Solstice.serverData.registerData(ID, AfkServerData.class, AfkServerData::new); + + this.commands.add(new AfkCommand(this)); + this.commands.add(new ActiveTimeCommand(this)); + + Placeholders.register(ResourceLocation.fromNamespaceAndPath(Solstice.MOD_ID, "afk"), (context, arg) -> { + if (!context.hasPlayer()) + return PlaceholderResult.invalid("No player!"); + + var player = context.player(); + + if (isPlayerAfk(player)) + return PlaceholderResult.value(Format.parse(getConfig().tag)); + else + return PlaceholderResult.value(""); + }); + + SolsticeEvents.READY.register((instance, server) -> { + Solstice.scheduler.scheduleAtFixedRate(this::updateActiveTime, 0, 1, TimeUnit.SECONDS); + calculateLeaderboard(); + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + activities.put(handler.getPlayer().getUUID(), new PlayerActivityState(handler.getPlayer(), server.getTickCount())); + }); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + activities.remove(handler.getPlayer().getUUID()); + }); + + ServerTickEvents.END_SERVER_TICK.register(this::tick); + + PlayerActivityEvents.AFK.register((player) -> { + var config = getConfig(); + + if (player.serverLevel().canSleepThroughNights()) { + player.serverLevel().updateSleepingPlayerList(); + } + + Solstice.LOGGER.info("{} is AFK. Active time: {} seconds.", player.getGameProfile().getName(), getActiveTime(player.getUUID())); + if (!config.announce) + return; + + var playerContext = PlaceholderContext.of(player); + + Solstice.getInstance().broadcast(locale().get("goneAfk", playerContext)); + }); + + PlayerActivityEvents.AFK_RETURN.register((player, reason) -> { + var config = getConfig(); + + if (player.serverLevel().canSleepThroughNights()) { + player.serverLevel().updateSleepingPlayerList(); + } + + Solstice.LOGGER.info("{} is no longer AFK due to {}. Active time: {} seconds.", player.getGameProfile().getName(), reason.name(), getActiveTime(player.getUUID())); + if (!config.announce) + return; + + var playerContext = PlaceholderContext.of(player); + + Solstice.getInstance().broadcast(locale().get("returnAfk", playerContext)); + }); + + registerTriggers(); + } + + public AfkServerData getServerData() { + return Solstice.serverData.getData(AfkServerData.class); + } + + private void updateActiveTime() { + var activePlayers = Solstice.server.getPlayerList().getPlayers() + .stream().filter(player -> !isPlayerAfk(player)); + + activePlayers.forEach(player -> { + var activity = activities.computeIfAbsent(player.getUUID(), uuid -> new PlayerActivityState(player, player.getServer().getTickCount())); + if (!activity.activeTimeEnabled) + return; + + var playerData = getPlayerData(player.getUUID()); + playerData.activeTime++; + tryInsertLeaderboard(player, playerData.activeTime); + }); + } + + private void tryInsertLeaderboard(ServerPlayer player, int activeTime) { + var serverData = getServerData(); + var leaderboard = serverData.leaderboard; + + // if in list, update + var entry = leaderboard.stream().filter(e -> e.uuid().equals(player.getUUID())).findFirst(); + if (entry.isPresent()) { + entry.get().activeTime(activeTime); + entry.get().name(player.getGameProfile().getName()); + leaderboard.sort((o1, o2) -> Integer.compare(o2.activeTime(), o1.activeTime())); + return; + } + + // if not in list, insert + var smallest = leaderboard.stream().min(Comparator.comparingInt(LeaderboardEntry::activeTime)); + if (smallest.isPresent()) { + if (smallest.get().activeTime() < activeTime) { + leaderboard.remove(smallest.get()); + leaderboard.add(new LeaderboardEntry(player.getGameProfile().getName(), player.getUUID(), activeTime)); + leaderboard.sort((o1, o2) -> Integer.compare(o2.activeTime(), o1.activeTime())); + } + } else { + leaderboard.add(new LeaderboardEntry(player.getGameProfile().getName(), player.getUUID(), activeTime)); + } + } + + private void tick(MinecraftServer server) { + var config = getConfig(); + if (!config.enable) + return; + + server.getPlayerList().getPlayers().forEach(player -> { + var activity = activities.computeIfAbsent(player.getUUID(), uuid -> new PlayerActivityState(player, server.getTickCount())); + + var curLocation = new ServerLocation(player); + var oldLocation = activity.location; + activity.location = curLocation; + + var delta = curLocation.getDelta(oldLocation); + var horizontalDelta = new Vec3(delta.x(), 0, delta.z()); + + var speed = horizontalDelta.length(); + + // Suppose the player in a vehicle will look around, so we only check for movement when not in a vehicle. + if (player.getVehicle() == null) { + // Defeats some anti-afk stuff, like pools. Works best when no lag. + if ((player.isShiftKeyDown() && speed >= sneakSpeed) || (player.isSprinting() && speed >= sprintSpeed) || (speed >= walkSpeed)) { + if (getConfig().triggers.onMovement) { + clearAfk(player, AfkTriggerReason.MOVEMENT); + } + } + } + + // Looking around requires player input + if (curLocation.getPitch() != oldLocation.getPitch() || curLocation.getYaw() != oldLocation.getYaw()) { + if (getConfig().triggers.onLookChange) { + clearAfk(player, AfkTriggerReason.LOOK_CHANGE); + } + } + + var ticks = server.getTickCount(); + if (activity.lastUpdate < ticks - config.timeTrigger * 20) { + if (!activity.isAfk && activity.afkEnabled) { + activity.isAfk = true; + PlayerActivityEvents.AFK.invoker().onAfk(player); + } + } + }); + } + + public AfkConfig getConfig() { + return Solstice.configManager.getData(AfkConfig.class); + } + + public AfkPlayerData getPlayerData(UUID playerUuid) { + return Solstice.playerData.get(playerUuid).getData(AfkPlayerData.class); + } + + public boolean isPlayerAfk(ServerPlayer player) { + return activities.containsKey(player.getUUID()) && activities.get(player.getUUID()).isAfk; + } + + public void setPlayerAfk(ServerPlayer player, boolean isAfk) { + if (!activities.containsKey(player.getUUID())) + return; + + var config = getConfig(); + var activity = activities.get(player.getUUID()); + if (isAfk) { + activity.lastUpdate = activity.lastUpdate - (config.timeTrigger * 20); + } else { + clearAfk(player, AfkTriggerReason.MANUAL); + } + } + + public int getActiveTime(UUID playerUuid) { + return getPlayerData(playerUuid).activeTime; + } + + private void calculateLeaderboard() { + var serverData = getServerData(); + if (!serverData.forceCalculateLeaderboard) + return; + + serverData.forceCalculateLeaderboard = false; + + var userCache = Solstice.getUserCache(); + var temp = new ArrayList(); + for (var name : userCache.getAllNames()) { + var profile = userCache.getByName(name); + if (profile.isEmpty()) + continue; + + var playerData = Solstice.playerData.get(profile.get().getId()).getData(AfkPlayerData.class); + if (playerData.activeTime > 0) { + var entry = new LeaderboardEntry(profile.get().getName(), profile.get().getId(), playerData.activeTime); + temp.add(entry); + } + } + + temp.sort((o1, o2) -> Integer.compare(o2.activeTime(), o1.activeTime())); + + serverData.leaderboard.clear(); + + for (var i = 0; i < Math.min(temp.size(), LEADERBOARD_SIZE); i++) { + serverData.leaderboard.add(temp.get(i)); + } + + var onlinePlayers = Solstice.server.getPlayerList().getPlayers().stream().map(Entity::getUUID).toList(); + Solstice.playerData.disposeMissing(onlinePlayers); + } + + public List getActiveTimeLeaderboard() { + var serverData = getServerData(); + return Collections.unmodifiableList(serverData.leaderboard); + } + + public List getCurrentActivePlayers() { + return Solstice.server.getPlayerList().getPlayers().stream().filter(player -> !isPlayerAfk(player)).toList(); + } + + private void clearAfk(ServerPlayer player, AfkTriggerReason reason) { + if (!activities.containsKey(player.getUUID())) + return; + + var activity = activities.get(player.getUUID()); + activity.lastUpdate = Solstice.server.getTickCount(); + + if (!activity.afkEnabled) + return; + + if (activity.isAfk) { + activity.isAfk = false; + PlayerActivityEvents.AFK_RETURN.invoker().onAfkReturn(player, reason); + } + + } + + private void registerTriggers() { + AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> { + if (getConfig().triggers.onBlockAttack) { + clearAfk((ServerPlayer) player, AfkTriggerReason.BLOCK_ATTACK); + } + return InteractionResult.PASS; + }); + + AttackEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (getConfig().triggers.onEntityAttack) { + clearAfk((ServerPlayer) player, AfkTriggerReason.ENTITY_ATTACK); + } + return InteractionResult.PASS; + }); + + UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> { + if (getConfig().triggers.onBlockInteract) { + clearAfk((ServerPlayer) player, AfkTriggerReason.BLOCK_INTERACT); + } + return InteractionResult.PASS; + }); + + UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (getConfig().triggers.onEntityInteract) { + clearAfk((ServerPlayer) player, AfkTriggerReason.ENTITY_INTERACT); + } + return InteractionResult.PASS; + }); + + UseItemCallback.EVENT.register((player, world, hand) -> { + if (getConfig().triggers.onItemUse) { + clearAfk((ServerPlayer) player, AfkTriggerReason.ITEM_USE); + } + return InteractionResultHolder.pass(player.getItemInHand(hand)); + }); + + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> { + if (getConfig().triggers.onChat) { + clearAfk(sender, AfkTriggerReason.CHAT_MESSAGE); + } + return true; + }); + + CommandEvents.ALLOW_COMMAND.register((source, command) -> { + if (!source.isPlayer()) + return true; + + if (getConfig().triggers.onCommand) { + clearAfk(source.getPlayer(), AfkTriggerReason.COMMAND); + } + return true; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/ActiveTimeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/ActiveTimeCommand.java new file mode 100644 index 0000000..2ff5c8b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/ActiveTimeCommand.java @@ -0,0 +1,122 @@ +package me.alexdevs.solstice.modules.afk.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.afk.AfkModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +public class ActiveTimeCommand extends ModCommand { + public ActiveTimeCommand(AfkModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("activetime"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var activeTime = module.getActiveTime(player.getUUID()); + + var longSpan = TimeSpan.toLongString(activeTime); + + var map = Map.of( + "activeTime", Component.nullToEmpty(longSpan), + "player", player.getName() + ); + + context.getSource().sendSuccess(() -> module.locale().get("yourActiveTime", map), false); + + return 1; + }) + .then(Commands.literal("player") + .requires(require("others", 1)) + .then(Commands.argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var profile = LocalGameProfile.getProfile(context, "player"); + var activeTime = module.getActiveTime(profile.getId()); + + if (activeTime == 0) { + context.getSource().sendSuccess(() -> module.locale().get("neverPlayed"), false); + return 0; + } + + var longSpan = TimeSpan.toLongString(activeTime); + + var map = Map.of( + "activeTime", Component.nullToEmpty(longSpan), + "player", Component.nullToEmpty(profile.getName()) + ); + + context.getSource().sendSuccess(() -> module.locale().get("playerActiveTime", map), false); + + return 1; + }) + )) + .then(Commands.literal("leaderboard") + .requires(require("leaderboard", true)) + .executes(context -> { + var leaderboard = module.getActiveTimeLeaderboard(); + + var text = Component.empty(); + + text.append(module.locale().get("leaderboardHeader")); + + var index = 0; + for (var entry : leaderboard) { + text.append("\n"); + index++; + var map = Map.of( + "index", Component.nullToEmpty(String.valueOf(index)), + "player", Component.nullToEmpty(entry.name()), + "uuid", Component.nullToEmpty(entry.uuid().toString()), + "time", Component.nullToEmpty(TimeSpan.toLongString(entry.activeTime())), + "shortTime", Component.nullToEmpty(TimeSpan.toShortString(entry.activeTime())), + "seconds", Component.nullToEmpty(String.valueOf(entry.activeTime())) + ); + text.append(module.locale().get("leaderboardEntry", map)); + } + + context.getSource().sendSuccess(() -> text, false); + + return 1; + }) + ) + .then(Commands.literal("set") + .requires(require("set", 3)) + .then(Commands.argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .then(Commands.argument("time", TimeSpan.timeSpan()) + .suggests(TimeSpan::suggest) + .executes(context -> { + var profile = LocalGameProfile.getProfile(context, "player"); + var time = TimeSpan.getTimeSpan(context, "time"); + + var data = module.getPlayerData(profile.getId()); + data.activeTime = time; + + var map = Map.of( + "player", Component.nullToEmpty(profile.getName()), + "time", Component.nullToEmpty(TimeSpan.toLongString(time)) + ); + context.getSource().sendSuccess(() -> module.locale().get("activeTimeSet", map), true); + + return 1; + }) + )) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/AfkCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/AfkCommand.java new file mode 100644 index 0000000..2b3ceed --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/commands/AfkCommand.java @@ -0,0 +1,33 @@ +package me.alexdevs.solstice.modules.afk.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.afk.AfkModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class AfkCommand extends ModCommand { + public AfkCommand(AfkModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("afk"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + module.setPlayerAfk(player, !module.isPlayerAfk(player)); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkConfig.java new file mode 100644 index 0000000..2183ec6 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkConfig.java @@ -0,0 +1,52 @@ +package me.alexdevs.solstice.modules.afk.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class AfkConfig { + @Comment("Enable the AFK functionality. Requires server restart.") + public boolean enable = true; + + @Comment("Announce in chat when a player goes or return from AFK.") + public boolean announce = true; + + @Comment("AFK triggers after the player has been inactive for the following seconds. Defaults to 300 seconds.") + public int timeTrigger = 300; + + @Comment("This tag is displayed with `solstice:afk` placeholder when the player is AFK.") + public String tag = "[AFK] "; + + @Comment("These triggers clear the AFK status. Events regarding entities, blocks or item usage may be triggered by fake players.") + public AfkTriggers triggers = new AfkTriggers(); + + @ConfigSerializable + public static class AfkTriggers { + @Comment("Movement is triggered when the velocity threshold is met.") + public boolean onMovement = true; + + @Comment("Look change is triggered when the player yaw and/or pitch change.") + public boolean onLookChange = true; + + @Comment("Trigger on chat messages sent by the player.") + public boolean onChat = true; + + @Comment("Trigger on commands.") + public boolean onCommand = true; + + @Comment("Trigger when a block is being attacked (left click).") + public boolean onBlockAttack = true; + + @Comment("Trigger when a block is being interacted with (right click).") + public boolean onBlockInteract = true; + + @Comment("Trigger when an entity is attacked.") + public boolean onEntityAttack = true; + + @Comment("Trigger when an entity is interacted with.") + public boolean onEntityInteract = true; + + @Comment("Trigger when an item is used.") + public boolean onItemUse = true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkLocale.java new file mode 100644 index 0000000..c3338de --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkLocale.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.afk.data; + +import java.util.Map; + +public class AfkLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("goneAfk", "%player:displayname% is now AFK"), + Map.entry("returnAfk", "%player:displayname% is no longer AFK"), + Map.entry("yourActiveTime", "Your active time is ${activeTime}."), + Map.entry("playerActiveTime", "${player}'s active time is ${activeTime}."), + Map.entry("neverPlayed", "This player never played."), + Map.entry("notFound", "Player not found."), + Map.entry("leaderboardHeader", "Active Time Leaderboard:"), + Map.entry("leaderboardEntry", " ${index}. ${player}: ${time}"), + Map.entry("activeTimeSet", "Set ${player}'s active time to ${time}!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkPlayerData.java new file mode 100644 index 0000000..f0ea769 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkPlayerData.java @@ -0,0 +1,8 @@ +package me.alexdevs.solstice.modules.afk.data; + +import com.google.gson.annotations.Expose; + +public class AfkPlayerData { + @Expose + public int activeTime = 0; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkServerData.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkServerData.java new file mode 100644 index 0000000..03391f2 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/AfkServerData.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.afk.data; + +import java.util.ArrayList; +import java.util.List; + +public class AfkServerData { + public boolean forceCalculateLeaderboard = true; + public List leaderboard = new ArrayList<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/LeaderboardEntry.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/LeaderboardEntry.java new file mode 100644 index 0000000..30298b7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/LeaderboardEntry.java @@ -0,0 +1,35 @@ +package me.alexdevs.solstice.modules.afk.data; + +import java.util.UUID; + +public class LeaderboardEntry { + protected String name; + protected final UUID uuid; + protected int activeTime; + + public LeaderboardEntry(String name, UUID uuid, int activeTime) { + this.name = name; + this.uuid = uuid; + this.activeTime = activeTime; + } + + public String name() { + return name; + } + + public void name(String name) { + this.name = name; + } + + public UUID uuid() { + return uuid; + } + + public int activeTime() { + return activeTime; + } + + public void activeTime(int activeTime) { + this.activeTime = activeTime; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/afk/data/PlayerActivityState.java b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/PlayerActivityState.java new file mode 100644 index 0000000..2383023 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/afk/data/PlayerActivityState.java @@ -0,0 +1,23 @@ +package me.alexdevs.solstice.modules.afk.data; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.modules.afk.AfkModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.level.ServerPlayer; + +public class PlayerActivityState { + public ServerLocation location; + public int lastUpdate; + public boolean isAfk; + public boolean afkEnabled; + public boolean activeTimeEnabled; + + public PlayerActivityState(ServerPlayer player, int lastUpdate) { + this.location = new ServerLocation(player); + this.lastUpdate = lastUpdate; + this.isAfk = false; + this.afkEnabled = Permissions.check(player, Solstice.MOD_ID + "." + AfkModule.ID + ".base", true); + this.activeTimeEnabled = Permissions.check(player, Solstice.MOD_ID + "." + AfkModule.ID + ".activetime", true); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/AutoAnnouncementModule.java b/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/AutoAnnouncementModule.java new file mode 100644 index 0000000..652e0bc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/AutoAnnouncementModule.java @@ -0,0 +1,79 @@ +package me.alexdevs.solstice.modules.autoAnnouncement; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.modules.autoAnnouncement.data.AutoAnnouncementConfig; +import me.lucko.fabric.api.permissions.v0.Permissions; + +import java.util.Random; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class AutoAnnouncementModule extends ModuleBase.Toggleable { + public static final String ID = "autoannouncement"; + + private ScheduledFuture scheduledFuture = null; + private int currentLine = 0; + + public AutoAnnouncementModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, AutoAnnouncementConfig.class, AutoAnnouncementConfig::new); + + SolsticeEvents.READY.register((instance, server) -> { + setup(); + }); + + SolsticeEvents.RELOAD.register(instance -> { + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + setup(); + }); + } + + private void setup() { + currentLine = 0; + if (getConfig().enable) { + scheduledFuture = Solstice.scheduler.scheduleAtFixedRate(this::announce, getConfig().delay, getConfig().delay, TimeUnit.SECONDS); + } + } + + public AutoAnnouncementConfig getConfig() { + return Solstice.configManager.getData(AutoAnnouncementConfig.class); + } + + public void announce() { + var lines = getConfig().announcements; + if (lines.isEmpty()) + return; + + if (getConfig().pickRandomly) { + currentLine = new Random().nextInt(lines.size()); + } + + currentLine %= lines.size(); + var line = lines.get(currentLine); + currentLine++; + + Solstice.server.getPlayerList().getPlayers().forEach(player -> { + if (line.permission() != null) { + var result = line.result(); + if (result == null) + result = true; + if (Permissions.check(player, line.permission()) != result) { + return; + } + } + var playerContext = PlaceholderContext.of(player); + player.sendSystemMessage(Format.parse(line.text(), playerContext)); + }); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/data/AutoAnnouncementConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/data/AutoAnnouncementConfig.java new file mode 100644 index 0000000..f099d6c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/autoAnnouncement/data/AutoAnnouncementConfig.java @@ -0,0 +1,31 @@ +package me.alexdevs.solstice.modules.autoAnnouncement.data; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class AutoAnnouncementConfig { + @Comment("Enable automatic announcements functionality.") + public boolean enable = true; + @Comment("Pick the next announcement randomly, else linearly.") + public boolean pickRandomly = false; + // every 5 mins + @Comment("Send announcement every X seconds. Defaults to 300 seconds.") + public int delay = 300; + @Comment("Announcement list. Announcements can have a permission as condition. If result is true, the permission has to be granted, else the permission has to be denied (or unset).") + public ArrayList announcements = new ArrayList<>(List.of( + new Announcement("Tip! Solstice is open-source! Contribute on GitHub!"), + new Announcement("Fun fact! This announcement is only visible to players that do not have the 'solstice.example' permission granted!", "solstice.example", false) + )); + + @ConfigSerializable + public record Announcement(String text, @Nullable String permission, @Nullable Boolean result) { + public Announcement(String text) { + this(text, null, null); + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/back/BackModule.java b/common/src/main/java/me/alexdevs/solstice/modules/back/BackModule.java new file mode 100644 index 0000000..6ce96c9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/back/BackModule.java @@ -0,0 +1,44 @@ +package me.alexdevs.solstice.modules.back; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.events.PlayerTeleportCallback; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.back.commands.BackCommand; +import me.alexdevs.solstice.modules.back.data.BackLocale; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.level.ServerPlayer; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class BackModule extends ModuleBase.Toggleable { + public static final String ID = "back"; + public final ConcurrentHashMap lastPlayerPositions = new ConcurrentHashMap<>(); + + public BackModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, BackLocale.MODULE); + + commands.add(new BackCommand(this)); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> lastPlayerPositions.remove(handler.getPlayer().getUUID())); + + PlayerTeleportCallback.EVENT.register((player, origin, destination) -> lastPlayerPositions.put(player.getUUID(), origin)); + + ServerLivingEntityEvents.AFTER_DEATH.register((entity, damageSource) -> { + if (entity.isAlwaysTicking()) { + try { + var player = (ServerPlayer) entity; + lastPlayerPositions.put(entity.getUUID(), new ServerLocation(player)); + } catch (ClassCastException e) { + // They were, in fact, not a player. + } + } + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/back/commands/BackCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/back/commands/BackCommand.java new file mode 100644 index 0000000..a240a3c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/back/commands/BackCommand.java @@ -0,0 +1,52 @@ +package me.alexdevs.solstice.modules.back.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.locale.Locale; +import me.alexdevs.solstice.modules.back.BackModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class BackCommand extends ModCommand { + private final Locale locale = Solstice.localeManager.getLocale(BackModule.ID); + + public BackCommand(BackModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("back"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var playerContext = PlaceholderContext.of(player); + + var lastPosition = module.lastPlayerPositions.get(player.getUUID()); + if (lastPosition == null) { + context.getSource().sendSuccess(() -> locale.get( + "noPosition", + playerContext + ), false); + return 1; + } + + context.getSource().sendSuccess(() -> locale.get( + "teleporting", + playerContext + ), false); + lastPosition.teleport(player); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/back/data/BackLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/back/data/BackLocale.java new file mode 100644 index 0000000..3ae29d5 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/back/data/BackLocale.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.back.data; + +import java.util.Map; + +public class BackLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("teleporting", "Teleporting to previous position..."), + Map.entry("noPosition", "There is no position to return back to.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/BanModule.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/BanModule.java new file mode 100644 index 0000000..dea1f06 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/BanModule.java @@ -0,0 +1,25 @@ +package me.alexdevs.solstice.modules.ban; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.ban.commands.BanCommand; +import me.alexdevs.solstice.modules.ban.commands.TempBanCommand; +import me.alexdevs.solstice.modules.ban.commands.UnbanCommand; +import me.alexdevs.solstice.modules.ban.data.BanLocale; + +public class BanModule extends ModuleBase.Toggleable { + public static final String ID = "ban"; + + public BanModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, BanLocale.MODULE); + + commands.add(new BanCommand(this)); + commands.add(new TempBanCommand(this)); + commands.add(new UnbanCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/BanCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/BanCommand.java new file mode 100644 index 0000000..5a21b1b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/BanCommand.java @@ -0,0 +1,92 @@ +package me.alexdevs.solstice.modules.ban.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.Utils; +import me.alexdevs.solstice.modules.ban.BanModule; +import me.alexdevs.solstice.modules.ban.formatters.BanMessageFormatter; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.server.players.UserBanListEntry; +import me.alexdevs.solstice.api.text.Format; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class BanCommand extends ModCommand { + public static final SimpleCommandExceptionType ALREADY_BANNED_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.ban.failed")); + + public BanCommand(BanModule module) { + super(module); + } + + static int execute(CommandContext context, Collection targets, @Nullable String reason, @Nullable Date expiryDate) throws CommandSyntaxException { + var source = context.getSource(); + var server = source.getServer(); + var banList = server.getPlayerList().getBans(); + + var banCounter = 0; + for (GameProfile target : targets) { + if (banList.isBanned(target)) { + continue; + } + + var banEntry = new UserBanListEntry(target, null, source.getTextName(), expiryDate, reason); + banList.add(banEntry); + banCounter++; + + var playerContext = PlaceholderContext.of(target, server); + + source.sendSuccess(() -> Component.translatable("commands.ban.success", Component.nullToEmpty(target.getName()), Format.parse(banEntry.getReason(), playerContext)), true); + + var serverPlayerEntity = source.getServer().getPlayerList().getPlayer(target.getId()); + if (serverPlayerEntity != null) { + serverPlayerEntity.connection.disconnect(BanMessageFormatter.format(target, banEntry)); + } + } + + if (banCounter == 0) { + throw ALREADY_BANNED_EXCEPTION.create(); + } else { + return banCounter; + } + } + + @Override + public void register(CommandDispatcher dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) { + Utils.removeCommands(dispatcher, "ban"); + super.register(dispatcher, commandRegistry, environment); + } + + @Override + public List getNames() { + return List.of("ban"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .then(argument("targets", GameProfileArgument.gameProfile()) + .executes(context -> execute(context, GameProfileArgument.getGameProfiles(context, "targets"), null, null)) + .then(argument("reason", StringArgumentType.greedyString()) + .executes(context -> execute(context, GameProfileArgument.getGameProfiles(context, "targets"), StringArgumentType.getString(context, "reason"), null)))); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/TempBanCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/TempBanCommand.java new file mode 100644 index 0000000..45fbae7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/TempBanCommand.java @@ -0,0 +1,57 @@ +package me.alexdevs.solstice.modules.ban.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ban.BanModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.GameProfileArgument; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TempBanCommand extends ModCommand { + public TempBanCommand(BanModule module) { + super(module); + } + + private static Date getDateFromNow(int seconds) { + var now = new Date(); + var c = Calendar.getInstance(); + c.setTime(now); + c.add(Calendar.SECOND, seconds); + return c.getTime(); + } + + @Override + public List getNames() { + return List.of("tempban"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("tempban", 3)) + .then(argument("targets", GameProfileArgument.gameProfile()) + .then(argument("duration", StringArgumentType.string()) + .suggests(TimeSpan::suggest) + .executes(context -> execute(context, GameProfileArgument.getGameProfiles(context, "targets"), null, TimeSpan.getTimeSpan(context, "duration"))) + .then(argument("reason", StringArgumentType.greedyString()) + .executes(context -> execute(context, GameProfileArgument.getGameProfiles(context, "targets"), StringArgumentType.getString(context, "reason"), TimeSpan.getTimeSpan(context, "duration")))))); + + } + + private int execute(CommandContext context, Collection targets, String reason, int duration) throws CommandSyntaxException { + var expiryDate = getDateFromNow(duration); + + return BanCommand.execute(context, targets, reason, expiryDate); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/UnbanCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/UnbanCommand.java new file mode 100644 index 0000000..a76df39 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/commands/UnbanCommand.java @@ -0,0 +1,69 @@ +package me.alexdevs.solstice.modules.ban.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.Utils; +import me.alexdevs.solstice.modules.ban.BanModule; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class UnbanCommand extends ModCommand { + private static final SimpleCommandExceptionType ALREADY_UNBANNED_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.pardon.failed")); + + public UnbanCommand(BanModule module) { + super(module); + } + + @Override + public void register(CommandDispatcher dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) { + Utils.removeCommands(dispatcher, "pardon"); + super.register(dispatcher, commandRegistry, environment); + } + + @Override + public List getNames() { + return List.of("unban", "pardon"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .then(Commands.argument("targets", GameProfileArgument.gameProfile()) + .suggests((context, builder) -> SharedSuggestionProvider.suggest((context.getSource()).getServer().getPlayerList().getBans().getUserList(), builder)) + .executes(context -> execute(context, GameProfileArgument.getGameProfiles(context, "targets")))); + } + + private int execute(CommandContext context, Collection targets) throws CommandSyntaxException { + var banList = context.getSource().getServer().getPlayerList().getBans(); + var source = context.getSource(); + var pardonCount = 0; + for (GameProfile profile : targets) { + if (banList.isBanned(profile)) { + banList.remove(profile); + pardonCount++; + source.sendSuccess(() -> Component.translatable("commands.pardon.success", Component.nullToEmpty(profile.getName())), true); + } + } + + if (pardonCount == 0) { + throw ALREADY_UNBANNED_EXCEPTION.create(); + } else { + return pardonCount; + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/data/BanLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/data/BanLocale.java new file mode 100644 index 0000000..e41948b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/data/BanLocale.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.ban.data; + +import java.util.Map; + +public class BanLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("banMessageFormat", "You are banned from this server:\n\n${reason}"), + Map.entry("tempBanMessageFormat", "You are temporarily banned from this server:\n\n${reason}\n\nExpires: ${expiry_date}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ban/formatters/BanMessageFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/ban/formatters/BanMessageFormatter.java new file mode 100644 index 0000000..84b8bb7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ban/formatters/BanMessageFormatter.java @@ -0,0 +1,33 @@ +package me.alexdevs.solstice.modules.ban.formatters; + +import com.mojang.authlib.GameProfile; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.ban.BanModule; +import net.minecraft.network.chat.Component; +import net.minecraft.server.players.UserBanListEntry; +import me.alexdevs.solstice.core.coreModule.CoreModule; +import me.alexdevs.solstice.api.text.Format; +import java.text.SimpleDateFormat; +import java.util.Map; + +public class BanMessageFormatter { + public static Component format(GameProfile profile, UserBanListEntry entry) { + var locale = Solstice.modules.getModule(BanModule.class).locale(); + var coreConfig = CoreModule.getConfig(); + var df = new SimpleDateFormat(coreConfig.dateTimeFormat); + + var context = PlaceholderContext.of(profile, Solstice.server); + var expiryDate = Component.nullToEmpty(entry.getExpires() != null ? df.format(entry.getExpires()) : "never"); + Map placeholders = Map.of( + "reason", Format.parse(entry.getReason(), context), + "creation_date", Component.nullToEmpty(df.format(entry.getCreated())), + "expiry_date", expiryDate, + "source", Component.nullToEmpty(entry.getSource()) + ); + + var format = entry.getExpires() != null ? locale.raw("tempBanMessageFormat") : locale.raw("banMessageFormat"); + + return Format.parse(format, context, placeholders); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/broadcast/BroadcastModule.java b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/BroadcastModule.java new file mode 100644 index 0000000..69e5f2e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/BroadcastModule.java @@ -0,0 +1,23 @@ +package me.alexdevs.solstice.modules.broadcast; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.broadcast.commands.BroadcastCommand; +import me.alexdevs.solstice.modules.broadcast.commands.PlainBroadcastCommand; +import me.alexdevs.solstice.modules.broadcast.data.BroadcastConfig; + +public class BroadcastModule extends ModuleBase.Toggleable { + public static final String ID = "broadcast"; + + public BroadcastModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, BroadcastConfig.class, BroadcastConfig::new); + + commands.add(new BroadcastCommand(this)); + commands.add(new PlainBroadcastCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/BroadcastCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/BroadcastCommand.java new file mode 100644 index 0000000..2c8bd04 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/BroadcastCommand.java @@ -0,0 +1,50 @@ +package me.alexdevs.solstice.modules.broadcast.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.broadcast.BroadcastModule; +import me.alexdevs.solstice.modules.broadcast.data.BroadcastConfig; +import net.minecraft.commands.CommandSourceStack; +import me.alexdevs.solstice.api.text.Format; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class BroadcastCommand extends ModCommand { + + public BroadcastCommand(BroadcastModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("broadcast", "bc"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(context -> { + var config = Solstice.configManager.getData(BroadcastConfig.class); + + var message = StringArgumentType.getString(context, "message"); + var serverContext = PlaceholderContext.of(context.getSource().getServer()); + + var placeholders = Map.of( + "message", Format.parse(message, serverContext) + ); + + Solstice.getInstance().broadcast(Format.parse(config.format, placeholders)); + + return 1; + })); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/PlainBroadcastCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/PlainBroadcastCommand.java new file mode 100644 index 0000000..f70fff1 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/commands/PlainBroadcastCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.broadcast.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.broadcast.BroadcastModule; +import net.minecraft.commands.CommandSourceStack; +import me.alexdevs.solstice.api.text.Format; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class PlainBroadcastCommand extends ModCommand { + + public PlainBroadcastCommand(BroadcastModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("plainbroadcast", "pbc"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("plain", 2)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(context -> { + var message = StringArgumentType.getString(context, "message"); + var serverContext = PlaceholderContext.of(context.getSource().getServer()); + + Solstice.getInstance().broadcast(Format.parse(message, serverContext)); + + return 1; + })); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/broadcast/data/BroadcastConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/data/BroadcastConfig.java new file mode 100644 index 0000000..6a50ef5 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/broadcast/data/BroadcastConfig.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.broadcast.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class BroadcastConfig { + @Comment("Format to use when broadcasting a message.") + public String format = "[Broadcast] ${message}"; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/CommandSpyModule.java b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/CommandSpyModule.java new file mode 100644 index 0000000..7611418 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/CommandSpyModule.java @@ -0,0 +1,61 @@ +package me.alexdevs.solstice.modules.commandSpy; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.CommandEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.commandSpy.data.CommandSpyConfig; +import me.alexdevs.solstice.modules.commandSpy.data.CommandSpyLocale; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.network.chat.Component; +import java.util.Map; + +public class CommandSpyModule extends ModuleBase.Toggleable { + public static final String ID = "commandspy"; + + public CommandSpyModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, CommandSpyConfig.class, CommandSpyConfig::new); + Solstice.localeManager.registerModule(ID, CommandSpyLocale.MODULE); + + CommandEvents.ALLOW_COMMAND.register((source, command) -> { + if (!source.isPlayer()) + return true; + + Solstice.LOGGER.info("{}: /{}", source.getTextName(), command); + + var parts = command.split("\\s"); + if (parts.length >= 1) { + var cmd = parts[0]; + if (isIgnored(cmd)) { + return true; + } + } + + var player = source.getPlayer(); + + var players = source.getServer().getPlayerList().getPlayers(); + var placeholders = Map.of("player", Component.nullToEmpty(player.getGameProfile().getName()), "command", Component.nullToEmpty(command)); + var message = locale().get("spyFormat", placeholders); + for (var pl : players) { + var commandSpyEnabled = Permissions.check(pl, this.getPermissionNode("base")); + + if (commandSpyEnabled && !pl.getUUID().equals(player.getUUID())) { + pl.displayClientMessage(message, false); + } + } + return true; + }); + + } + + public boolean isIgnored(String command) { + if(!isEnabled()) + return false; + + return Solstice.configManager.getData(CommandSpyConfig.class).ignoredCommands.contains(command); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyConfig.java new file mode 100644 index 0000000..d989199 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyConfig.java @@ -0,0 +1,15 @@ +package me.alexdevs.solstice.modules.commandSpy.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class CommandSpyConfig { + @Comment("Commands to ignore.") + public ArrayList ignoredCommands = new ArrayList<>(List.of( + "tell", "w", "msg", "dm", "r", "staffchat", "sc", "helpop", "sos" + )); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyLocale.java new file mode 100644 index 0000000..7a87bb6 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/commandSpy/data/CommandSpyLocale.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.commandSpy.data; + +import java.util.Map; + +public class CommandSpyLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("spyFormat", "\uD83D\uDC41 ${player}: /${command}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/customName/CustomNameModule.java b/common/src/main/java/me/alexdevs/solstice/modules/customName/CustomNameModule.java new file mode 100644 index 0000000..af0389f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/customName/CustomNameModule.java @@ -0,0 +1,98 @@ +package me.alexdevs.solstice.modules.customName; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.api.text.RawPlaceholder; +import me.alexdevs.solstice.integrations.LuckPermsIntegration; +import me.alexdevs.solstice.modules.customName.commands.NicknameCommand; +import me.alexdevs.solstice.modules.customName.data.CustomNameConfig; +import me.alexdevs.solstice.modules.customName.data.CustomNamePlayerData; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.level.ServerPlayer; +import java.util.Map; + +public class CustomNameModule extends ModuleBase.Toggleable { + public static final String ID = "customname"; + + public CustomNameModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, CustomNameConfig.class, CustomNameConfig::new); + Solstice.playerData.registerData(ID, CustomNamePlayerData.class, CustomNamePlayerData::new); + + commands.add(new NicknameCommand(this)); + } + + public String fetchUsernameFormat(ServerPlayer player) { + var formats = Solstice.configManager.getData(CustomNameConfig.class).nameFormats; + + String format = null; + for (var f : formats) { + if (LuckPermsIntegration.isInGroup(player, f.group())) { + format = f.format(); + break; + } + } + + var isOperator = player.getServer().getPlayerList().isOp(player.getGameProfile()); + + if (format == null) { + format = "${username}"; + + for (var f : formats) { + if (isOperator && f.group().equals("operator")) { + format = f.format(); + break; + } + if (f.group().equals("default")) { + format = f.format(); + break; + } + } + } + + return format; + } + + public String getResolvedUsername(ServerPlayer player) { + var format = fetchUsernameFormat(player); + var playerData = Solstice.playerData.get(player).getData(CustomNamePlayerData.class); + var name = playerData.nickname == null ? player.getGameProfile().getName() : playerData.nickname; + + var prefix = LuckPermsIntegration.getPrefix(player); + var suffix = LuckPermsIntegration.getSuffix(player); + if (prefix == null) + prefix = ""; + if (suffix == null) + suffix = ""; + + Map placeholders = Map.of( + "name", name, + "prefix", prefix, + "suffix", suffix + ); + + return RawPlaceholder.parse(format, placeholders); + } + + public MutableComponent getNameForPlayer(ServerPlayer player) { + var name = getResolvedUsername(player); + var playerContext = PlaceholderContext.of(player); + return Format.parse(name, playerContext).copy(); + } + + public void setCustomName(ServerPlayer player, String name) { + var playerData = Solstice.playerData.get(player).getData(CustomNamePlayerData.class); + playerData.nickname = name; + } + + public void clearCustomName(ServerPlayer player) { + var playerData = Solstice.playerData.get(player).getData(CustomNamePlayerData.class); + playerData.nickname = null; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/customName/commands/NicknameCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/customName/commands/NicknameCommand.java new file mode 100644 index 0000000..3f91b7e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/customName/commands/NicknameCommand.java @@ -0,0 +1,77 @@ +package me.alexdevs.solstice.modules.customName.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.customName.CustomNameModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class NicknameCommand extends ModCommand { + + public NicknameCommand(CustomNameModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("nickname", "nick"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(literal("clear") + .executes(context -> executeClear(context, null)) + ) + .then(argument("nickname", StringArgumentType.string()) + .executes(context -> execute(context, StringArgumentType.getString(context, "nickname"), null)) + ) + .then(argument("player", EntityArgument.player()) + .requires(require("others", 2)) + .then(literal("clear") + .executes(context -> executeClear(context, EntityArgument.getPlayer(context, "player"))) + ) + .then(argument("nickname", StringArgumentType.string()) + .executes(context -> execute(context, StringArgumentType.getString(context, "nickname"), EntityArgument.getPlayer(context, "player"))) + ) + ); + } + + private int execute(CommandContext context, String nickname, @Nullable ServerPlayer player) throws CommandSyntaxException { + if (player == null) { + player = context.getSource().getPlayerOrException(); + } + + module.setCustomName(player, nickname); + + var name = player.getGameProfile().getName(); + context.getSource().sendSuccess(() -> Component.literal(String.format("Changed %s's nickname", name)), true); + + return 1; + } + + private int executeClear(CommandContext context, @Nullable ServerPlayer player) throws CommandSyntaxException { + if (player == null) { + player = context.getSource().getPlayerOrException(); + } + + module.clearCustomName(player); + + var name = player.getGameProfile().getName(); + context.getSource().sendSuccess(() -> Component.literal(String.format("Cleared %s's nickname", name)), true); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNameConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNameConfig.java new file mode 100644 index 0000000..a912a77 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNameConfig.java @@ -0,0 +1,20 @@ +package me.alexdevs.solstice.modules.customName.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class CustomNameConfig { + @Comment("Customize player display names based on their LuckPerms group. Priority is determined by the list order: first comes before last.") + public ArrayList nameFormats = new ArrayList<>(List.of( + new NameFormat("admin", "${prefix}${name}${suffix}"), + new NameFormat("default", "${prefix}${name}${suffix}") + )); + + @ConfigSerializable + public record NameFormat(String group, String format) { + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNamePlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNamePlayerData.java new file mode 100644 index 0000000..bdda602 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/customName/data/CustomNamePlayerData.java @@ -0,0 +1,7 @@ +package me.alexdevs.solstice.modules.customName.data; + +import org.jetbrains.annotations.Nullable; + +public class CustomNamePlayerData { + public @Nullable String nickname; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/enderchest/EnderChestModule.java b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/EnderChestModule.java new file mode 100644 index 0000000..0853883 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/EnderChestModule.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.enderchest; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.enderchest.commands.EnderChestCommand; +import me.alexdevs.solstice.modules.enderchest.data.EnderChestLocale; + +public class EnderChestModule extends ModuleBase.Toggleable { + public static final String ID = "enderchest"; + + public EnderChestModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, EnderChestLocale.LOCALE); + + commands.add(new EnderChestCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/enderchest/commands/EnderChestCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/commands/EnderChestCommand.java new file mode 100644 index 0000000..0ba5671 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/commands/EnderChestCommand.java @@ -0,0 +1,120 @@ +package me.alexdevs.solstice.modules.enderchest.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.utils.PlayerUtils; +import me.alexdevs.solstice.modules.enderchest.EnderChestModule; +import me.alexdevs.solstice.modules.inventorySee.ImmutableSlot; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.stats.Stats; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.inventory.Slot; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class EnderChestCommand extends ModCommand { + public EnderChestCommand(EnderChestModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("enderchest", "ec"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + player.awardStat(Stats.OPEN_ENDERCHEST); + + open(player, player.getEnderChestInventory(), Component.translatable("container.enderchest"), true, () -> { + }); + + return 1; + }) + .then(argument("player", StringArgumentType.word()) + .requires(require("others", 2)) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + final var source = context.getSource(); + var player = source.getPlayerOrException(); + var profile = LocalGameProfile.getProfile(context, "player"); + + Permissions.check(profile, getPermissionNode("exempt"), 3, source.getServer()).thenAccept(exempt -> { + if (exempt) { + source.sendSuccess(() -> module.locale().get("exempt"), false); + return; + } + + var isOnline = PlayerUtils.isOnline(profile.getId()); + if (!isOnline && !Permissions.check(player, getPermissionNode("offline"), 3)) { + source.sendSuccess(() -> module.locale().get("offlineNotAllowed"), false); + return; + } + + ServerPlayer targetPlayer; + + if (isOnline) { + targetPlayer = source.getServer().getPlayerList().getPlayer(profile.getId()); + } else { + targetPlayer = PlayerUtils.loadOfflinePlayer(profile); + } + + var inventory = targetPlayer.getEnderChestInventory(); + + var canEdit = Permissions.check(player, getPermissionNode("edit"), 3); + + var map = Map.of( + "player", Component.nullToEmpty(profile.getName()) + ); + var title = module.locale().get("title", map); + + open(player, inventory, title, canEdit, () -> { + if(!isOnline) { + PlayerUtils.saveOfflinePlayer(targetPlayer); + } + }); + + source.sendSuccess(() -> module.locale().get("opened", map), true); + }); + + return 1; + })); + } + + private void open(ServerPlayer player, PlayerEnderChestContainer inventory, Component title, boolean canEdit, Runnable onClose) { + var container = new SimpleGui(MenuType.GENERIC_9x3, player, false) { + @Override + public void onClose() { + onClose.run(); + } + }; + + for (var i = 0; i < inventory.getContainerSize(); i++) { + Slot slot; + if (canEdit) { + slot = new Slot(inventory, i, 0, 0); + } else { + slot = new ImmutableSlot(inventory, i, 0, 0); + } + container.setSlotRedirect(i, slot); + } + + container.setTitle(title); + + container.open(); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/enderchest/data/EnderChestLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/data/EnderChestLocale.java new file mode 100644 index 0000000..678cd09 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/enderchest/data/EnderChestLocale.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.enderchest.data; + +import java.util.Map; + +public class EnderChestLocale { + public static final Map LOCALE = Map.ofEntries( + Map.entry("title", "${player}'s Ender Chest"), + Map.entry("exempt", "You cannot open this Ender Chest because the player is exempt."), + Map.entry("opened", "Opened ${player}'s Ender Chest.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/experiments/ExperimentsModule.java b/common/src/main/java/me/alexdevs/solstice/modules/experiments/ExperimentsModule.java new file mode 100644 index 0000000..0f623e9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/experiments/ExperimentsModule.java @@ -0,0 +1,31 @@ +package me.alexdevs.solstice.modules.experiments; + +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.experiments.commands.FlagsCommand; +import me.alexdevs.solstice.modules.experiments.commands.TimeSpanCommand; + +import java.util.Collection; +import java.util.List; + +public class ExperimentsModule extends ModuleBase { + public static final boolean ENABLED = false; + public static final String ID = "experiments"; + + public ExperimentsModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new TimeSpanCommand(this)); + commands.add(new FlagsCommand(this)); + } + + @Override + public Collection> getCommands() { + if (!ENABLED) return List.of(); + + return commands; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/FlagsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/FlagsCommand.java new file mode 100644 index 0000000..e29b95e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/FlagsCommand.java @@ -0,0 +1,59 @@ +package me.alexdevs.solstice.modules.experiments.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.command.Flags; +import me.alexdevs.solstice.api.command.flags.ArgumentFlag; +import me.alexdevs.solstice.api.command.flags.DoubleFlag; +import me.alexdevs.solstice.api.command.flags.Flag; +import me.alexdevs.solstice.api.command.flags.StringFlag; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.experiments.ExperimentsModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import java.util.List; + +public class FlagsCommand extends ModCommand { + public FlagsCommand(ExperimentsModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("flags"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(true)) + .then(Commands.argument("flags", StringArgumentType.greedyString()) + .executes(context -> { + var source = context.getSource(); + + var testFlag = Flag.of("test"); + var stringFlag = new StringFlag("string", List.of('s')); + var numberFlag = new DoubleFlag("number", List.of('n')); + + Flags.parse(StringArgumentType.getString(context, "flags"), testFlag, stringFlag, numberFlag); + + for (var flag : List.of(testFlag, stringFlag, numberFlag)) { + if (flag.isUsed()) { + if (flag.acceptsValue() && flag instanceof ArgumentFlag argFlag) { + var value = argFlag.getValue(); + source.sendSystemMessage(Component.nullToEmpty(String.format("Flag %s: %s", flag.getName(), value))); + + } else { + source.sendSystemMessage(Component.nullToEmpty(String.format("Flag %s", flag.getName()))); + } + } + } + + return 1; + }) + ); + } + + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/TimeSpanCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/TimeSpanCommand.java new file mode 100644 index 0000000..041c9bd --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/experiments/commands/TimeSpanCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.experiments.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.experiments.ExperimentsModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TimeSpanCommand extends ModCommand { + + public TimeSpanCommand(ExperimentsModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("timespan"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("timespan", StringArgumentType.string()) + .suggests(TimeSpan::suggest) + .executes(this::execute)); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var timespan = TimeSpan.getTimeSpan(context, "timespan"); + context.getSource().sendSuccess(() -> Component.nullToEmpty(String.format("Got %s (%d)", TimeSpan.toShortString(timespan), timespan)), false); + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/extinguish/ExtinguishModule.java b/common/src/main/java/me/alexdevs/solstice/modules/extinguish/ExtinguishModule.java new file mode 100644 index 0000000..2c73c95 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/extinguish/ExtinguishModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.extinguish; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.extinguish.commands.ExtinguishCommand; + +public class ExtinguishModule extends ModuleBase.Toggleable { + public static final String ID = "extinguish"; + + public ExtinguishModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new ExtinguishCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/extinguish/commands/ExtinguishCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/extinguish/commands/ExtinguishCommand.java new file mode 100644 index 0000000..4d672c4 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/extinguish/commands/ExtinguishCommand.java @@ -0,0 +1,56 @@ +package me.alexdevs.solstice.modules.extinguish.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.extinguish.ExtinguishModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class ExtinguishCommand extends ModCommand { + public ExtinguishCommand(ExtinguishModule module) { + super(module); + } + + private static int execute(CommandContext context, @Nullable Collection players) throws CommandSyntaxException { + var source = context.getSource(); + if (players == null) { + extinguish(source, source.getPlayerOrException()); + return 1; + } else { + for (ServerPlayer player : players) { + extinguish(source, player); + } + + return players.size(); + } + } + + private static void extinguish(CommandSourceStack source, ServerPlayer player) { + player.clearFire(); + source.sendSuccess(() -> Component.literal("Extinguished ").append(source.getDisplayName()), true); + } + + @Override + public List getNames() { + return List.of("extinguish", "ex"); + } + + public LiteralArgumentBuilder command(String command) { + return literal(command) + .requires(require(2)) + .executes(context -> execute(context, null)) + .then(argument("players", EntityArgument.players()) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "players")))); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/feed/FeedModule.java b/common/src/main/java/me/alexdevs/solstice/modules/feed/FeedModule.java new file mode 100644 index 0000000..504771f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/feed/FeedModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.feed; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.feed.commands.FeedCommand; + +public class FeedModule extends ModuleBase.Toggleable { + public static final String ID = "feed"; + + public FeedModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new FeedCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/feed/commands/FeedCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/feed/commands/FeedCommand.java new file mode 100644 index 0000000..9280b75 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/feed/commands/FeedCommand.java @@ -0,0 +1,61 @@ +package me.alexdevs.solstice.modules.feed.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.feed.FeedModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class FeedCommand extends ModCommand { + private static final int MAX_FOOD_LEVEL = 20; + + public FeedCommand(FeedModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("feed"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> execute(context, null)) + .then(argument("targets", EntityArgument.players()) + .requires(require("others", 2)) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "targets")))); + } + + private int execute(CommandContext context, @Nullable Collection targets) throws CommandSyntaxException { + if (targets == null) { + var player = context.getSource().getPlayerOrException(); + feed(context, player); + return 1; + } else { + for (var target : targets) { + feed(context, target); + } + + return targets.size(); + } + } + + private void feed(CommandContext context, ServerPlayer player) { + player.getFoodData().setFoodLevel(MAX_FOOD_LEVEL); + player.getFoodData().setSaturation(MAX_FOOD_LEVEL); + context.getSource().sendSuccess(() -> Component.literal("Fed ").append(player.getDisplayName()), true); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/fly/FlyModule.java b/common/src/main/java/me/alexdevs/solstice/modules/fly/FlyModule.java new file mode 100644 index 0000000..ce31848 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/fly/FlyModule.java @@ -0,0 +1,35 @@ +package me.alexdevs.solstice.modules.fly; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.fly.commands.FlyCommand; +import me.alexdevs.solstice.modules.fly.data.FlyLocale; +import me.alexdevs.solstice.modules.fly.data.FlyPlayerData; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; + +public class FlyModule extends ModuleBase.Toggleable { + public static final String ID = "fly"; + + public FlyModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, FlyLocale.MODULE); + Solstice.playerData.registerData(ID, FlyPlayerData.class, FlyPlayerData::new); + + commands.add(new FlyCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + var player = handler.getPlayer(); + + var data = Solstice.playerData.get(player).getData(FlyPlayerData.class); + if(data.flightEnabled) { + var abilities = player.getAbilities(); + abilities.mayfly = true; + player.onUpdateAbilities(); + } + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/fly/commands/FlyCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/fly/commands/FlyCommand.java new file mode 100644 index 0000000..4170bfb --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/fly/commands/FlyCommand.java @@ -0,0 +1,81 @@ +package me.alexdevs.solstice.modules.fly.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.fly.FlyModule; +import me.alexdevs.solstice.modules.fly.data.FlyPlayerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class FlyCommand extends ModCommand { + public FlyCommand(FlyModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("fly"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .executes(context -> execute(context, null)) + .then(argument("player", EntityArgument.player()) + .requires(require("others", 3)) + .executes(context -> execute(context, EntityArgument.getPlayer(context, "player"))) + ); + } + + private int execute(CommandContext context, @Nullable ServerPlayer player) throws CommandSyntaxException { + var forOther = player != null; + if (player == null) { + player = context.getSource().getPlayerOrException(); + } + + var abilities = player.getAbilities(); + abilities.mayfly = !abilities.mayfly; + player.onUpdateAbilities(); + + var data = Solstice.playerData.get(player).getData(FlyPlayerData.class); + data.flightEnabled = abilities.mayfly; + + Component text; + var sourceContext = PlaceholderContext.of(context.getSource()); + if (forOther) { + var placeholders = Map.of( + "player", player.getDisplayName() + ); + + if (abilities.mayfly) { + text = module.locale().get("enabledForOther", sourceContext, placeholders); + } else { + text = module.locale().get("disabledForOther", sourceContext, placeholders); + } + } else { + if (abilities.mayfly) { + text = module.locale().get("enabled", sourceContext); + } else { + text = module.locale().get("disabled", sourceContext); + } + } + + context.getSource().sendSuccess(() -> text, forOther); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyLocale.java new file mode 100644 index 0000000..11babb9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyLocale.java @@ -0,0 +1,12 @@ +package me.alexdevs.solstice.modules.fly.data; + +import java.util.Map; + +public class FlyLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("enabled", "Flight enabled"), + Map.entry("disabled", "Flight disabled"), + Map.entry("enabledForOther", "Flight enabled for ${player}"), + Map.entry("disabledForOther", "Flight disabled for ${player}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyPlayerData.java new file mode 100644 index 0000000..14d86fe --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/fly/data/FlyPlayerData.java @@ -0,0 +1,5 @@ +package me.alexdevs.solstice.modules.fly.data; + +public class FlyPlayerData { + public boolean flightEnabled = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/god/GodModule.java b/common/src/main/java/me/alexdevs/solstice/modules/god/GodModule.java new file mode 100644 index 0000000..cdc0b5d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/god/GodModule.java @@ -0,0 +1,35 @@ +package me.alexdevs.solstice.modules.god; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.god.commands.GodCommand; +import me.alexdevs.solstice.modules.god.data.GodLocale; +import me.alexdevs.solstice.modules.god.data.GodPlayerData; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; + +public class GodModule extends ModuleBase.Toggleable { + public static final String ID = "god"; + + public GodModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, GodLocale.MODULE); + Solstice.playerData.registerData(ID, GodPlayerData.class, GodPlayerData::new); + + commands.add(new GodCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + var player = handler.getPlayer(); + + var data = Solstice.playerData.get(player).getData(GodPlayerData.class); + if(data.invulnerabilityEnabled) { + var abilities = player.getAbilities(); + abilities.invulnerable = true; + player.onUpdateAbilities(); + } + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/god/commands/GodCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/god/commands/GodCommand.java new file mode 100644 index 0000000..d26540f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/god/commands/GodCommand.java @@ -0,0 +1,81 @@ +package me.alexdevs.solstice.modules.god.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.god.GodModule; +import me.alexdevs.solstice.modules.god.data.GodPlayerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class GodCommand extends ModCommand { + public GodCommand(GodModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("god"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .executes(context -> execute(context, null)) + .then(argument("player", EntityArgument.player()) + .requires(require("others", 3)) + .executes(context -> execute(context, EntityArgument.getPlayer(context, "player"))) + ); + } + + private int execute(CommandContext context, @Nullable ServerPlayer player) throws CommandSyntaxException { + var forOther = player != null; + if (player == null) { + player = context.getSource().getPlayerOrException(); + } + + var abilities = player.getAbilities(); + abilities.invulnerable = !abilities.invulnerable; + player.onUpdateAbilities(); + + var data = Solstice.playerData.get(player).getData(GodPlayerData.class); + data.invulnerabilityEnabled = abilities.invulnerable; + + Component text; + var sourceContext = PlaceholderContext.of(context.getSource()); + if (forOther) { + var placeholders = Map.of( + "player", player.getDisplayName() + ); + + if (abilities.invulnerable) { + text = module.locale().get("enabledForOther", sourceContext, placeholders); + } else { + text = module.locale().get("disabledForOther", sourceContext, placeholders); + } + } else { + if (abilities.invulnerable) { + text = module.locale().get("enabled", sourceContext); + } else { + text = module.locale().get("disabled", sourceContext); + } + } + + context.getSource().sendSuccess(() -> text, forOther); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodLocale.java new file mode 100644 index 0000000..9bb8736 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodLocale.java @@ -0,0 +1,12 @@ +package me.alexdevs.solstice.modules.god.data; + +import java.util.Map; + +public class GodLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("enabled", "Invincibility enabled"), + Map.entry("disabled", "Invincibility disabled"), + Map.entry("enabledForOther", "Invincibility enabled for ${player}"), + Map.entry("disabledForOther", "Invincibility disabled for ${player}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodPlayerData.java new file mode 100644 index 0000000..0604c90 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/god/data/GodPlayerData.java @@ -0,0 +1,5 @@ +package me.alexdevs.solstice.modules.god.data; + +public class GodPlayerData { + public boolean invulnerabilityEnabled = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/hat/HatModule.java b/common/src/main/java/me/alexdevs/solstice/modules/hat/HatModule.java new file mode 100644 index 0000000..200f3c3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/hat/HatModule.java @@ -0,0 +1,58 @@ +package me.alexdevs.solstice.modules.hat; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.hat.commands.HatCommand; +import me.alexdevs.solstice.modules.hat.data.HatConfig; +import me.alexdevs.solstice.modules.hat.data.HatLocale; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import java.util.List; +import java.util.stream.Stream; + +public class HatModule extends ModuleBase.Toggleable { + public static final String ID = "hat"; + + public HatModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, HatConfig.class, HatConfig::new); + Solstice.localeManager.registerModule(ID, HatLocale.MODULE); + + commands.add(new HatCommand(this)); + } + + public HatConfig getConfig() { + return Solstice.configManager.getData(HatConfig.class); + } + + public List getConfigTags() { + return getConfig().filter.stream().filter(s -> s.startsWith("#")).toList(); + } + + public List getConfigItems() { + return getConfig().filter.stream().filter(s -> !s.startsWith("#")).toList(); + } + + public boolean isInFilter(String key) { + if (key.startsWith("#")) { + return getConfigTags().contains(key); + } else { + return getConfigItems().contains(key); + } + } + + public boolean isInFilter(Stream> stream) { + var tags = getConfigTags().stream().map(t -> t.substring(1)).toList(); + var iter = stream.iterator(); + while (iter.hasNext()) { + var tag = iter.next(); + if(tags.contains(tag.location().toString())) + return true; + } + return false; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/hat/commands/HatCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/hat/commands/HatCommand.java new file mode 100644 index 0000000..25a3f88 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/hat/commands/HatCommand.java @@ -0,0 +1,63 @@ +package me.alexdevs.solstice.modules.hat.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.hat.HatModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class HatCommand extends ModCommand { + public HatCommand(HatModule module) { + super(module); + } + + + @Override + public List getNames() { + return List.of("hat"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var handStack = player.getMainHandItem(); + + if (handStack.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyStack"), false); + return 0; + } + + var config = module.getConfig(); + + var itemId = handStack.getItemHolder().unwrapKey().get().location().toString(); + var tags = handStack.getTags(); + if (config.whitelistFilter) { + if(!module.isInFilter(itemId) && !module.isInFilter(tags)) { + context.getSource().sendSuccess(() -> module.locale().get("notAllowed"), false); + return 0; + } + } else { + if(module.isInFilter(itemId) || module.isInFilter(tags)) { + context.getSource().sendSuccess(() -> module.locale().get("notAllowed"), false); + return 0; + } + } + + //handStack.streamTags().toList().get(0).id().toString(); + + var inventory = player.getInventory(); + var oldHeadStack = inventory.armor.get(3); // head slot + inventory.setItem(inventory.selected, oldHeadStack.copyAndClear()); + inventory.armor.set(3, handStack.copyAndClear()); + + context.getSource().sendSuccess(() -> module.locale().get("success"), false); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatConfig.java new file mode 100644 index 0000000..f2cde2d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatConfig.java @@ -0,0 +1,18 @@ +package me.alexdevs.solstice.modules.hat.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.List; + +@ConfigSerializable +public class HatConfig { + + @Comment("Make the filter setting act as a whitelist instead of a blacklist.") + public boolean whitelistFilter = false; + + @Comment("Items & tags to allow/deny. See the 'whitelist-filter' setting to change the behaviour of this list.\nUse '#' as prefix to filter as tag.") + public List filter = List.of( + "#c:shulker_boxes" + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatLocale.java new file mode 100644 index 0000000..b844c5c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/hat/data/HatLocale.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.hat.data; + +import java.util.Map; + +public class HatLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("emptyStack", "You are not holding any item!"), + Map.entry("success", "Check out your new hat!"), + Map.entry("notAllowed", "You cannot wear this item!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/heal/HealModule.java b/common/src/main/java/me/alexdevs/solstice/modules/heal/HealModule.java new file mode 100644 index 0000000..3c9aa86 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/heal/HealModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.heal; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.heal.commands.HealCommand; + +public class HealModule extends ModuleBase.Toggleable { + public static final String ID = "heal"; + + public HealModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new HealCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/heal/commands/HealCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/heal/commands/HealCommand.java new file mode 100644 index 0000000..167fe47 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/heal/commands/HealCommand.java @@ -0,0 +1,66 @@ +package me.alexdevs.solstice.modules.heal.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.heal.HealModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class HealCommand extends ModCommand { + public HealCommand(HealModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("heal"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> execute(context, null)) + .then(argument("targets", EntityArgument.entities()) + .requires(require("others", 2)) + .executes(context -> execute(context, EntityArgument.getEntities(context, "targets")))); + } + + private int execute(CommandContext context, @Nullable Collection targets) throws CommandSyntaxException { + if (targets == null) { + var player = context.getSource().getPlayerOrException(); + heal(context, player); + return 1; + } else { + var healedCount = 0; + for (var target : targets) { + if (target instanceof LivingEntity livingEntity) { + healedCount++; + heal(context, livingEntity); + } + } + + if (healedCount == 0) { + context.getSource().sendFailure(Component.nullToEmpty("There are no living entities in the selector")); + } + return healedCount; + } + } + + private void heal(CommandContext context, LivingEntity entity) { + entity.setHealth(entity.getMaxHealth()); + context.getSource().sendSuccess(() -> Component.literal("Healed ").append(entity.getDisplayName()), true); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/helpOp/HelpOpModule.java b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/HelpOpModule.java new file mode 100644 index 0000000..351f6da --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/HelpOpModule.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.helpOp; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.helpOp.commands.HelpOpCommand; +import me.alexdevs.solstice.modules.helpOp.data.HelpOpLocale; + +public class HelpOpModule extends ModuleBase.Toggleable { + public static final String ID = "helpop"; + + public HelpOpModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, HelpOpLocale.MODULE); + + commands.add(new HelpOpCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/helpOp/commands/HelpOpCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/commands/HelpOpCommand.java new file mode 100644 index 0000000..67a756f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/commands/HelpOpCommand.java @@ -0,0 +1,64 @@ +package me.alexdevs.solstice.modules.helpOp.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.locale.Locale; +import me.alexdevs.solstice.modules.helpOp.HelpOpModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class HelpOpCommand extends ModCommand { + private final Locale locale = Solstice.localeManager.getLocale(HelpOpModule.ID); + + public HelpOpCommand(HelpOpModule module) { + super(module); + } + + + @Override + public List getNames() { + return List.of("helpop", "sos"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(context -> { + var source = context.getSource(); + var sourceContext = PlaceholderContext.of(source); + var message = StringArgumentType.getString(context, "message"); + + var placeholders = Map.of( + "message", Component.nullToEmpty(message) + ); + var requestMessage = locale.get( + "helpRequestMessage", + sourceContext, + placeholders + + ); + source.getServer().sendSystemMessage(requestMessage); + + source.getServer().getPlayerList().getPlayers().forEach(player -> { + if (Permissions.check(player, getPermissionNode("operator"), 1)) { + player.sendSystemMessage(requestMessage); + } + }); + + source.sendSuccess(() -> locale.get("helpRequestFeedback", sourceContext, placeholders), false); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/helpOp/data/HelpOpLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/data/HelpOpLocale.java new file mode 100644 index 0000000..adc70aa --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/helpOp/data/HelpOpLocale.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.helpOp.data; + +import java.util.Map; + +public class HelpOpLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("helpRequestMessage", "❗ [%player:displayname%] ${message}"), + Map.entry("helpRequestFeedback", "Help request sent: ${message}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/HomeModule.java b/common/src/main/java/me/alexdevs/solstice/modules/home/HomeModule.java new file mode 100644 index 0000000..bdac234 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/HomeModule.java @@ -0,0 +1,39 @@ +package me.alexdevs.solstice.modules.home; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.home.commands.*; +import me.alexdevs.solstice.modules.home.data.HomeConfig; +import me.alexdevs.solstice.modules.home.data.HomeLocale; +import me.alexdevs.solstice.modules.home.data.HomePlayerData; + +import java.util.UUID; + +public class HomeModule extends ModuleBase.Toggleable { + public static final String ID = "home"; + + public HomeModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, HomeConfig.class, HomeConfig::new); + Solstice.playerData.registerData(ID, HomePlayerData.class, HomePlayerData::new); + Solstice.localeManager.registerModule(ID, HomeLocale.MODULE); + + commands.add(new HomeCommand(this)); + commands.add(new SetHomeCommand(this)); + commands.add(new HomesCommand(this)); + commands.add(new DeleteHomeCommand(this)); + commands.add(new HomeOtherCommand(this)); + } + + public HomePlayerData getData(UUID player) { + return Solstice.playerData.get(player).getData(HomePlayerData.class); + } + + public HomeConfig getConfig() { + return Solstice.configManager.getData(HomeConfig.class); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/commands/DeleteHomeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/DeleteHomeCommand.java new file mode 100644 index 0000000..57cdcba --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/DeleteHomeCommand.java @@ -0,0 +1,74 @@ +package me.alexdevs.solstice.modules.home.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.home.HomeModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class DeleteHomeCommand extends ModCommand { + public DeleteHomeCommand(HomeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("delhome"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> execute(context, "home")) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isPlayer()) + return SharedSuggestionProvider.suggest(new String[]{}, builder); + + var data = module.getData(context.getSource().getPlayer().getUUID()); + + return SharedSuggestionProvider.suggest(data.homes.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String name) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var data = module.getData(player.getUUID()); + var playerContext = PlaceholderContext.of(player); + + var placeholders = Map.of( + "home", Component.nullToEmpty(name) + ); + + if (!data.homes.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get( + "homeNotFound", + playerContext, + placeholders + ), false); + return 1; + } + + data.homes.remove(name); + + context.getSource().sendSuccess(() -> module.locale().get( + "homeDeleted", + playerContext, + placeholders + ), false); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeCommand.java new file mode 100644 index 0000000..cc301f8 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeCommand.java @@ -0,0 +1,79 @@ +package me.alexdevs.solstice.modules.home.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.home.HomeModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class HomeCommand extends ModCommand { + public HomeCommand(HomeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("home"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> execute(context, "home")) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isPlayer()) + return SharedSuggestionProvider.suggest(new String[]{}, builder); + + var data = module.getData(context.getSource().getPlayer().getUUID()); + + return SharedSuggestionProvider.suggest(data.homes.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String name) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var data = module.getData(player.getUUID()); + var playerContext = PlaceholderContext.of(player); + + var placeholders = Map.of( + "home", Component.nullToEmpty(name) + ); + + if (!data.homes.containsKey(name)) { + context.getSource().sendSuccess(() -> + module.locale().get( + "homeNotFound", + playerContext, + placeholders + ), false); + + return 0; + } + + context.getSource().sendSuccess(() -> + module.locale().get( + "teleporting", + playerContext, + placeholders + ), false); + + var homePosition = data.homes.get(name); + homePosition.teleport(player); + + return 1; + } + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeOtherCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeOtherCommand.java new file mode 100644 index 0000000..eb7b123 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomeOtherCommand.java @@ -0,0 +1,76 @@ +package me.alexdevs.solstice.modules.home.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.home.HomeModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class HomeOtherCommand extends ModCommand { + public HomeOtherCommand(HomeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("homeother"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("others", 2)) + .then(argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> execute(context, "home")) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, StringArgumentType.getString(context, "name"))))); + } + + private int execute(CommandContext context, String name) throws CommandSyntaxException { + var sourcePlayer = context.getSource().getPlayerOrException(); + var profile = LocalGameProfile.getProfile(context, "player"); + var playerContext = PlaceholderContext.of(context.getSource().getPlayer()); + + + var data = module.getData(profile.getId()); + + var placeholders = Map.of( + "home", Component.nullToEmpty(name), + "owner", Component.nullToEmpty(profile.getName()) + ); + + if (!data.homes.containsKey(name)) { + context.getSource().sendSuccess(() -> + module.locale().get( + "homeNotFound", + playerContext, + placeholders + ), false); + + return 1; + } + + context.getSource().sendSuccess(() -> + module.locale().get( + "teleportingOther", + playerContext, + placeholders + ), true); + + var homePosition = data.homes.get(name); + homePosition.teleport(sourcePlayer); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomesCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomesCommand.java new file mode 100644 index 0000000..522600c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/HomesCommand.java @@ -0,0 +1,133 @@ +package me.alexdevs.solstice.modules.home.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.home.HomeModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class HomesCommand extends ModCommand { + public HomesCommand(HomeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("homes"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(this::execute) + .then(argument("player", StringArgumentType.word()) + .requires(require("others", 2)) + .suggests(LocalGameProfile::suggest) + .executes(context -> executeOthers(context, LocalGameProfile.getProfile(context, "player")))); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var data = module.getData(player.getUUID()); + var homeList = data.homes.keySet().stream().toList(); + var playerContext = PlaceholderContext.of(player); + + if (homeList.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get( + "noHomes", + playerContext + ), false); + return 1; + } + + var listText = Component.empty(); + var comma = module.locale().get("homesComma"); + for (var i = 0; i < homeList.size(); i++) { + if (i > 0) { + listText = listText.append(comma); + } + var placeholders = Map.of( + "home", Component.nullToEmpty(homeList.get(i)) + ); + + listText = listText.append(module.locale().get( + "homesFormat", + playerContext, + placeholders + )); + } + + var placeholders = Map.of( + "homeList", (Component) listText + ); + context.getSource().sendSuccess(() -> module.locale().get( + "homeList", + playerContext, + placeholders + ), false); + + return homeList.size(); + } + + private int executeOthers(CommandContext context, GameProfile profile) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var playerContext = PlaceholderContext.of(player); + + var data = module.getData(profile.getId()); + var homeList = data.homes.keySet().stream().toList(); + + if (homeList.isEmpty()) { + var placeholders = Map.of( + "owner", Component.nullToEmpty(profile.getName()) + ); + context.getSource().sendSuccess(() -> module.locale().get( + "noHomesOther", + playerContext, + placeholders + ), false); + return 1; + } + + var listText = Component.empty(); + var comma = module.locale().get("homesComma"); + for (var i = 0; i < homeList.size(); i++) { + if (i > 0) { + listText = listText.append(comma); + } + var placeholders = Map.of( + "home", Component.nullToEmpty(homeList.get(i)), + "owner", Component.nullToEmpty(profile.getName()) + ); + + listText = listText.append(module.locale().get( + "homesFormatOther", + playerContext, + placeholders + )); + } + + var placeholders = Map.of( + "homeList", listText, + "owner", Component.nullToEmpty(profile.getName()) + ); + context.getSource().sendSuccess(() -> module.locale().get( + "homeListOther", + playerContext, + placeholders + ), false); + + return homeList.size(); + } +} \ No newline at end of file diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/commands/SetHomeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/SetHomeCommand.java new file mode 100644 index 0000000..a92b33c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/commands/SetHomeCommand.java @@ -0,0 +1,109 @@ +package me.alexdevs.solstice.modules.home.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.home.HomeModule; +import me.alexdevs.solstice.api.text.Components; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SetHomeCommand extends ModCommand { + public SetHomeCommand(HomeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("sethome"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> execute(context, + "home", + false)) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, + StringArgumentType.getString(context, "name"), + false)) + .then(literal("force") + .executes(context -> execute(context, + StringArgumentType.getString(context, "name"), + true)))); + + } + + private int execute(CommandContext context, String name, boolean forced) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var data = module.getData(player.getUUID()); + var homes = data.homes; + var playerContext = PlaceholderContext.of(player); + + var placeholders = Map.of( + "home", Component.nullToEmpty(name), + "forceSetButton", Components.button( + module.locale().raw("forceSetLabel"), + module.locale().raw("forceSetHover"), + "/sethome " + name + " force" + ) + ); + + var exists = homes.containsKey(name); + if (exists && !forced) { + var text = module.locale().get( + "homeExists", + playerContext, + placeholders + ); + + context.getSource().sendSuccess(() -> text, false); + + return 0; + } + + homes.remove(name); + + var groups = module.getConfig().homes; + var maxHomes = Integer.MIN_VALUE; + for(var entry : groups.entrySet()) { + var group = entry.getKey(); + if(Permissions.check(player, "group." + group)) { + maxHomes = Math.max(maxHomes, entry.getValue()); + } + } + + var allowUnlimited = Permissions.check(player, getPermissionNode("unlimited"), 3); + if (!allowUnlimited && homes.size() >= maxHomes) { + context.getSource().sendSuccess(() -> module.locale().get( + "maxHomesReached", + playerContext, + placeholders + ), false); + return 0; + } + + var homePosition = new ServerLocation(player); + homes.put(name, homePosition); + + context.getSource().sendSuccess(() -> module.locale().get( + "homeSetSuccess", + playerContext, + placeholders + ), false); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeConfig.java new file mode 100644 index 0000000..50ee8ec --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeConfig.java @@ -0,0 +1,16 @@ +package me.alexdevs.solstice.modules.home.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.Map; + +@ConfigSerializable +public class HomeConfig { + + @Comment("The amount of homes a player can set based on their permission group. = .\nUse the permission 'solstice.home.unlimited' to bypass this limit.") + public Map homes = Map.of( + "default", 5, + "vip", 10 + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeLocale.java new file mode 100644 index 0000000..55fe8aa --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomeLocale.java @@ -0,0 +1,24 @@ +package me.alexdevs.solstice.modules.home.data; + +import java.util.Map; + +public class HomeLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("teleporting", "Teleporting to ${home}"), + Map.entry("teleportingOther", "Teleporting to ${owner}'s ${home}"), + Map.entry("homeExists", "You already have set this home.\n ${forceSetButton}"), + Map.entry("homeNotFound", "The home ${home} does not exist!"), + Map.entry("maxHomesReached", "You have reached the maximum amount of homes!"), + Map.entry("homeSetSuccess", "New home ${home} set!"), + Map.entry("forceSetLabel", "Force set home"), + Map.entry("forceSetHover", "Click to force setting new home"), + Map.entry("homeDeleted", "Home ${home} deleted!"), + Map.entry("homeList", "Your homes: ${homeList}"), + Map.entry("homeListOther", "${owner}'s homes: ${homeList}"), + Map.entry("homesFormat", "${home}"), + Map.entry("homesFormatOther", "${home}"), + Map.entry("homesComma", ", "), + Map.entry("noHomes", "You did not set any home so far."), + Map.entry("noHomesOther", "${owner} did not set any home so far.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomePlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomePlayerData.java new file mode 100644 index 0000000..4cfecb5 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/home/data/HomePlayerData.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.home.data; + +import me.alexdevs.solstice.api.ServerLocation; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.util.concurrent.ConcurrentHashMap; + +@ConfigSerializable +public class HomePlayerData { + public ConcurrentHashMap homes = new ConcurrentHashMap<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignite/IgniteModule.java b/common/src/main/java/me/alexdevs/solstice/modules/ignite/IgniteModule.java new file mode 100644 index 0000000..588b0c1 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignite/IgniteModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.ignite; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.ignite.commands.IgniteCommand; + +public class IgniteModule extends ModuleBase.Toggleable { + public static final String ID = "ignite"; + + public IgniteModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new IgniteCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignite/commands/IgniteCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ignite/commands/IgniteCommand.java new file mode 100644 index 0000000..b3b2441 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignite/commands/IgniteCommand.java @@ -0,0 +1,70 @@ +package me.alexdevs.solstice.modules.ignite.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ignite.IgniteModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class IgniteCommand extends ModCommand { + public static final int defaultTicks = 200; // 10 seconds + + public IgniteCommand(IgniteModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("ignite"); + } + + public LiteralArgumentBuilder command(String command) { + return literal(command) + .requires(require(2)) + .executes(context -> execute(context, null, null)) + .then(argument("players", EntityArgument.players()) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "players"), null)) + .then(argument("ticks", IntegerArgumentType.integer(0)) + .executes(context -> + execute(context, EntityArgument.getPlayers(context, "players"), IntegerArgumentType.getInteger(context, "ticks")) + ) + ) + ); + } + + private int execute(CommandContext context, @Nullable Collection players, @Nullable Integer ticks) throws CommandSyntaxException { + var source = context.getSource(); + if (players == null) { + ignite(source, source.getPlayerOrException(), ticks); + return 1; + } else { + for (ServerPlayer player : players) { + ignite(source, player, ticks); + } + + return players.size(); + } + } + + private void ignite(CommandSourceStack source, ServerPlayer player, @Nullable Integer ticks) { + if (ticks == null) { + player.setRemainingFireTicks(defaultTicks); + source.sendSuccess(() -> Component.literal("Ignited ").append(source.getDisplayName()), true); + } else { + player.setRemainingFireTicks(ticks); + source.sendSuccess(() -> Component.literal("Ignited ").append(source.getDisplayName()).append(Component.nullToEmpty(String.format(" for %d ticks", ticks))), true); + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignore/IgnoreModule.java b/common/src/main/java/me/alexdevs/solstice/modules/ignore/IgnoreModule.java new file mode 100644 index 0000000..20f5a5c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignore/IgnoreModule.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.ignore; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.ignore.commands.IgnoreCommand; +import me.alexdevs.solstice.modules.ignore.commands.IgnoreListCommand; +import me.alexdevs.solstice.modules.ignore.data.IgnoreLocale; +import me.alexdevs.solstice.modules.ignore.data.IgnorePlayerData; +import me.alexdevs.solstice.modules.styling.StylingModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.level.ServerPlayer; +import java.util.UUID; + +public class IgnoreModule extends ModuleBase.Toggleable { + public static final String ID = "ignore"; + + public IgnoreModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, IgnoreLocale.MODULE); + Solstice.playerData.registerData(ID, IgnorePlayerData.class, IgnorePlayerData::new); + + commands.add(new IgnoreCommand(this)); + commands.add(new IgnoreListCommand(this)); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && Solstice.modules.getModule(StylingModule.class).isEnabled(); + } + + public IgnorePlayerData getPlayerData(UUID playerUuid) { + return Solstice.playerData.get(playerUuid).getData(IgnorePlayerData.class); + } + + public boolean isIgnoring(ServerPlayer player, ServerPlayer target) { + return getPlayerData(player.getUUID()).ignoredPlayers.contains(target.getUUID()) && !Permissions.check(target, this.getPermissionNode("exempt"), 2); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreCommand.java new file mode 100644 index 0000000..07afe32 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreCommand.java @@ -0,0 +1,76 @@ +package me.alexdevs.solstice.modules.ignore.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class IgnoreCommand extends ModCommand { + public IgnoreCommand(IgnoreModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("ignore"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("target", StringArgumentType.word()) + .suggests((context, builder) -> { + var player = context.getSource().getPlayerOrException(); + var playerManager = context.getSource().getServer().getPlayerList(); + return SharedSuggestionProvider.suggest(Arrays.stream(playerManager.getPlayerNamesArray()).filter(s -> !s.equals(player.getGameProfile().getName())), builder); + }) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var targetName = StringArgumentType.getString(context, "target"); + + context.getSource().getServer().getProfileCache().getAsync(targetName).thenAcceptAsync(profileOpt -> { + var playerContext = PlaceholderContext.of(player); + + if (profileOpt.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("playerNotFound", playerContext), false); + return; + } + + var profile = profileOpt.get(); + + if (profile.getId().equals(player.getGameProfile().getId())) { + context.getSource().sendSuccess(() -> module.locale().get("targetIsSelf", playerContext), false); + return; + } + + var playerData = module.getPlayerData(player.getUUID()); + + var map = Map.of("targetName", Component.nullToEmpty(profile.getName())); + + if (playerData.ignoredPlayers.contains(profile.getId())) { + playerData.ignoredPlayers.remove(profile.getId()); + context.getSource().sendSuccess(() -> module.locale().get("unblockedPlayer", playerContext, map), false); + + } else { + playerData.ignoredPlayers.add(profile.getId()); + context.getSource().sendSuccess(() -> module.locale().get("blockedPlayer", playerContext, map), false); + } + }); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreListCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreListCommand.java new file mode 100644 index 0000000..eb32209 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignore/commands/IgnoreListCommand.java @@ -0,0 +1,79 @@ +package me.alexdevs.solstice.modules.ignore.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.literal; + +public class IgnoreListCommand extends ModCommand { + public IgnoreListCommand(IgnoreModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("ignorelist"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var playerData = module.getPlayerData(player.getUUID()); + var ignoreList = playerData.ignoredPlayers; + var playerContext = PlaceholderContext.of(player); + + if (ignoreList.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("ignoreListEmpty", + playerContext + ), false); + return 1; + } + + var listText = Component.empty(); + var comma = module.locale().get("ignoreListComma"); + for (var i = 0; i < ignoreList.size(); i++) { + if (i > 0) { + listText = listText.append(comma); + } + + String playerName; + var gameProfile = context.getSource().getServer().getProfileCache().get(ignoreList.get(i)); + if (gameProfile.isPresent()) { + playerName = gameProfile.get().getName(); + } else { + playerName = ignoreList.get(i).toString(); + } + + var placeholders = Map.of( + "player", Component.nullToEmpty(playerName) + ); + + listText = listText.append(module.locale().get( + "ignoreListFormat", + playerContext, + placeholders + )); + } + + var placeholders = Map.of( + "playerList", (Component) listText + ); + context.getSource().sendSuccess(() -> module.locale().get( + "ignoreList", + playerContext, + placeholders + ), false); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnoreLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnoreLocale.java new file mode 100644 index 0000000..bdef17b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnoreLocale.java @@ -0,0 +1,16 @@ +package me.alexdevs.solstice.modules.ignore.data; + +import java.util.Map; + +public class IgnoreLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("playerNotFound", "Could not find this player"), + Map.entry("targetIsSelf", "You cannot ignore yourself."), + Map.entry("blockedPlayer", "${targetName} is now ignored."), + Map.entry("unblockedPlayer", "${targetName} is no longer ignored."), + Map.entry("ignoreList", "Ignored players: ${playerList}"), + Map.entry("ignoreListFormat", "${player}"), + Map.entry("ignoreListComma", ", "), + Map.entry("ignoreListEmpty", "You are not ignoring anyone at the moment.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnorePlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnorePlayerData.java new file mode 100644 index 0000000..18fe01a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/ignore/data/IgnorePlayerData.java @@ -0,0 +1,8 @@ +package me.alexdevs.solstice.modules.ignore.data; + +import java.util.ArrayList; +import java.util.UUID; + +public class IgnorePlayerData { + public ArrayList ignoredPlayers = new ArrayList<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/InfoModule.java b/common/src/main/java/me/alexdevs/solstice/modules/info/InfoModule.java new file mode 100644 index 0000000..33225d2 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/InfoModule.java @@ -0,0 +1,135 @@ +package me.alexdevs.solstice.modules.info; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Paths; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.modules.info.commands.InfoCommand; +import me.alexdevs.solstice.modules.info.commands.MotdCommand; +import me.alexdevs.solstice.modules.info.commands.RulesCommand; +import me.alexdevs.solstice.modules.info.data.InfoConfig; +import me.alexdevs.solstice.modules.info.data.InfoLocale; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; + +public class InfoModule extends ModuleBase.Toggleable { + public static final String ID = "info"; + + private static final String[] startingPages = new String[]{ + "motd.txt", + "rules.txt", + "formatting.txt" + }; + public final String nameFilterRegex = "[^a-z0-9-]"; + private final Path infoDir; + + public InfoModule() { + super(ID); + infoDir = Paths.configDirectory.resolve("info"); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, InfoConfig.class, InfoConfig::new); + Solstice.localeManager.registerModule(ID, InfoLocale.MODULE); + + commands.add(new InfoCommand(this)); + commands.add(new MotdCommand(this)); + commands.add(new RulesCommand(this)); + + if (!infoDir.toFile().isDirectory()) { + if (!infoDir.toFile().mkdirs()) { + Solstice.LOGGER.error("Couldn't create info directory"); + return; + } + + var classLoader = Solstice.class.getClassLoader(); + var infoDirBase = "assets/" + Solstice.MOD_ID + "/info/"; + for (var name : startingPages) { + var outputPath = infoDir.resolve(name); + try (var inputStream = classLoader.getResourceAsStream(infoDirBase + name)) { + if (inputStream == null) { + Solstice.LOGGER.warn("Missing {} info file in resources, skipping", name); + continue; + } + var content = inputStream.readAllBytes(); + Files.write(outputPath, content); + } catch (IOException e) { + Solstice.LOGGER.error("Could not read info file {} from resources", name, e); + } + } + } + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + if (getConfig().enableMotd) { + if (!exists("motd")) { + Solstice.LOGGER.warn("Could not send MOTD because info/motd.txt does not exist!"); + return; + } + Solstice.nextTick(() -> { + var motd = buildMotd(PlaceholderContext.of(handler.getPlayer())); + handler.getPlayer().sendSystemMessage(motd); + }); + } + }); + } + + public InfoConfig getConfig() { + return Solstice.configManager.getData(InfoConfig.class); + } + + public Component buildMotd(PlaceholderContext context) { + return getPage("motd", context); + } + + private String sanitize(String name) { + return name.toLowerCase().replaceAll(nameFilterRegex, ""); + } + + public Collection enumerate() { + return Arrays.stream(Objects.requireNonNull(infoDir.toFile().listFiles())) + .map(f -> f.getName().replace(".txt", "")).toList(); + } + + public boolean exists(String name) { + name = sanitize(name); + var infoFile = infoDir.resolve(name + ".txt"); + return infoFile.toFile().exists(); + } + + public Component getPage(String name, @Nullable PlaceholderContext context) { + name = sanitize(name); + if (!exists(name)) { + return locale().get("pageNotFound"); + } + + var infoFile = infoDir.resolve(name + ".txt"); + + try { + // Use readAllLines instead readString to avoid \r chars; looking at you, Windows. + var lines = Files.readAllLines(infoFile, StandardCharsets.UTF_8); + StringBuilder content = new StringBuilder(); + for (var line : lines) { + content.append(line).append("\n"); + } + var output = content.toString().trim(); + if (context != null) + return Format.parse(output, context); + else + return Component.nullToEmpty(output); + } catch (IOException e) { + Solstice.LOGGER.error("Could not read info file", e); + return locale().get("pageError"); + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/commands/InfoCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/InfoCommand.java new file mode 100644 index 0000000..d6d8b8c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/InfoCommand.java @@ -0,0 +1,95 @@ +package me.alexdevs.solstice.modules.info.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.Utils; +import me.alexdevs.solstice.modules.info.InfoModule; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class InfoCommand extends ModCommand { + public InfoCommand(InfoModule module) { + super(module); + } + + @Override + public void register(CommandDispatcher dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) { + // WorldEdit's /info -> /tool info + Utils.removeCommands(dispatcher, "info"); + super.register(dispatcher, commandRegistry, environment); + } + + @Override + public List getNames() { + return List.of("info", "pages"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + + var source = context.getSource(); + var pageList = module.enumerate(); + var sourceContext = PlaceholderContext.of(source); + + if (pageList.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get( + "noPages", + sourceContext + ), false); + return 1; + } + + var listText = Component.empty(); + var comma = module.locale().get("pagesComma"); + var list = pageList.stream().toList(); + for (var i = 0; i < list.size(); i++) { + if (i > 0) { + listText = listText.append(comma); + } + var placeholders = Map.of( + "page", Component.nullToEmpty(list.get(i)) + ); + + listText = listText.append(module.locale().get( + "pagesFormat", + sourceContext, + placeholders + )); + } + + var placeholders = Map.of( + "pageList", (Component) listText + ); + context.getSource().sendSuccess(() -> module.locale().get( + "pageList", + sourceContext, + placeholders + ), false); + + return 1; + }) + .then(argument("page", StringArgumentType.word()) + .suggests((context, builder) -> SharedSuggestionProvider.suggest(module.enumerate(), builder)) + .executes(context -> { + var sourceContext = PlaceholderContext.of(context.getSource()); + var page = module.getPage(StringArgumentType.getString(context, "page"), sourceContext); + context.getSource().sendSuccess(() -> page, false); + return 1; + })); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/commands/MotdCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/MotdCommand.java new file mode 100644 index 0000000..a1cc78e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/MotdCommand.java @@ -0,0 +1,34 @@ +package me.alexdevs.solstice.modules.info.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.info.InfoModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class MotdCommand extends ModCommand { + public MotdCommand(InfoModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("motd"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("motd", true)) + .executes(context -> { + var sourceContext = PlaceholderContext.of(context.getSource()); + + context.getSource().sendSystemMessage(module.buildMotd(sourceContext)); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/commands/RulesCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/RulesCommand.java new file mode 100644 index 0000000..c767c36 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/commands/RulesCommand.java @@ -0,0 +1,33 @@ +package me.alexdevs.solstice.modules.info.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.info.InfoModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class RulesCommand extends ModCommand { + public RulesCommand(InfoModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("rules"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("rules", true)) + .executes(context -> { + var sourceContext = PlaceholderContext.of(context.getSource()); + var rules = module.getPage("rules", sourceContext); + context.getSource().sendSuccess(() -> rules, false); + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoConfig.java new file mode 100644 index 0000000..f606bc0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoConfig.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.info.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class InfoConfig { + @Comment("Send the 'Message Of The Day' to the player when joining the server. Content is in the 'config/solstice/info/motd.txt' file.") + public boolean enableMotd = true; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoLocale.java new file mode 100644 index 0000000..f0ef598 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/info/data/InfoLocale.java @@ -0,0 +1,14 @@ +package me.alexdevs.solstice.modules.info.data; + +import java.util.Map; + +public class InfoLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("pageNotFound", "This page does not exist!"), + Map.entry("pageError", "There was an error opening the info page."), + Map.entry("pageList", "Available pages: ${pageList}"), + Map.entry("pagesFormat", "${page}"), + Map.entry("pagesComma", ", "), + Map.entry("noPages ", "There are no pages so far.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/ImmutableSlot.java b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/ImmutableSlot.java new file mode 100644 index 0000000..cd2489d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/ImmutableSlot.java @@ -0,0 +1,28 @@ +package me.alexdevs.solstice.modules.inventorySee; + +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public class ImmutableSlot extends Slot { + + public ImmutableSlot(Container inventory, int index, int x, int y) { + super(inventory, index, x, y); + } + + @Override + public boolean mayPickup(Player playerEntity) { + return false; + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + + @Override + public boolean allowModification(Player player) { + return false; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/InventorySeeModule.java b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/InventorySeeModule.java new file mode 100644 index 0000000..0076f46 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/InventorySeeModule.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.inventorySee; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.inventorySee.commands.InventorySeeCommand; +import me.alexdevs.solstice.modules.inventorySee.data.InventorySeeLocale; + +public class InventorySeeModule extends ModuleBase.Toggleable { + public static final String ID = "inventorysee"; + + public InventorySeeModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, InventorySeeLocale.MODULE); + + commands.add(new InventorySeeCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/commands/InventorySeeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/commands/InventorySeeCommand.java new file mode 100644 index 0000000..319851d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/commands/InventorySeeCommand.java @@ -0,0 +1,215 @@ +package me.alexdevs.solstice.modules.inventorySee.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import dev.emi.trinkets.api.TrinketsApi; +import eu.pb4.sgui.api.gui.SimpleGui; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.utils.PlayerUtils; +import me.alexdevs.solstice.integrations.TrinketsIntegration; +import me.alexdevs.solstice.modules.inventorySee.ImmutableSlot; +import me.alexdevs.solstice.modules.inventorySee.InventorySeeModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class InventorySeeCommand extends ModCommand { + public InventorySeeCommand(InventorySeeModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("invsee", "inventorysee"); + } + + private static final LinkedHashMap> invSizes = new LinkedHashMap<>(); + + static { + invSizes.put(9, MenuType.GENERIC_9x1); + invSizes.put(18, MenuType.GENERIC_9x2); + invSizes.put(27, MenuType.GENERIC_9x3); + invSizes.put(36, MenuType.GENERIC_9x4); + invSizes.put(45, MenuType.GENERIC_9x5); + invSizes.put(54, MenuType.GENERIC_9x6); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var targetProfile = LocalGameProfile.getProfile(context, "player"); + var targetOnline = PlayerUtils.isOnline(targetProfile.getId()); + + if (!targetOnline && !Permissions.check(player, getPermissionNode("offline"), 3)) { + source.sendSuccess(() -> module.locale().get("offlineNotAllowed"), false); + return 0; + } + + ServerPlayer target; + if (targetOnline) { + target = context.getSource().getServer().getPlayerList().getPlayer(targetProfile.getId()); + if (Permissions.check(target, getPermissionNode("exempt"), 3)) { + source.sendSuccess(() -> module.locale().get("exempt"), false); + return 0; + } + } else { + target = PlayerUtils.loadOfflinePlayer(targetProfile); + if (Permissions.check(targetProfile, getPermissionNode("exempt"), 3, source.getServer()).getNow(false)) { + source.sendSuccess(() -> module.locale().get("exempt"), false); + return 0; + } + } + + var canEdit = Permissions.check(player, getPermissionNode("edit"), 3); + + var targetInventory = target.getInventory(); + + var container = new SimpleGui(MenuType.GENERIC_9x5, player, false) { + @Override + public void onClose() { + if (!targetOnline) { + PlayerUtils.saveOfflinePlayer(target); + } + } + }; + + for (var i = 0; i < targetInventory.getContainerSize(); i++) { + Slot slot; + if (canEdit) { + slot = new Slot(targetInventory, i, 0, 0); + } else { + slot = new ImmutableSlot(targetInventory, i, 0, 0); + } + container.setSlotRedirect(i, slot); + } + + var barrier = new ItemStack(Items.BLACK_STAINED_GLASS_PANE); + barrier.set(DataComponents.CUSTOM_NAME, Component.literal("")); + for (var i = targetInventory.getContainerSize(); i < container.getSize(); i++) { + container.setSlot(i, barrier); + } + + container.setTitle(target.getName()); + + container.open(); + + var map = Map.of( + "user", Component.nullToEmpty(target.getGameProfile().getName()) + ); + source.sendSuccess(() -> module.locale().get("openedInventory", map), true); + + return 1; + }) + .then(literal("trinkets") + .executes(context -> { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var targetProfile = LocalGameProfile.getProfile(context, "player"); + var targetOnline = PlayerUtils.isOnline(targetProfile.getId()); + + if (!targetOnline && !Permissions.check(player, getPermissionNode("offline"), 3)) { + source.sendSuccess(() -> module.locale().get("offlineNotAllowed"), false); + return 0; + } + + ServerPlayer target; + if (targetOnline) { + target = context.getSource().getServer().getPlayerList().getPlayer(targetProfile.getId()); + if (Permissions.check(target, getPermissionNode("exempt"), 3)) { + source.sendSuccess(() -> module.locale().get("exempt"), false); + return 0; + } + } else { + target = PlayerUtils.loadOfflinePlayer(targetProfile); + if (Permissions.check(targetProfile, getPermissionNode("exempt"), 3, source.getServer()).getNow(false)) { + source.sendSuccess(() -> module.locale().get("exempt"), false); + return 0; + } + } + + if (!TrinketsIntegration.isAvailable()) { + source.sendSuccess(() -> module.locale().get("trinketsNotInstalled"), false); + return 0; + } + + var canEdit = Permissions.check(player, getPermissionNode("edit"), 3); + + var trinkets = TrinketsApi.getTrinketComponent(target).orElse(null); + var slots = new ArrayList(); + for (var group : trinkets.getInventory().values()) { + for (var inventory : group.values()) { + for (var i = 0; i < inventory.getContainerSize(); i++) { + Slot slot; + if (canEdit) { + slot = new Slot(inventory, i, 0, 0); + } else { + slot = new ImmutableSlot(inventory, i, 0, 0); + } + slots.add(slot); + } + } + } + + var size = slots.size(); + MenuType handlerType = null; + for (var entry : invSizes.entrySet()) { + handlerType = entry.getValue(); + if (size <= entry.getKey()) { + break; + } + } + + var container = new SimpleGui(handlerType, player, false) { + @Override + public void onClose() { + if (!targetOnline) { + PlayerUtils.saveOfflinePlayer(target); + } + } + }; + + for (var i = 0; i < slots.size(); i++) { + var slot = slots.get(i); + container.setSlotRedirect(i, slot); + } + + var barrier = new ItemStack(Items.BLACK_STAINED_GLASS_PANE); + barrier.set(DataComponents.CUSTOM_NAME, Component.literal("")); + for (var i = size; i < container.getSize(); i++) { + container.setSlot(i, barrier); + } + + container.setTitle(target.getName()); + container.open(); + + var map = Map.of( + "user", Component.nullToEmpty(target.getGameProfile().getName()) + ); + source.sendSuccess(() -> module.locale().get("openedTrinkets", map), true); + + return 1; + })) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/data/InventorySeeLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/data/InventorySeeLocale.java new file mode 100644 index 0000000..fd826b3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/inventorySee/data/InventorySeeLocale.java @@ -0,0 +1,14 @@ +package me.alexdevs.solstice.modules.inventorySee.data; + +import java.util.Map; + +public class InventorySeeLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("exempt", "You cannot open this inventory because the user is exempt."), + Map.entry("openedInventory", "Opened ${user}'s inventory."), + Map.entry("openedTrinkets", "Opened ${user}'s trinkets inventory."), + Map.entry("trinketsNotInstalled", "Trinkets not available because the mod is missing."), + Map.entry("playerNotFound", "Player not found!"), + Map.entry("offlineNotAllowed", "You cannot open offline player inventories.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/ItemModule.java b/common/src/main/java/me/alexdevs/solstice/modules/item/ItemModule.java new file mode 100644 index 0000000..da32313 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/ItemModule.java @@ -0,0 +1,26 @@ +package me.alexdevs.solstice.modules.item; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.item.commands.ItemLoreCommand; +import me.alexdevs.solstice.modules.item.commands.ItemNameCommand; +import me.alexdevs.solstice.modules.item.commands.MoreCommand; +import me.alexdevs.solstice.modules.item.commands.RepairCommand; +import me.alexdevs.solstice.modules.item.data.ItemLocale; + +public class ItemModule extends ModuleBase.Toggleable { + public static final String ID = "item"; + public ItemModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, ItemLocale.MODULE); + + commands.add(new ItemLoreCommand(this)); + commands.add(new ItemNameCommand(this)); + commands.add(new RepairCommand(this)); + commands.add(new MoreCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemLoreCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemLoreCommand.java new file mode 100644 index 0000000..82b429f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemLoreCommand.java @@ -0,0 +1,75 @@ +package me.alexdevs.solstice.modules.item.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.modules.item.ItemModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.component.ItemLore; + +import java.util.ArrayList; +import java.util.List; + +public class ItemLoreCommand extends ModCommand { + public ItemLoreCommand(ItemModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("lore", "itemlore"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("lore", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var item = player.getMainHandItem(); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + item.remove(DataComponents.LORE); + + context.getSource().sendSuccess(() -> module.locale().get("loreCleared"), false); + + return 1; + }) + .then(Commands.argument("lore", StringArgumentType.greedyString()) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var item = player.getMainHandItem(); + var itemLore = StringArgumentType.getString(context, "lore"); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + var playerContext = PlaceholderContext.of(player); + var list = new ArrayList(); + for(var line : itemLore.split("\\\\n")) { + list.add(Format.parse(line, playerContext)); + } + + item.set(DataComponents.LORE, new ItemLore(list)); + + context.getSource().sendSuccess(() -> module.locale().get("loreSet"), false); + + return 1; + }) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemNameCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemNameCommand.java new file mode 100644 index 0000000..1ffaed3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/ItemNameCommand.java @@ -0,0 +1,64 @@ +package me.alexdevs.solstice.modules.item.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.modules.item.ItemModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.component.DataComponents; + +import java.util.List; + +public class ItemNameCommand extends ModCommand { + public ItemNameCommand(ItemModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("itemname"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("name", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var item = player.getMainHandItem(); + + if(item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + item.remove(DataComponents.CUSTOM_NAME); + + context.getSource().sendSuccess(() -> module.locale().get("nameCleared"), false); + + return 1; + }) + .then(Commands.argument("name", StringArgumentType.greedyString()) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var item = player.getMainHandItem(); + var itemName = StringArgumentType.getString(context, "name"); + + if(item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + var playerContext = PlaceholderContext.of(player); + item.set(DataComponents.CUSTOM_NAME, Format.parse(itemName, playerContext)); + + context.getSource().sendSuccess(() -> module.locale().get("nameSet"), false); + + return 1; + }) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/commands/MoreCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/MoreCommand.java new file mode 100644 index 0000000..891f8bb --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/MoreCommand.java @@ -0,0 +1,40 @@ +package me.alexdevs.solstice.modules.item.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.item.ItemModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import java.util.List; + +public class MoreCommand extends ModCommand { + public MoreCommand(ItemModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("more", "stack"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("more", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var item = player.getMainHandItem(); + + if(item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + item.setCount(item.getMaxStackSize()); + + context.getSource().sendSuccess(() -> module.locale().get("stackRefilled"), false); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/commands/RepairCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/RepairCommand.java new file mode 100644 index 0000000..477d663 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/commands/RepairCommand.java @@ -0,0 +1,54 @@ +package me.alexdevs.solstice.modules.item.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.item.ItemModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.component.DataComponents; + +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + + +public class RepairCommand extends ModCommand { + public RepairCommand(ItemModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("repair", "repairitem"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("repair", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var item = player.getMainHandItem(); + + if(item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noItem"), false); + return 0; + } + + if(!item.isDamageableItem()) { + context.getSource().sendSuccess(() -> module.locale().get("notRepairable"), false); + return 0; + } + + if(item.isDamaged()) { + // Removes the NBT tag altogether instead of just setting it to 0 + item.remove(DataComponents.DAMAGE); + context.getSource().sendSuccess(() -> module.locale().get("repaired"), false); + return 1; + } else { + context.getSource().sendSuccess(() -> module.locale().get("alreadyRepaired"), false); + return 0; + } + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/item/data/ItemLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/item/data/ItemLocale.java new file mode 100644 index 0000000..350f8bc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/item/data/ItemLocale.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.item.data; + +import java.util.Map; + +public class ItemLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("noItem", "You are not holding any item!"), + Map.entry("nameSet", "Successfully set the new item name!"), + Map.entry("nameCleared", "Item name cleared!"), + Map.entry("loreSet", "Successfully set the new item lore!"), + Map.entry("loreCleared", "Item lore cleared!"), + Map.entry("repaired", "Item repaired!"), + Map.entry("alreadyRepaired", "This item is already repaired!"), + Map.entry("notRepairable", "This item is not repairable!"), + Map.entry("stackRefilled", "Item stack refilled!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/JailModule.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/JailModule.java new file mode 100644 index 0000000..7146214 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/JailModule.java @@ -0,0 +1,230 @@ +package me.alexdevs.solstice.modules.jail; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.events.CommandEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.jail.commands.CheckJailCommand; +import me.alexdevs.solstice.modules.jail.commands.JailCommand; +import me.alexdevs.solstice.modules.jail.commands.JailsCommand; +import me.alexdevs.solstice.modules.jail.commands.UnjailCommand; +import me.alexdevs.solstice.modules.jail.data.JailConfig; +import me.alexdevs.solstice.modules.jail.data.JailLocale; +import me.alexdevs.solstice.modules.jail.data.JailPlayerData; +import me.alexdevs.solstice.modules.jail.data.JailServerData; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.player.*; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class JailModule extends ModuleBase.Toggleable { + public static final String ID = "jail"; + + public JailModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, JailConfig.class, JailConfig::new); + Solstice.localeManager.registerModule(ID, JailLocale.MODULE); + Solstice.playerData.registerData(ID, JailPlayerData.class, JailPlayerData::new); + Solstice.serverData.registerData(ID, JailServerData.class, JailServerData::new); + + commands.add(new JailsCommand(this)); + commands.add(new JailCommand(this)); + commands.add(new UnjailCommand(this)); + commands.add(new CheckJailCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + Solstice.nextTick(() -> { + var data = getPlayer(handler.getPlayer().getUUID()); + if (data.jailed) { + sendToJail(handler.getPlayer()); + } else if (data.teleportToPreviousLocation) { + unjailPlayer(handler.getPlayer().getUUID()); + } + }); + }); + + ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, player, alive) -> { + if (isPlayerJailed(player.getUUID())) { + sendToJail(player); + } + }); + + ServerLifecycleEvents.SERVER_STARTED.register((server) -> { + Solstice.scheduler.scheduleAtFixedRate(this::checkJailedPlayers, 0, 1, TimeUnit.SECONDS); + }); + + CommandEvents.ALLOW_COMMAND.register((source, command) -> { + if (!source.isPlayer()) + return true; + + if (isPlayerJailed(source.getPlayer().getUUID())) { + var config = getConfig(); + var cmd = command.split(" ")[0]; + var canRun = config.allowedCommands.contains(cmd); + if (!canRun) { + source.sendSuccess(() -> locale().get("cannotRunCommands"), false); + } + return canRun; + } + + return true; + }); + + AttackBlockCallback.EVENT.register((player, world, hand, blockPos, direction) -> { + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotBreakBlocks")); + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + }); + + AttackEntityCallback.EVENT.register((player, world, hand, entity, entityHitResult) -> { + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotAttackEntities")); + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + }); + + PlayerBlockBreakEvents.BEFORE.register((world, player, blockPos, blockState, blockEntity) -> { + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotBreakBlocks")); + return false; + } + + return true; + }); + + UseBlockCallback.EVENT.register((player, world, hand, blockHitResult) -> { + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotUseBlocks")); + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + }); + + UseEntityCallback.EVENT.register((player, world, hand, entity, entityHitResult) -> { + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotUseEntities")); + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + }); + + UseItemCallback.EVENT.register((player, world, hand) -> { + var stack = player.getItemInHand(hand); + if (isPlayerJailed(player.getUUID())) { + player.sendSystemMessage(locale().get("cannotUseItems")); + return InteractionResultHolder.fail(stack); + } + return InteractionResultHolder.pass(stack); + }); + + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((signedMessage, player, parameters) -> { + if (isPlayerJailed(player.getUUID())) { + var config = getConfig(); + if(config.mute) { + player.sendSystemMessage(locale().get("cannotSpeak")); + return false; + } + } + return true; + }); + } + + private void checkJailedPlayers() { + // run on server thread + Solstice.nextTick(() -> { + var players = Solstice.server.getPlayerList().getPlayers(); + for (var player : players) { + var data = getPlayer(player.getUUID()); + if (isPlayerJailed(player.getUUID()) && data.jailTime > 0) { + if (data.jailedOn != null && data.jailedOn.getTime() + (data.jailTime * 1000L) < System.currentTimeMillis()) { + unjailPlayer(player.getUUID()); + } + } + } + + }); + } + + public JailConfig getConfig() { + return Solstice.configManager.getData(JailConfig.class); + } + + public Map getJails() { + return Solstice.serverData.getData(JailServerData.class).jails; + } + + public JailPlayerData getPlayer(UUID uuid) { + return Solstice.playerData.get(uuid).getData(JailPlayerData.class); + } + + public boolean isPlayerJailed(UUID uuid) { + return getPlayer(uuid).jailed; + } + + public void sendToJail(ServerPlayer player) { + Solstice.nextTick(() -> { + var data = getPlayer(player.getUUID()); + var jails = getJails(); + var jail = jails.get(data.jailName); + if (jail != null) { + jail.teleport(player); + + var map = Map.of( + "player", player.getName(), + "jail", Component.nullToEmpty(data.jailName), + "duration", Component.nullToEmpty(TimeSpan.toLongString(data.jailTime)), + "reason", Component.nullToEmpty(data.jailReason) + ); + + Component text; + if (data.jailTime > 0) { + if (data.jailReason != null) { + text = locale().get("playerJailedForWithReason", map); + } else { + text = locale().get("playerJailedFor", map); + } + } else { + text = locale().get("playerJailed", map); + } + player.displayClientMessage(text, false); + } + }); + } + + public void unjailPlayer(UUID uuid) { + var data = getPlayer(uuid); + data.jailed = false; + data.teleportToPreviousLocation = true; + + var player = Solstice.server.getPlayerList().getPlayer(uuid); + if (player != null) { + data.teleportToPreviousLocation = false; + + player.sendSystemMessage(locale().get("playerUnjailed")); + + if (data.previousLocation != null) { + data.previousLocation.teleport(player); + } else { + var spawnModule = Solstice.modules.getModule(SpawnModule.class); + spawnModule.getGlobalSpawnPosition().teleport(player); + } + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/CheckJailCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/CheckJailCommand.java new file mode 100644 index 0000000..ed8f0b0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/CheckJailCommand.java @@ -0,0 +1,103 @@ +package me.alexdevs.solstice.modules.jail.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.core.coreModule.data.CoreConfig; +import me.alexdevs.solstice.modules.jail.JailModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class CheckJailCommand extends ModCommand { + + public CheckJailCommand(JailModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("checkjail"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(2)) + .then(Commands.argument("user", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var user = LocalGameProfile.getProfile(context, "user"); + var data = module.getPlayer(user.getId()); + + if (!data.jailed) { + context.getSource().sendSuccess(() -> module.locale().get("notJailed"), false); + return 0; + } + + String operator; + if (new UUID(0, 0).equals(data.jailedBy)) { + operator = "Server"; + } else { + var opProfile = context.getSource().getServer().getProfileCache().get(data.jailedBy); + if (opProfile.isPresent()) { + operator = opProfile.get().getName(); + } else { + operator = data.jailedBy != null ? data.jailedBy.toString() : "Unknown"; + } + } + + String reason; + if (data.jailReason != null) { + reason = data.jailReason; + } else { + reason = module.locale().raw("infoJailReasonEmpty"); + } + + String duration; + if (data.jailTime == 0) { + duration = module.locale().raw("infoJailedForEmpty"); + } else { + duration = TimeSpan.toLongString(data.jailTime); + } + + var coreConfig = Solstice.configManager.getData(CoreConfig.class); + var df = new SimpleDateFormat(coreConfig.dateTimeFormat); + + var map = Map.of( + "player", Component.nullToEmpty(user.getName()), + "jail", Component.nullToEmpty(data.jailName), + "operator", Component.nullToEmpty(operator), + "reason", Component.nullToEmpty(reason), + "duration", Component.nullToEmpty(duration), + "date", Component.nullToEmpty(df.format(data.jailedOn)) + + ); + + var text = Component.empty(); + text.append(module.locale().get("infoHeader", map)); + text.append("\n"); + text.append(module.locale().get("infoJailedAt", map)); + text.append("\n"); + text.append(module.locale().get("infoJailedBy", map)); + text.append("\n"); + text.append(module.locale().get("infoJailReason", map)); + text.append("\n"); + text.append(module.locale().get("infoJailedFor", map)); + text.append("\n"); + text.append(module.locale().get("infoJailedOn", map)); + + context.getSource().sendSuccess(() -> text, false); + + return 1; + }) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailCommand.java new file mode 100644 index 0000000..ca7159e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailCommand.java @@ -0,0 +1,132 @@ +package me.alexdevs.solstice.modules.jail.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.core.coreModule.data.CorePlayerData; +import me.alexdevs.solstice.modules.jail.JailModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class JailCommand extends ModCommand { + public JailCommand(JailModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("jail"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("jail", 2)) + .then(Commands.argument("user", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .then(Commands.argument("jail", StringArgumentType.word()) + .suggests(this::suggestJails) + .executes(context -> execute(context, 0, null)) + .then(Commands.argument("duration", TimeSpan.timeSpan()) + .suggests(TimeSpan::suggest) + .executes(context -> execute(context, TimeSpan.getTimeSpan(context, "duration"), null)) + .then(Commands.argument("reason", StringArgumentType.greedyString()) + .executes(context -> execute(context, TimeSpan.getTimeSpan(context, "duration"), StringArgumentType.getString(context, "reason"))) + ) + ) + ) + ); + } + + private int execute(CommandContext context, int seconds, @Nullable String reason) throws CommandSyntaxException { + var source = context.getSource(); + var profile = LocalGameProfile.getProfile(context, "user"); + + var data = module.getPlayer(profile.getId()); + var coreData = Solstice.playerData.get(profile.getId()).getData(CorePlayerData.class); + + if (data.jailed) { + source.sendSuccess(() -> module.locale().get("alreadyJailed"), false); + return 0; + } + + var jailName = StringArgumentType.getString(context, "jail"); + + var jails = module.getJails(); + if (!jails.containsKey(jailName)) { + source.sendSuccess(() -> module.locale().get("jailNotFound"), false); + return 0; + } + + Permissions.check(profile, getPermissionNode("exempt")).thenAccept(granted -> { + if (granted) { + source.sendSuccess(() -> module.locale().get("playerExempt"), false); + return; + } + + var player = source.getServer().getPlayerList().getPlayer(profile.getId()); + + data.jailed = true; + data.jailedBy = source.isPlayer() ? source.getPlayer().getUUID() : new UUID(0L, 0L); + data.jailedOn = new Date(); + data.jailName = jailName; + data.jailTime = seconds; + data.jailReason = reason; + + if (player != null) { + data.previousLocation = new ServerLocation(player); + } else { + data.previousLocation = coreData.logoffPosition; + } + + var map = Map.of( + "player", Component.nullToEmpty(profile.getName()), + "jail", Component.nullToEmpty(jailName), + "duration", Component.nullToEmpty(TimeSpan.toLongString(seconds)), + "reason", Component.nullToEmpty(reason) + ); + + Component text; + if (seconds > 0) { + if (reason != null) { + text = module.locale().get("jailedForWithReason", map); + } else { + text = module.locale().get("jailedFor", map); + } + } else { + text = module.locale().get("jailed", map); + } + + source.sendSuccess(() -> text, true); + + if (player != null) { + module.sendToJail(player); + } + }); + + return 1; + } + + private CompletableFuture suggestJails(CommandContext context, SuggestionsBuilder builder) { + var jails = module.getJails().keySet().stream(); + return SharedSuggestionProvider.suggest(jails, builder); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailsCommand.java new file mode 100644 index 0000000..29c9457 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/JailsCommand.java @@ -0,0 +1,149 @@ +package me.alexdevs.solstice.modules.jail.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.jail.JailModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class JailsCommand extends ModCommand { + public JailsCommand(JailModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("jails"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(2)) + .executes(this::listJails) + .then(Commands.literal("set") + .requires(require("set", 3)) + .then(Commands.argument("name", StringArgumentType.word()) + .executes(this::createJail) + )) + .then(Commands.literal("delete") + .requires(require("set", 3)) + .then(Commands.argument("name", StringArgumentType.word()) + .suggests(this::suggestJails) + .executes(this::deleteJail) + )) + .then(Commands.literal("tp") + .requires(require("tp", 2)) + .then(Commands.argument("name", StringArgumentType.word()) + .suggests(this::suggestJails) + .executes(this::teleport) + )); + } + + private CompletableFuture suggestJails(CommandContext context, SuggestionsBuilder builder) { + var jails = module.getJails().keySet().stream(); + return SharedSuggestionProvider.suggest(jails, builder); + } + + private int listJails(CommandContext context) { + var jails = module.getJails().keySet().stream().toList(); + + if(jails.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noJails"), false); + return 0; + } + + var comma = module.locale().get("comma"); + var list = Component.empty(); + + for(var i = 0; i < jails.size(); i++) { + if(i > 0) { + list.append(comma); + } + + list.append(module.locale().get("listEntry", Map.of( + "jail", Component.nullToEmpty(jails.get(i)) + ))); + } + + context.getSource().sendSuccess(() -> module.locale().get("jailList", Map.of( + "list", list + )), false); + + return 1; + } + + private int createJail(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var jailName = StringArgumentType.getString(context, "name"); + + var position = new ServerLocation(player); + + var jails = module.getJails(); + if (jails.containsKey(jailName)) { + context.getSource().sendSuccess(() -> module.locale().get("jailAlreadyExists"), false); + return 0; + } + + jails.put(jailName, position); + + var map = Map.of( + "jail", Component.nullToEmpty(jailName) + ); + context.getSource().sendSuccess(() -> module.locale().get("created", map), true); + + return 1; + } + + private int deleteJail(CommandContext context) { + var jailName = StringArgumentType.getString(context, "name"); + + var jails = module.getJails(); + if (!jails.containsKey(jailName)) { + context.getSource().sendSuccess(() -> module.locale().get("jailNotFound"), false); + return 0; + } + + jails.remove(jailName); + + var map = Map.of( + "jail", Component.nullToEmpty(jailName) + ); + context.getSource().sendSuccess(() -> module.locale().get("deleted", map), true); + + return 1; + } + + private int teleport(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var jailName = StringArgumentType.getString(context, "name"); + var jails = module.getJails(); + if(!jails.containsKey(jailName)) { + context.getSource().sendSuccess(() -> module.locale().get("jailNotFound"), false); + return 0; + } + + var jail = jails.get(jailName); + + var map = Map.of( + "jail", Component.nullToEmpty(jailName) + ); + + context.getSource().sendSuccess(() -> module.locale().get("teleporting", map), true); + + jail.teleport(player); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/UnjailCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/UnjailCommand.java new file mode 100644 index 0000000..319c255 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/commands/UnjailCommand.java @@ -0,0 +1,51 @@ +package me.alexdevs.solstice.modules.jail.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.jail.JailModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +public class UnjailCommand extends ModCommand { + public UnjailCommand(JailModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("unjail"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("unjail", 2)) + .then(Commands.argument("user", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var profile = LocalGameProfile.getProfile(context, "user"); + + var data = module.getPlayer(profile.getId()); + + if (!data.jailed) { + context.getSource().sendSuccess(() -> module.locale().get("notJailed"), false); + return 0; + } + + module.unjailPlayer(profile.getId()); + + var map = Map.of( + "player", Component.nullToEmpty(profile.getName()) + ); + context.getSource().sendSuccess(() -> module.locale().get("unjailed", map), false); + + return 1; + }) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailConfig.java new file mode 100644 index 0000000..bdd0cca --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailConfig.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.jail.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.List; + +@ConfigSerializable +public class JailConfig { + @Comment("List of commands the jailed players can execute.") + public List allowedCommands = List.of( + "afk", + "ignore", + "msg", "tell", "w", "dm", "r", "reply", + "mail", + "info", "motd", "rules" + ); + + @Comment("Mute jailed players. They will not be able to send chat messages.") + public boolean mute = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailLocale.java new file mode 100644 index 0000000..9b6ee95 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailLocale.java @@ -0,0 +1,49 @@ +package me.alexdevs.solstice.modules.jail.data; + +import java.util.Map; + +public class JailLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("jailNotFound", "This jail does not exist."), + Map.entry("jailAlreadyExists", "A jail with this name already exists."), + Map.entry("teleporting", "Teleporting to ${jail}..."), + Map.entry("created", "New jail ${jail} created!"), + Map.entry("deleted", "Jail ${jail} deleted!"), + Map.entry("notJailed", "This user is not jailed!"), + Map.entry("alreadyJailed", "This player is already jailed!"), + + Map.entry("jailed", "${player} has been jailed at ${jail} indefinitely!"), + Map.entry("jailedFor", "${player} has been jailed at ${jail} for ${duration}!"), + Map.entry("jailedForWithReason", "${player} has been jailed at ${jail} for ${duration} with reason: ${reason}"), + + Map.entry("playerJailed", "You have been jailed!"), + Map.entry("playerJailedFor", "You have been jailed for ${duration}!"), + Map.entry("playerJailedForWithReason", "You have been jailed for ${duration} with reason: ${reason}"), + Map.entry("playerUnjailed", "You have been unjailed!"), + Map.entry("unjailed", "Unjailed ${player}!"), + + Map.entry("cannotRunCommands", "You are not allowed to run this command in jail!"), + Map.entry("cannotBreakBlocks", "You are not allowed to break blocks in jail!"), + Map.entry("cannotAttackEntities", "You are not allowed to attack entities in jail!"), + Map.entry("cannotUseBlocks", "You are not allowed to use blocks in jail!"), + Map.entry("cannotUseEntities", "You are not allowed to interact with entities in jail!"), + Map.entry("cannotUseItems", "You are not allowed to use items in jail!"), + Map.entry("cannotSpeak", "You are not allowed to speak in jail!"), + + Map.entry("playerExempt", "This player is exempt from being jailed!"), + + Map.entry("jailList", "Jails: ${list}."), + Map.entry("listEntry", "${jail}"), + Map.entry("comma", ", "), + Map.entry("noJails", "There are no jails yet."), + + Map.entry("infoHeader", "${player} Jail Information:"), + Map.entry("infoJailedAt", "Jailed at: ${jail}"), + Map.entry("infoJailedBy", "Jailed by: ${operator}"), + Map.entry("infoJailReason", "Reason: ${reason}"), + Map.entry("infoJailReasonEmpty", "No reason"), + Map.entry("infoJailedFor", "Jail time: ${duration}"), + Map.entry("infoJailedForEmpty", "Indefinitely"), + Map.entry("infoJailedOn", "Jailed on: ${date}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailPlayerData.java new file mode 100644 index 0000000..1f6bd75 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailPlayerData.java @@ -0,0 +1,18 @@ +package me.alexdevs.solstice.modules.jail.data; + +import me.alexdevs.solstice.api.ServerLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.UUID; + +public class JailPlayerData { + public boolean jailed = false; + public @Nullable String jailName = null; + public @Nullable UUID jailedBy = null; + public @Nullable Date jailedOn = null; + public int jailTime = 0; + public @Nullable String jailReason = null; + public @Nullable ServerLocation previousLocation = null; + public boolean teleportToPreviousLocation = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailServerData.java b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailServerData.java new file mode 100644 index 0000000..d8cb13e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/jail/data/JailServerData.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.jail.data; + +import me.alexdevs.solstice.api.ServerLocation; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JailServerData { + public Map jails = new ConcurrentHashMap<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kick/KickModule.java b/common/src/main/java/me/alexdevs/solstice/modules/kick/KickModule.java new file mode 100644 index 0000000..40fd303 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kick/KickModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.kick; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.kick.commands.KickCommand; + +public class KickModule extends ModuleBase.Toggleable { + public static final String ID = "kick"; + + public KickModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new KickCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kick/commands/KickCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/kick/commands/KickCommand.java new file mode 100644 index 0000000..31f1f21 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kick/commands/KickCommand.java @@ -0,0 +1,64 @@ +package me.alexdevs.solstice.modules.kick.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.Utils; +import me.alexdevs.solstice.modules.kick.KickModule; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import me.alexdevs.solstice.api.text.Format; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class KickCommand extends ModCommand { + public KickCommand(KickModule module) { + super(module); + } + + private static int execute(CommandContext context, Collection targets, @Nullable String reason) { + var source = context.getSource(); + for (var target : targets) { + var playerContext = PlaceholderContext.of(target); + var reasonText = reason != null ? Format.parse(reason, playerContext) : Component.translatable("multiplayer.disconnect.kicked"); + target.connection.disconnect(reasonText); + source.sendSuccess(() -> Component.translatable("commands.kick.success", target.getDisplayName(), reasonText), true); + } + + return targets.size(); + } + + @Override + public void register(CommandDispatcher dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) { + Utils.removeCommands(dispatcher, "kick"); + super.register(dispatcher, commandRegistry, environment); + } + + @Override + public List getNames() { + return List.of("kick"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .then(argument("targets", EntityArgument.players()) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "targets"), null)) + .then(argument("reason", StringArgumentType.greedyString()) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "targets"), StringArgumentType.getString(context, "reason")))) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/KitInventory.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/KitInventory.java new file mode 100644 index 0000000..a055727 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/KitInventory.java @@ -0,0 +1,61 @@ +package me.alexdevs.solstice.modules.kit; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public class KitInventory implements Container { + public static final int SIZE = 27; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return items.isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + var stack = getItem(slot); + var taken = stack.copyWithCount(amount); + stack.shrink(amount); + return taken; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + var stack = items.get(slot); + return stack.copyAndClear(); + } + + @Override + public void setItem(int slot, ItemStack stack) { + items.set(slot, stack); + } + + @Override + public void setChanged() { + + } + + @Override + public boolean stillValid(Player player) { + return false; + } + + @Override + public void clearContent() { + items.clear(); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/KitModule.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/KitModule.java new file mode 100644 index 0000000..de1046e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/KitModule.java @@ -0,0 +1,122 @@ +package me.alexdevs.solstice.modules.kit; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.kit.commands.KitCommand; +import me.alexdevs.solstice.modules.kit.commands.KitsCommand; +import me.alexdevs.solstice.modules.kit.data.*; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class KitModule extends ModuleBase.Toggleable { + public static final String ID = "kit"; + + public KitModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, KitConfig.class, KitConfig::new); + Solstice.localeManager.registerModule(ID, KitLocale.MODULE); + Solstice.playerData.registerData(ID, KitPlayerData.class, KitPlayerData::new); + Solstice.serverData.registerData(ID, KitServerData.class, KitServerData::new); + + commands.add(new KitCommand(this)); + commands.add(new KitsCommand(this)); + + SolsticeEvents.WELCOME.register((player, server) -> { + for (var kit : getKits().entrySet()) { + if (kit.getValue().firstJoin) { + claimKit(player, kit.getKey()); + } + } + }); + } + + public Map getKits() { + return Solstice.serverData.getData(KitServerData.class).kits; + } + + public boolean createKit(String name, List items) { + var kits = getKits(); + if (kits.containsKey(name)) { + return false; + } + var kit = new Kit(); + kit.itemStacks = items.stream().map(Utils::serializeItemStack).toList(); + kits.put(name, kit); + return true; + } + + /** + * Claim a kit regardless if the player could claim it. + * Also flag the player as having it claimed. + * + * @param player Player + * @param name Kit name + */ + public void claimKit(ServerPlayer player, String name) { + var playerData = Solstice.playerData.get(player).getData(KitPlayerData.class); + var kit = getKits().get(name); + var items = kit.getItemStacks(); + var inventory = player.getInventory(); + for (var stack : items) { + inventory.add(stack); + } + playerData.claimedKits.put(name, new Date()); + } + + /** + * Check if a player has permission to claim a kit. + * + * @param player Player + * @param name Kit name + * @return Whether the player has permission to claim the kit. + */ + public boolean hasKitPermission(ServerPlayer player, String name) { + var config = Solstice.configManager.getData(KitConfig.class); + if (config.requirePermission) { + return Permissions.check(player, getPermissionNode("kits." + name), 2); + } else { + return Permissions.check(player, getPermissionNode("kits." + name), true); + } + } + + /** + * Check if a player could technically claim the kit regardless of permission. + * This method checks the oneTime flag and cooldown. + * + * @param player Player + * @param name Kit name + * @return Whether the player could claim the kit. + */ + public boolean couldClaimKit(ServerPlayer player, String name) { + var kit = getKits().get(name); + var playerData = Solstice.playerData.get(player).getData(KitPlayerData.class); + if (kit.oneTime && playerData.claimedKits.containsKey(name)) { + return false; + } + + if (kit.cooldownSeconds > 0) { + if (playerData.claimedKits.containsKey(name)) { + var startDate = playerData.claimedKits.get(name); + var nowDate = new Date(); + + var delta = (nowDate.getTime() - startDate.getTime()) / 1000; + return delta >= kit.cooldownSeconds; + } + } + + return true; + } + + public List getPlayerKitNames(ServerPlayer player) { + return getKits().keySet().stream().filter(kit -> hasKitPermission(player, kit)).toList(); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/Utils.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/Utils.java new file mode 100644 index 0000000..d1bfdab --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/Utils.java @@ -0,0 +1,52 @@ +package me.alexdevs.solstice.modules.kit; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.sgui.api.gui.SimpleGui; +import java.util.ArrayList; +import java.util.List; + +import me.alexdevs.solstice.Solstice; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public class Utils { + public static String serializeItemStack(ItemStack itemStack) { + var registry = Solstice.server.registryAccess(); + var nbt = itemStack.save(registry); + return nbt.getAsString(); + } + + public static ItemStack deserializeItemStack(String string) throws CommandSyntaxException { + var registry = Solstice.server.registryAccess(); + var nbt = TagParser.parseTag(string); + return ItemStack.parseOptional(registry, nbt); + } + + public static KitInventory createInventory(List items) { + var inventory = new KitInventory(); + for (var i = 0; i < items.size(); i++) { + inventory.setItem(i, items.get(i)); + } + return inventory; + } + + public static List getItemStacks(KitInventory inventory) { + var items = new ArrayList(); + for (var i = 0; i < inventory.getContainerSize(); i++) { + var stack = inventory.getItem(i); + if(!stack.isEmpty()) { + items.add(stack); + } + } + return items; + } + + public static void redirect(SimpleGui container, KitInventory inventory) { + for(var i = 0; i < container.getSize(); i++) { + container.setSlotRedirect(i, new Slot(inventory, i, 0, 0)); + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitCommand.java new file mode 100644 index 0000000..8e48985 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitCommand.java @@ -0,0 +1,347 @@ +package me.alexdevs.solstice.modules.kit.commands; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.kit.KitInventory; +import me.alexdevs.solstice.modules.kit.KitModule; +import me.alexdevs.solstice.modules.kit.Utils; +import me.alexdevs.solstice.modules.kit.data.KitPlayerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.MenuType; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class KitCommand extends ModCommand { + public KitCommand(KitModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("kit"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(literal("list") + .executes(this::listKits)) + .then(literal("claim") + .then(argument("name", StringArgumentType.word()) + .suggests(this::suggestKitList) + .executes(this::claimKit)) + ) + .then(literal("create") + .requires(require("set", 3)) + .then(argument("name", StringArgumentType.word()) + .executes(this::createKit)) + ) + .then(literal("delete") + .requires(require("set", 3)) + .then(argument("name", StringArgumentType.word()) + .suggests(this::suggestAllKits) + .executes(this::deleteKit)) + ) + .then(literal("edit") + .requires(require("set", 3)) + .then(argument("name", StringArgumentType.word()) + .suggests(this::suggestKitList) + .executes(this::editKit)) + ) + .then(literal("set") + .requires(require("set", 3)) + .then(argument("name", StringArgumentType.word()) + .suggests(this::suggestAllKits) + .then(literal("first-join") + .then(argument("enable", BoolArgumentType.bool()) + .executes(this::setFirstJoin) + )) + .then(literal("cooldown") + .then(argument("timespan", StringArgumentType.word()) + .suggests(TimeSpan::suggest) + .executes(this::setCooldown) + )) + .then(literal("one-time") + .then(argument("enable", BoolArgumentType.bool()) + .executes(this::setOneTime) + )) + .then(literal("icon") + .executes(this::setIcon)) + ) + ); + } + + private int listKits(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var kits = module.getPlayerKitNames(player); + + if(kits.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("listNoKits"), false); + return 0; + } + + var comma = module.locale().get("listComma"); + var items = Component.empty(); + for (var i = 0; i < kits.size(); i++) { + var kit = kits.get(i); + if (module.couldClaimKit(player, kit)) { + items.append(module.locale().get("listAvailableKit", Map.of( + "kit", Component.nullToEmpty(kit) + ))); + } else { + items.append(module.locale().get("listUnavailableKit", Map.of( + "kit", Component.nullToEmpty(kit) + ))); + } + + if (i < kits.size() - 1) { + items.append(comma); + } + } + + var list = module.locale().get("listHeader", Map.of( + "list", items + )); + + context.getSource().sendSuccess(() -> list, false); + + return 1; + } + + private int claimKit(CommandContext context) throws CommandSyntaxException { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var name = StringArgumentType.getString(context, "name"); + + if (!module.hasKitPermission(player, name)) { + source.sendSuccess(() -> module.locale().get("noPermission", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + source.sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kit = kits.get(name); + + var playerData = Solstice.playerData.get(player).getData(KitPlayerData.class); + if (kit.oneTime && playerData.claimedKits.containsKey(name)) { + source.sendSuccess(() -> module.locale().get("alreadyClaimed", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + if (kit.cooldownSeconds > 0) { + if (playerData.claimedKits.containsKey(name)) { + var startDate = playerData.claimedKits.get(name); + var nowDate = new Date(); + + var delta = (nowDate.getTime() - startDate.getTime()) / 1000; + if (delta < kit.cooldownSeconds) { + var remaining = kit.cooldownSeconds - delta; + + var timespan = TimeSpan.toShortString((int) remaining); + source.sendSuccess(() -> module.locale().get("onCooldown", Map.of( + "kit", Component.nullToEmpty(name), + "timespan", Component.nullToEmpty(timespan) + )), false); + return 0; + } + } + } + + module.claimKit(player, name); + + source.sendSuccess(() -> module.locale().get("claimed", Map.of("kit", Component.nullToEmpty(name))), false); + + return 1; + } + + private int createKit(CommandContext context) throws CommandSyntaxException { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var name = StringArgumentType.getString(context, "name"); + + if (module.getKits().containsKey(name)) { + source.sendSuccess(() -> module.locale().get("alreadyExists"), false); + return 0; + } + + var kitInventory = new KitInventory(); + var container = new SimpleGui(MenuType.GENERIC_9x3, player, false) { + @Override + public void onClose() { + if (module.createKit(name, Utils.getItemStacks(kitInventory))) { + source.sendSuccess(() -> module.locale().get("created", Map.of("kit", Component.nullToEmpty(name))), true); + } else { + source.sendSuccess(() -> module.locale().get("alreadyExists"), false); + } + } + }; + + Utils.redirect(container, kitInventory); + container.setTitle(module.locale().get("newKitTitle", Map.of("kit", Component.nullToEmpty(name)))); + container.open(); + + return 1; + } + + private int deleteKit(CommandContext context) { + var name = StringArgumentType.getString(context, "name"); + if (module.getKits().remove(name) != null) { + context.getSource().sendSuccess(() -> module.locale().get("deleted", Map.of("kit", Component.nullToEmpty(name))), true); + } else { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + } + + return 1; + } + + private int editKit(CommandContext context) throws CommandSyntaxException { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var name = StringArgumentType.getString(context, "name"); + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kit = kits.get(name); + var kitInventory = Utils.createInventory(kit.getItemStacks()); + + var container = new SimpleGui(MenuType.GENERIC_9x3, player, false) { + @Override + public void onClose() { + var items = Utils.getItemStacks(kitInventory); + kit.itemStacks = items.stream().map(Utils::serializeItemStack).toList(); + source.sendSuccess(() -> module.locale().get("edited", Map.of("kit", Component.nullToEmpty(name))), true); + } + }; + + Utils.redirect(container, kitInventory); + container.setTitle(module.locale().get("editKitTitle", Map.of("kit", Component.nullToEmpty(name)))); + container.open(); + + return 1; + } + + private int setFirstJoin(CommandContext context) { + var name = StringArgumentType.getString(context, "name"); + var enable = BoolArgumentType.getBool(context, "enable"); + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kit = kits.get(name); + kit.firstJoin = enable; + + context.getSource().sendSuccess(() -> module.locale().get("setFirstJoin", Map.of( + "kit", Component.nullToEmpty(name), + "value", Component.nullToEmpty(String.valueOf(enable)) + )), true); + + return 1; + } + + private int setCooldown(CommandContext context) throws CommandSyntaxException { + var name = StringArgumentType.getString(context, "name"); + var timespan = TimeSpan.getTimeSpan(context, "timespan"); + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kit = kits.get(name); + kit.cooldownSeconds = timespan; + + context.getSource().sendSuccess(() -> module.locale().get("setCooldown", Map.of( + "kit", Component.nullToEmpty(name), + "value", Component.nullToEmpty(TimeSpan.toShortString(timespan)) + )), true); + + return 1; + } + + private int setOneTime(CommandContext context) { + var name = StringArgumentType.getString(context, "name"); + var enable = BoolArgumentType.getBool(context, "enable"); + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var kit = kits.get(name); + kit.oneTime = enable; + + context.getSource().sendSuccess(() -> module.locale().get("setOneTime", Map.of( + "kit", Component.nullToEmpty(name), + "value", Component.nullToEmpty(String.valueOf(enable)) + )), true); + + return 1; + } + + private int setIcon(CommandContext context) throws CommandSyntaxException { + var name = StringArgumentType.getString(context, "name"); + var player = context.getSource().getPlayerOrException(); + + var kits = module.getKits(); + if (!kits.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get("notFound", Map.of("kit", Component.nullToEmpty(name))), false); + return 0; + } + + var hand = player.getUsedItemHand(); + var stack = player.getItemInHand(hand).copy(); + if(stack.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noStackInHand"), false); + return 0; + } + + var kit = kits.get(name); + kit.icon = Utils.serializeItemStack(stack); + + context.getSource().sendSuccess(() -> module.locale().get("setIcon", Map.of( + "kit", Component.nullToEmpty(name) + )), true); + + return 1; + } + + private CompletableFuture suggestAllKits(CommandContext context, SuggestionsBuilder builder) { + var kits = module.getKits().keySet(); + return SharedSuggestionProvider.suggest(kits, builder); + } + + private CompletableFuture suggestKitList(CommandContext context, SuggestionsBuilder builder) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + return SharedSuggestionProvider.suggest(module.getPlayerKitNames(player), builder); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitsCommand.java new file mode 100644 index 0000000..5edad7c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/commands/KitsCommand.java @@ -0,0 +1,86 @@ +package me.alexdevs.solstice.modules.kit.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.sgui.api.elements.GuiElement; +import eu.pb4.sgui.api.gui.SimpleGui; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.kit.KitModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.component.ItemLore; + +import java.util.LinkedHashMap; +import java.util.List; + +public class KitsCommand extends ModCommand { + public KitsCommand(KitModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("kits"); + } + + private static final LinkedHashMap> invSizes = new LinkedHashMap<>(); + static { + invSizes.put(9, MenuType.GENERIC_9x1); + invSizes.put(18, MenuType.GENERIC_9x2); + invSizes.put(27, MenuType.GENERIC_9x3); + invSizes.put(36, MenuType.GENERIC_9x4); + invSizes.put(45, MenuType.GENERIC_9x5); + invSizes.put(54, MenuType.GENERIC_9x6); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var playerAvailableKits = module.getPlayerKitNames(player); + var kits = module.getKits(); + + var size = playerAvailableKits.size(); + MenuType handlerType = null; + for (var entry : invSizes.entrySet()) { + handlerType = entry.getValue(); + if (size <= entry.getKey()) { + break; + } + } + + var gui = new SimpleGui(handlerType, player, false); + + for (var i = 0; i < Math.min(size, 54); i++) { + var kitName = playerAvailableKits.get(i); + var kit = kits.get(kitName); + + var icon = kit.getIcon(); + icon.set(DataComponents.CUSTOM_NAME, Component.nullToEmpty(kitName)); + icon.set(DataComponents.LORE, new ItemLore(List.of(module.locale().get("claimKit")))); + + gui.setSlot(0, new GuiElement(icon, (syncId, clickType, slotActionType) -> { + try { + dispatcher.execute("kit claim " + kitName, context.getSource()); + } catch (CommandSyntaxException e) { + context.getSource().sendFailure(Component.nullToEmpty(e.getLocalizedMessage())); + } + })); + } + + gui.setTitle(module.locale().get("kits")); + gui.open(); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/data/Kit.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/Kit.java new file mode 100644 index 0000000..a7a51fa --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/Kit.java @@ -0,0 +1,48 @@ +package me.alexdevs.solstice.modules.kit.data; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.kit.Utils; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class Kit { + /** + * itemStacks nbt is serialized and deserialized + */ + public List itemStacks = new ArrayList<>(); + public boolean oneTime = false; + public int cooldownSeconds = 0; + public boolean firstJoin = false; + public @Nullable String icon; + + public List getItemStacks() { + var stacks = new ArrayList(); + + for (var stackNbt : itemStacks) { + try { + stacks.add(Utils.deserializeItemStack(stackNbt)); + } catch (CommandSyntaxException e) { + Solstice.LOGGER.error("Could not load item from kit", e); + } + } + + return stacks; + } + + public ItemStack getIcon() { + var defaultStack = Items.DIRT.getDefaultInstance(); + if (icon == null) { + return defaultStack; + } + try { + return Utils.deserializeItemStack(icon); + } catch (CommandSyntaxException e) { + return defaultStack; + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitConfig.java new file mode 100644 index 0000000..449f298 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitConfig.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.kit.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class KitConfig { + @Comment("Require 'solstice.kit.kits.' permission node by default to claim a kit.") + public boolean requirePermission = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitLocale.java new file mode 100644 index 0000000..badf086 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitLocale.java @@ -0,0 +1,31 @@ +package me.alexdevs.solstice.modules.kit.data; + +import java.util.Map; + +public class KitLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("claimed", "Kit ${kit} claimed!"), + Map.entry("alreadyClaimed", "You already claimed this kit!"), + Map.entry("editKitTitle", "Edit kit: ${kit}"), + Map.entry("edited", "Kit ${kit} edited!"), + Map.entry("newKitTitle", "New kit: ${kit}"), + Map.entry("created", "New kit ${kit} created!"), + Map.entry("alreadyExists", "A kit with this name already exists!"), + Map.entry("deleted", "Kit ${kit} deleted!"), + Map.entry("notFound", "The kit ${kit} does not exist!"), + Map.entry("setFirstJoin", "Set first join to ${value} for kit ${kit}."), + Map.entry("setCooldown", "Set cooldown to ${value} for kit ${kit}."), + Map.entry("setOneTime", "Set one time to ${value} for kit ${kit}."), + Map.entry("onCooldown", "You can claim this kit again in ${timespan}!"), + Map.entry("noPermission", "You do not have permission to claim this kit!"), + Map.entry("listHeader", "Available kits: ${list}."), + Map.entry("listAvailableKit", "${kit}"), + Map.entry("listUnavailableKit", "${kit}"), + Map.entry("listComma", ", "), + Map.entry("listNoKits", "There are no kits available."), + Map.entry("kits", "Kits"), + Map.entry("claimKit", "Click to claim this kit!"), + Map.entry("noStackInHand", "You need to hold an item to set a kit icon!"), + Map.entry("setIcon", "Set icon for kit ${kit}.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitPlayerData.java new file mode 100644 index 0000000..11436bc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitPlayerData.java @@ -0,0 +1,8 @@ +package me.alexdevs.solstice.modules.kit.data; + +import java.util.Date; +import java.util.HashMap; + +public class KitPlayerData { + public HashMap claimedKits = new HashMap<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitServerData.java b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitServerData.java new file mode 100644 index 0000000..07e9a8b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/kit/data/KitServerData.java @@ -0,0 +1,8 @@ +package me.alexdevs.solstice.modules.kit.data; + +import java.util.HashMap; + +public class KitServerData { + public HashMap kits = new HashMap<>(); + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mail/MailModule.java b/common/src/main/java/me/alexdevs/solstice/modules/mail/MailModule.java new file mode 100644 index 0000000..8294631 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mail/MailModule.java @@ -0,0 +1,73 @@ +package me.alexdevs.solstice.modules.mail; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.PlayerMail; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.mail.commands.MailCommand; +import me.alexdevs.solstice.modules.mail.data.MailLocale; +import me.alexdevs.solstice.modules.mail.data.MailPlayerData; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class MailModule extends ModuleBase.Toggleable { + public static final String ID = "mail"; + + public MailModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, MailLocale.MODULE); + Solstice.playerData.registerData(ID, MailPlayerData.class, MailPlayerData::new); + + commands.add(new MailCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + var player = handler.getPlayer(); + var playerContext = PlaceholderContext.of(player); + + Solstice.scheduler.schedule(() -> { + if (!getMailData(player.getUUID()).mails.isEmpty()) { + player.sendSystemMessage(locale().get("mailPending", playerContext)); + } + }, 1, TimeUnit.SECONDS); + }); + } + + public boolean sendMail(UUID playerUuid, PlayerMail mail) { + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + var playerData = ignoreModule.getPlayerData(playerUuid); + if (playerData.ignoredPlayers.contains(mail.sender)) { + return false; + } + getMailData(playerUuid).mails.add(mail); + return true; + } + + public List getMailList(UUID playerUuid) { + return getMailData(playerUuid).mails.stream().toList(); + } + + public boolean deleteMail(UUID playerUuid, int index) { + var mailData = getMailData(playerUuid); + if (index < 0 || index >= mailData.mails.size()) { + return false; + } + mailData.mails.remove(index); + return true; + } + + public void clearAllMail(UUID playerUuid) { + getMailData(playerUuid).mails.clear(); + } + + public MailPlayerData getMailData(UUID playerUuid) { + return Solstice.playerData.get(playerUuid).getData(MailPlayerData.class); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mail/commands/MailCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/mail/commands/MailCommand.java new file mode 100644 index 0000000..52cb6fc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mail/commands/MailCommand.java @@ -0,0 +1,189 @@ +package me.alexdevs.solstice.modules.mail.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.PlayerMail; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.api.text.parser.MarkdownParser; +import me.alexdevs.solstice.core.coreModule.CoreModule; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.mail.MailModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class MailCommand extends ModCommand { + public MailCommand(MailModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("mail"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(this::listMails) + .then(literal("send") + .then(argument("recipient", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .then(argument("message", StringArgumentType.greedyString()) + .executes(this::sendMail) + ) + ) + ) + .then(literal("read") + .then(argument("index", IntegerArgumentType.integer(0)) + .executes(this::readMail))) + .then(literal("delete") + .then(argument("index", IntegerArgumentType.integer(0)) + .executes(this::deleteMail))); + } + + private int listMails(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var playerContext = PlaceholderContext.of(player); + var mails = module.getMailList(player.getUUID()); + + if (mails.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyMailbox", playerContext), false); + return 0; + } + + var output = Component.empty() + .append(module.locale().get("mailListHeader", playerContext)) + .append(Component.nullToEmpty("\n")); + + for (var i = 0; i < mails.size(); i++) { + if (i > 0) + output = output.append(Component.nullToEmpty("\n")); + + var mail = mails.get(i); + var index = i + 1; + + var readButton = Components.button( + module.locale().raw("readButton"), + module.locale().raw("hoverRead"), + "/mail read " + index + ); + + var senderName = CoreModule.getUsername(mail.sender); + var dateFormatter = new SimpleDateFormat(CoreModule.getConfig().dateTimeFormat); + var placeholders = Map.of( + "index", Component.nullToEmpty(String.valueOf(index)), + "sender", Component.nullToEmpty(senderName), + "date", Component.nullToEmpty(dateFormatter.format(mail.date)), + "readButton", readButton + ); + output = output.append(module.locale().get("mailListEntry", playerContext, placeholders)); + } + + final var finalOutput = output; + + context.getSource().sendSuccess(() -> finalOutput, false); + + return 1; + } + + private int readMail(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var playerContext = PlaceholderContext.of(player); + var mails = module.getMailList(player.getUUID()); + var index = IntegerArgumentType.getInteger(context, "index") - 1; + + if (index < 0 || index >= mails.size()) { + context.getSource().sendSuccess(() -> module.locale().get("notFound"), false); + return 0; + } + + var mail = mails.get(index); + + var username = CoreModule.getUsername(mail.sender); + + var replyButton = Components.buttonSuggest( + module.locale().raw("replyButton"), + module.locale().raw("hoverReply"), + "/mail send " + username + " " + ); + var deleteButton = Components.button( + module.locale().raw("deleteButton"), + module.locale().raw("hoverDelete"), + "/mail delete " + index + 1 + ); + + var senderName = CoreModule.getUsername(mail.sender); + var dateFormatter = new SimpleDateFormat(CoreModule.getConfig().dateTimeFormat); + var message = MarkdownParser.defaultParser.parseNode(mail.message); + var placeholders = Map.of( + "sender", Component.nullToEmpty(senderName), + "date", Component.nullToEmpty(dateFormatter.format(mail.date)), + "message", message.toText(), + "replyButton", replyButton, + "deleteButton", deleteButton + ); + + context.getSource().sendSuccess(() -> module.locale().get("mailDetails", playerContext, placeholders), false); + + return 1; + } + + private int deleteMail(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var playerContext = PlaceholderContext.of(player); + var index = IntegerArgumentType.getInteger(context, "index") - 1; + + if (module.deleteMail(player.getUUID(), index)) { + context.getSource().sendSuccess(() -> module.locale().get("mailDeleted", playerContext), false); + } else { + context.getSource().sendSuccess(() -> module.locale().get("notFound"), false); + } + + return 1; + } + + private int sendMail(CommandContext context) throws CommandSyntaxException { + var sender = context.getSource().getPlayerOrException(); + var recipient = LocalGameProfile.getProfile(context, "recipient"); + + var message = StringArgumentType.getString(context, "message"); + var server = context.getSource().getServer(); + + var mail = new PlayerMail(message, sender.getUUID()); + var actuallySent = module.sendMail(recipient.getId(), mail); + + var senderContext = PlaceholderContext.of(sender); + + context.getSource().sendSuccess(() -> module.locale().get("mailSent", senderContext), false); + + if (actuallySent) { + var recPlayer = server.getPlayerList().getPlayer(recipient.getId()); + if (recPlayer == null) { + return 1; + } + + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + if (ignoreModule.isIgnoring(recPlayer, sender)) + return 0; + + var recContext = PlaceholderContext.of(recPlayer); + recPlayer.sendSystemMessage(module.locale().get("mailReceived", recContext)); + } + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailLocale.java new file mode 100644 index 0000000..ea522df --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailLocale.java @@ -0,0 +1,24 @@ +package me.alexdevs.solstice.modules.mail.data; + +import java.util.Map; + +public class MailLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("mailPending", "You have pending mails! Run /mail to read your mails."), + Map.entry("replyButton", "Reply"), + Map.entry("deleteButton", "Delete"), + Map.entry("readButton", "Read"), + Map.entry("hoverReply", "Click to reply to the mail"), + Map.entry("hoverDelete", "Click to delete the mail"), + Map.entry("hoverRead", "Click to read the mail"), + Map.entry("playerNotFound", "Player ${recipient} not found!"), + Map.entry("mailSent", "Mail sent!"), + Map.entry("mailReceived", "You received a new mail! Run /mail to read the emails!"), + Map.entry("mailDetails", "From ${sender} on ${date}\n ${message}\n\n ${replyButton} ${deleteButton}"), + Map.entry("mailListHeader", "Your mails:"), + Map.entry("mailListEntry", "${index}. From ${sender} on ${date} ${readButton}"), + Map.entry("notFound", "Mail not found"), + Map.entry("mailDeleted", "Mail deleted!"), + Map.entry("emptyMailbox", "Your mailbox is empty.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailPlayerData.java new file mode 100644 index 0000000..80ed363 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mail/data/MailPlayerData.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.mail.data; + +import me.alexdevs.solstice.api.PlayerMail; + +import java.util.ArrayList; + +public class MailPlayerData { + public ArrayList mails = new ArrayList<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/DummyExplosion.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/DummyExplosion.java new file mode 100644 index 0000000..bc595f9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/DummyExplosion.java @@ -0,0 +1,22 @@ +package me.alexdevs.solstice.modules.miscellaneous; + +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.particles.SimpleParticleType; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.phys.Vec3; + +public class DummyExplosion { + public static void spawn(ServerLevel world, Vec3 pos, float power) { + world.playSound(null, pos.x, pos.y, pos.z, SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS, 4.0F, (1.0F + (world.random.nextFloat() - world.random.nextFloat()) * 0.2F) * 0.7F); + SimpleParticleType particle; + if(power >= 2.0) { + particle = ParticleTypes.EXPLOSION_EMITTER; + } else { + particle = ParticleTypes.EXPLOSION; + } + world.sendParticles(particle, pos.x, pos.y, pos.z, 1, 1, 0, 0, 1); + + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/MiscellaneousModule.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/MiscellaneousModule.java new file mode 100644 index 0000000..4b651d9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/MiscellaneousModule.java @@ -0,0 +1,74 @@ +package me.alexdevs.solstice.modules.miscellaneous; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.miscellaneous.commands.*; +import me.alexdevs.solstice.modules.miscellaneous.data.MiscellaneousLocale; +import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class MiscellaneousModule extends ModuleBase.Toggleable { + public static final String ID = "miscellaneous"; + + private final Map commandSleeping = new ConcurrentHashMap<>(); + + public MiscellaneousModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, MiscellaneousLocale.MODULE); + + commands.add(new EffectsCommand(this)); + commands.add(new SleepCommand(this)); + commands.add(new NudgeCommand(this)); + commands.add(new TopCommand(this)); + //commands.add(new KittyCannonCommand(this)); + //commands.add(new RocketCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> commandSleeping.remove(handler.getPlayer().getUUID())); + EntitySleepEvents.STOP_SLEEPING.register((entity, pos) -> commandSleeping.remove(entity.getUUID())); + + EntitySleepEvents.ALLOW_SLEEP_TIME.register((player, pos, vanillaResult) -> { + if (commandSleeping.getOrDefault(player.getUUID(), false)) { + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + }); + + EntitySleepEvents.ALLOW_RESETTING_TIME.register(player -> { + if (commandSleeping.getOrDefault(player.getUUID(), false)) { + return !player.level().isDay(); + } + + return true; + }); + } + + public boolean isCommandSleep(LivingEntity entity) { + return commandSleeping.getOrDefault(entity.getUUID(), false); + } + + /** + * Make the entity sleep regardless of the bed check. + *

+ * No, this does not euthanize the entity. + * + * @param entity The entity to make sleep + */ + public void putToSleep(LivingEntity entity) { + commandSleeping.put(entity.getUUID(), true); + entity.startSleeping(entity.blockPosition()); + if (entity instanceof ServerPlayer player) { + player.serverLevel().updateSleepingPlayerList(); + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/EffectsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/EffectsCommand.java new file mode 100644 index 0000000..f9bbfa6 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/EffectsCommand.java @@ -0,0 +1,73 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.util.List; +import java.util.Map; + +public class EffectsCommand extends ModCommand { + public EffectsCommand(MiscellaneousModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("effects"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("effects.base", 1)) + .executes(context -> execute(context, context.getSource().getPlayerOrException())) + .then(Commands.argument("player", EntityArgument.player()) + .requires(require("effects.others", 2)) + .executes(context -> execute(context, EntityArgument.getPlayer(context, "player"))) + ); + } + + private int execute(CommandContext context, ServerPlayer target) { + var effects = target.getActiveEffectsMap(); + if (effects.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("noEffects"), false); + return 0; + } + + var text = Component.empty(); + text.append(module.locale().get("effectHeader")); + + for (var entry : effects.entrySet()) { + text.append("\n"); + + var effect = entry.getKey(); + var instance = entry.getValue(); + + String duration; + if (instance.isInfiniteDuration()) { + duration = module.locale().raw("infinite"); + } else { + duration = TimeSpan.toShortString(instance.getDuration() / 20); + } + + var map = Map.of( + "effect", Component.translatable(effect.value().getDescriptionId()), + "amplifier", Component.nullToEmpty(String.valueOf(instance.getAmplifier())), + "duration", Component.nullToEmpty(duration) + ); + text.append(module.locale().get("effect", map)); + } + + context.getSource().sendSuccess(() -> text, false); + + return effects.size(); + } + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/KittyCannonCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/KittyCannonCommand.java new file mode 100644 index 0000000..c213515 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/KittyCannonCommand.java @@ -0,0 +1,53 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.DummyExplosion; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobSpawnType; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class KittyCannonCommand extends ModCommand { + public KittyCannonCommand(MiscellaneousModule module) { + super(module); + } + + public static final EntityType BALL = EntityType.CAT; + + @Override + public List getNames() { + return List.of("kittycannon"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("kittycannon.base", 2)) + .executes(context -> { + final var player = context.getSource().getPlayerOrException(); + + final var world = player.serverLevel(); + + BALL.create(world, entity -> { + entity.setDeltaMovement(player.getLookAngle().scale(3.5)); + entity.setPos(player.getEyePosition().add(player.getLookAngle())); + world.addFreshEntity(entity); + + Solstice.scheduler.scheduleSync(() -> { + final var pos = entity.position(); + DummyExplosion.spawn(world, pos, 0); + entity.remove(Entity.RemovalReason.DISCARDED); + }, 1, TimeUnit.SECONDS); + }, player.blockPosition().above(), MobSpawnType.COMMAND, true, false); + + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/NudgeCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/NudgeCommand.java new file mode 100644 index 0000000..2b5c2a0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/NudgeCommand.java @@ -0,0 +1,69 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import java.util.List; + +public class NudgeCommand extends ModCommand { + public NudgeCommand(MiscellaneousModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("nudge"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("nudge.base", 2)) + .then(Commands.argument("entities", EntityArgument.entities()) + .executes(context -> this.execute(context, 1, false)) + .then(Commands.argument("power", FloatArgumentType.floatArg(0, 32)) + .executes(context -> this.execute(context, FloatArgumentType.getFloat(context, "power"), false)) + .then(Commands.argument("quiet", BoolArgumentType.bool()) + .executes(context -> this.execute(context, FloatArgumentType.getFloat(context, "power"), BoolArgumentType.getBool(context, "quiet"))) + ) + )); + } + + private int execute(CommandContext context, float power, boolean quiet) throws CommandSyntaxException { + var entities = EntityArgument.getEntities(context, "entities"); + + var random = context.getSource().getServer().overworld().getRandom(); + + for (var entity : entities) { + if (entity instanceof LivingEntity living) { + var angle = random.nextDouble() * Math.PI * 2; + var x = Math.sin(angle); + var z = Math.cos(angle); + var vec = new Vec3(x, 0, z).normalize().scale(power); + + living.setDeltaMovement(vec); + living.hurtMarked = true; + if (!quiet) { + if (entity instanceof ServerPlayer player) { + var pitch = (float) (Math.sin(random.nextDouble() * Math.PI * 2) / 10 + 1); + player.playNotifySound(SoundEvents.PLAYER_ATTACK_NODAMAGE, SoundSource.MASTER, 1f, pitch); + } + } + } + } + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/RocketCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/RocketCommand.java new file mode 100644 index 0000000..3063e2c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/RocketCommand.java @@ -0,0 +1,76 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.command.Flags; +import me.alexdevs.solstice.api.command.flags.Flag; +import me.alexdevs.solstice.api.command.flags.FloatFlag; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.DummyExplosion; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import java.util.List; + +public class RocketCommand extends ModCommand { + public RocketCommand(MiscellaneousModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("rocket"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("rocket.base", 2)) + .then(Commands.argument("targets", EntityArgument.entities()) + .executes(context -> execute(context, "")) + .then(Commands.argument("flags", StringArgumentType.greedyString()) + .executes(context -> execute(context, StringArgumentType.getString(context, "flags"))) + ) + ); + } + + private int execute(CommandContext context, String flags) throws CommandSyntaxException { + var targets = EntityArgument.getEntities(context, "targets"); + + var explodeFlag = new Flag("explode", List.of('e')); + var powerFlag = new FloatFlag("power", List.of('p')); + Flags.parse(flags, explodeFlag, powerFlag); + + var explode = explodeFlag.isUsed(); + var power = 2.0f; + if (powerFlag.isUsed()) { + power = powerFlag.getValue(); + } + + var count = 0; + for (var target : targets) { + count++; + if (explode) { + var world = (ServerLevel) target.level(); + var pos = target.position(); + DummyExplosion.spawn(world, pos, power * 2); + world.playSound(null, + pos.x, pos.y, pos.z, + SoundEvents.FIREWORK_ROCKET_LAUNCH, SoundSource.MASTER, + 2, 1); + } + + target.push(0, power, 0); + target.hurtMarked = true; + } + + + return count; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/SleepCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/SleepCommand.java new file mode 100644 index 0000000..afae492 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/SleepCommand.java @@ -0,0 +1,46 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.world.entity.LivingEntity; +import java.util.List; + +public class SleepCommand extends ModCommand { + public SleepCommand(MiscellaneousModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("sleep"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("sleep.base", 1)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + module.putToSleep(player); + return 1; + }) + .then(Commands.argument("entities", EntityArgument.entities()) + .requires(require("sleep.others", 2)) + .executes(context -> { + var targets = EntityArgument.getEntities(context, "entities"); + var count = 0; + for (var target : targets) { + if (target instanceof LivingEntity entity) { + module.putToSleep(entity); + count++; + } + } + return count; + }) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/TopCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/TopCommand.java new file mode 100644 index 0000000..9d160e6 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/commands/TopCommand.java @@ -0,0 +1,40 @@ +package me.alexdevs.solstice.modules.miscellaneous.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.miscellaneous.MiscellaneousModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.world.level.levelgen.Heightmap; +import java.util.List; + +public class TopCommand extends ModCommand { + public TopCommand(MiscellaneousModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("top"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require("top.base", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var world = player.serverLevel(); + var top = world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, player.blockPosition()); + var pos = top.getCenter(); + + player.teleportTo(pos.x(), pos.y(), pos.z()); + + player.setDeltaMovement(player.getDeltaMovement().multiply(1.0, 0.0, 1.0)); + player.setOnGround(true); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/data/MiscellaneousLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/data/MiscellaneousLocale.java new file mode 100644 index 0000000..c89dbfe --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/miscellaneous/data/MiscellaneousLocale.java @@ -0,0 +1,12 @@ +package me.alexdevs.solstice.modules.miscellaneous.data; + +import java.util.Map; + +public class MiscellaneousLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("noEffects", "This player has no active effects."), + Map.entry("effectHeader", "Active effects:"), + Map.entry("effect", "${effect}: x${amplifier} for ${duration}"), + Map.entry("infinite", "infinite") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mute/MuteModule.java b/common/src/main/java/me/alexdevs/solstice/modules/mute/MuteModule.java new file mode 100644 index 0000000..40a412a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mute/MuteModule.java @@ -0,0 +1,46 @@ +package me.alexdevs.solstice.modules.mute; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.mute.commands.MuteCommand; +import me.alexdevs.solstice.modules.mute.commands.UnmuteCommand; +import me.alexdevs.solstice.modules.mute.data.MuteLocale; +import me.alexdevs.solstice.modules.mute.data.MutePlayerData; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; + +import java.util.UUID; + +public class MuteModule extends ModuleBase.Toggleable { + public static final String ID = "mute"; + + public MuteModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, MuteLocale.MODULE); + Solstice.playerData.registerData(ID, MutePlayerData.class, MutePlayerData::new); + + commands.add(new MuteCommand(this)); + commands.add(new UnmuteCommand(this)); + + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((signedMessage, player, parameters) -> { + if (isMuted(player.getUUID())) { + player.sendSystemMessage(locale().get("youAreMuted")); + return false; + } + return true; + }); + } + + public MutePlayerData getPlayerData(UUID playerUuid) { + return Solstice.playerData.get(playerUuid).getData(MutePlayerData.class); + } + + public boolean isMuted(UUID playerUuid) { + return getPlayerData(playerUuid).muted; + } + + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/MuteCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/MuteCommand.java new file mode 100644 index 0000000..5d9636f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/MuteCommand.java @@ -0,0 +1,50 @@ +package me.alexdevs.solstice.modules.mute.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.mute.MuteModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.network.chat.Component; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class MuteCommand extends ModCommand { + public MuteCommand(MuteModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("mute"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("targets", GameProfileArgument.gameProfile()) + .executes(context -> { + var targets = GameProfileArgument.getGameProfiles(context, "targets"); + + var names = targets.stream().map(GameProfile::getName).toArray(String[]::new); + + var muteModule = Solstice.modules.getModule(MuteModule.class); + + targets.forEach(profile -> { + var playerData = muteModule.getPlayerData(profile.getId()); + playerData.muted = true; + }); + + Solstice.playerData.saveAll(); + + context.getSource().sendSuccess(() -> Component.literal("Muted " + String.join(", ", names)), true); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/UnmuteCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/UnmuteCommand.java new file mode 100644 index 0000000..3d10660 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mute/commands/UnmuteCommand.java @@ -0,0 +1,48 @@ +package me.alexdevs.solstice.modules.mute.commands; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.mute.MuteModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.network.chat.Component; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class UnmuteCommand extends ModCommand { + public UnmuteCommand(MuteModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("unmute"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("targets", GameProfileArgument.gameProfile()) + .executes(context -> { + var targets = GameProfileArgument.getGameProfiles(context, "targets"); + + var names = targets.stream().map(GameProfile::getName).toArray(String[]::new); + + targets.forEach(profile -> { + var playerData = module.getPlayerData(profile.getId()); + playerData.muted = false; + }); + + Solstice.playerData.saveAll(); + + context.getSource().sendSuccess(() -> Component.literal("Unmuted " + String.join(", ", names)), true); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MuteLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MuteLocale.java new file mode 100644 index 0000000..56cf982 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MuteLocale.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.mute.data; + +import java.util.Map; + +public class MuteLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("youAreMuted", "You are muted!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MutePlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MutePlayerData.java new file mode 100644 index 0000000..67f2487 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/mute/data/MutePlayerData.java @@ -0,0 +1,5 @@ +package me.alexdevs.solstice.modules.mute.data; + +public class MutePlayerData { + public boolean muted = false; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/near/NearModule.java b/common/src/main/java/me/alexdevs/solstice/modules/near/NearModule.java new file mode 100644 index 0000000..eddb709 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/near/NearModule.java @@ -0,0 +1,23 @@ +package me.alexdevs.solstice.modules.near; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.near.commands.NearCommand; +import me.alexdevs.solstice.modules.near.data.NearConfig; +import me.alexdevs.solstice.modules.near.data.NearLocale; + +public class NearModule extends ModuleBase.Toggleable { + public static final String ID = "near"; + + public NearModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, NearConfig.class, NearConfig::new); + Solstice.localeManager.registerModule(ID, NearLocale.MODULE); + + commands.add(new NearCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/near/commands/NearCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/near/commands/NearCommand.java new file mode 100644 index 0000000..80a5d9a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/near/commands/NearCommand.java @@ -0,0 +1,99 @@ +package me.alexdevs.solstice.modules.near.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.near.NearModule; +import me.alexdevs.solstice.modules.near.data.NearConfig; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class NearCommand extends ModCommand { + public NearCommand(NearModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("near"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> execute(context, Solstice.configManager.getData(NearConfig.class).defaultRange, context.getSource().getPlayerOrException())) + .then(argument("range", IntegerArgumentType.integer(0, Solstice.configManager.getData(NearConfig.class).maxRange)) + .executes(context -> execute(context, IntegerArgumentType.getInteger(context, "range"), context.getSource().getPlayerOrException()))); + } + + private int execute(CommandContext context, int range, ServerPlayer sourcePlayer) { + var playerContext = PlaceholderContext.of(sourcePlayer); + var list = new ArrayList(); + + var sourcePos = sourcePlayer.position(); + sourcePlayer.serverLevel().players().forEach(targetPlayer -> { + var targetPos = targetPlayer.position(); + if (!sourcePlayer.getUUID().equals(targetPlayer.getUUID()) && sourcePos.closerThan(targetPos, range)) { + var distance = sourcePos.distanceTo(targetPos); + list.add(new ClosePlayers(targetPlayer, distance)); + } + }); + + if (list.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get( + "noOne", + playerContext + ), false); + return 1; + } + + list.sort(Comparator.comparingDouble(ClosePlayers::distance)); + + var listText = Component.empty(); + var comma = module.locale().get("comma"); + for (int i = 0; i < list.size(); i++) { + var player = list.get(i); + if (i > 0) { + listText = listText.append(comma); + } + var placeholders = Map.of( + "player", player.player.getDisplayName(), + "distance", Component.nullToEmpty(String.format("%.1fm", player.distance)) + ); + + var targetContext = PlaceholderContext.of(sourcePlayer); + + listText = listText.append(module.locale().get( + "format", + targetContext, + placeholders + )); + } + + var placeholders = Map.of( + "playerList", (Component) listText + ); + context.getSource().sendSuccess(() -> module.locale().get( + "nearestPlayers", + playerContext, + placeholders + ), false); + + return 1; + } + + private record ClosePlayers(ServerPlayer player, double distance) { + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearConfig.java new file mode 100644 index 0000000..4771a18 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearConfig.java @@ -0,0 +1,13 @@ +package me.alexdevs.solstice.modules.near.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class NearConfig { + @Comment("Max range in blocks. Defaults to 48 blocks.") + public int maxRange = 48; + + @Comment("Default range in blocks. Defaults to 32 blocks.") + public int defaultRange = 32; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearLocale.java new file mode 100644 index 0000000..fcbe0b0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/near/data/NearLocale.java @@ -0,0 +1,12 @@ +package me.alexdevs.solstice.modules.near.data; + +import java.util.Map; + +public class NearLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("noOne", "There are no players near you."), + Map.entry("nearestPlayers", "Nearest players: ${playerList}"), + Map.entry("format", "${player} (${distance})"), + Map.entry("comma", ", ") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/NoteModule.java b/common/src/main/java/me/alexdevs/solstice/modules/note/NoteModule.java new file mode 100644 index 0000000..4521a78 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/NoteModule.java @@ -0,0 +1,75 @@ +package me.alexdevs.solstice.modules.note; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.modules.note.commands.NotesCommand; +import me.alexdevs.solstice.modules.note.data.Note; +import me.alexdevs.solstice.modules.note.data.NoteConfig; +import me.alexdevs.solstice.modules.note.data.NoteLocale; +import me.alexdevs.solstice.modules.note.data.NotePlayerData; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class NoteModule extends ModuleBase.Toggleable { + public static final String ID = "note"; + + public NoteModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, NoteConfig.class, NoteConfig::new); + Solstice.localeManager.registerModule(ID, NoteLocale.MODULE); + Solstice.playerData.registerData(ID, NotePlayerData.class, NotePlayerData::new); + + commands.add(new NotesCommand(this)); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + var config = Solstice.configManager.getData(NoteConfig.class); + if (!config.showLogin) + return; + + var player = handler.getPlayer(); + var notes = getNotes(player.getUUID()); + + if (notes.isEmpty()) + return; + + var context = PlaceholderContext.of(player); + + var checkButton = Components.button( + locale().raw("checkButton"), + locale().raw("hoverCheck"), + "/notes " + player.getGameProfile().getName() + ); + final var text = locale().get("loginInfo", context, Map.of( + "user", Component.nullToEmpty(player.getGameProfile().getName()), + "notes", Component.nullToEmpty(String.valueOf(notes.size())), + "checkButton", checkButton + )); + + Solstice.nextTick(() -> + server.getPlayerList().getPlayers().forEach(pl -> { + if (Permissions.check(pl, getPermissionNode("showonlogin"), 2)) { + pl.sendSystemMessage(text); + } + })); + }); + } + + public NotePlayerData getData(UUID uuid) { + return Solstice.playerData.get(uuid).getData(NotePlayerData.class); + } + + public List getNotes(UUID uuid) { + var data = getData(uuid); + return data.notes; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/commands/NotesCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/note/commands/NotesCommand.java new file mode 100644 index 0000000..9877157 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/commands/NotesCommand.java @@ -0,0 +1,205 @@ +package me.alexdevs.solstice.modules.note.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.core.coreModule.CoreModule; +import me.alexdevs.solstice.modules.note.NoteModule; +import me.alexdevs.solstice.modules.note.data.Note; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class NotesCommand extends ModCommand { + public NotesCommand(NoteModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("notes", "note"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("user", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(this::listNotes) + .then(literal("add") + .requires(require("add", 2)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(this::addNote) + ) + ) + .then(literal("check") + .then(argument("index", IntegerArgumentType.integer(0)) + .executes(this::checkNote))) + .then(literal("delete") + .requires(require("delete", 2)) + .then(argument("index", IntegerArgumentType.integer(0)) + .executes(this::deleteNote))) + .then(literal("clear") + .requires(require("clear", 2)) + .executes(this::clearNotes)) + ); + } + + private int listNotes(CommandContext context) throws CommandSyntaxException { + var user = LocalGameProfile.getProfile(context, "user"); + var notes = module.getNotes(user.getId()); + + if (notes.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyNotes"), false); + return 0; + } + + var output = Component.empty() + .append(module.locale().get("noteListHeader", Map.of( + "user", Component.nullToEmpty(user.getName()) + ))) + .append(Component.nullToEmpty("\n")); + + for (var i = 0; i < notes.size(); i++) { + if (i > 0) + output = output.append(Component.nullToEmpty("\n")); + + var note = notes.get(i); + + var checkButton = Components.button( + module.locale().raw("checkButton"), + module.locale().raw("hoverCheck"), + "/notes " + user.getName() + " check " + i + ); + + var senderName = CoreModule.getUsername(note.createdBy); + var dateFormatter = new SimpleDateFormat(CoreModule.getConfig().dateTimeFormat); + var placeholders = Map.of( + "index", Component.nullToEmpty(String.valueOf(i)), + "operator", Component.nullToEmpty(senderName), + "date", Component.nullToEmpty(dateFormatter.format(note.creationDate)), + "message", Format.parse(note.note), + "checkButton", checkButton + ); + output = output.append(module.locale().get("noteListEntry", placeholders)); + } + + final var finalOutput = output; + + context.getSource().sendSuccess(() -> finalOutput, false); + + return 1; + } + + private int checkNote(CommandContext context) throws CommandSyntaxException { + var user = LocalGameProfile.getProfile(context, "user"); + var notes = module.getNotes(user.getId()); + var index = IntegerArgumentType.getInteger(context, "index"); + + if (index < 0 || index >= notes.size()) { + context.getSource().sendSuccess(() -> module.locale().get("notFound"), false); + return 0; + } + + var note = notes.get(index); + + var deleteButton = Components.button( + module.locale().raw("deleteButton"), + module.locale().raw("hoverDelete"), + "/note " + user.getName() + " delete " + index + ); + + var operator = CoreModule.getUsername(note.createdBy); + var dateFormatter = new SimpleDateFormat(CoreModule.getConfig().dateTimeFormat); + var placeholders = Map.of( + "operator", Component.nullToEmpty(operator), + "date", Component.nullToEmpty(dateFormatter.format(note.creationDate)), + "message", Format.parse(note.note), + "deleteButton", deleteButton + ); + + context.getSource().sendSuccess(() -> module.locale().get("noteDetails", placeholders), false); + + return 1; + } + + private int deleteNote(CommandContext context) throws CommandSyntaxException { + var user = LocalGameProfile.getProfile(context, "user"); + var notes = module.getNotes(user.getId()); + var index = IntegerArgumentType.getInteger(context, "index"); + + if (index < notes.size()) { + notes.remove(index); + context.getSource().sendSuccess(() -> module.locale().get("noteDeleted"), false); + } else { + context.getSource().sendSuccess(() -> module.locale().get("notFound"), false); + return 0; + } + + return 1; + } + + private int addNote(CommandContext context) throws CommandSyntaxException { + var user = LocalGameProfile.getProfile(context, "user"); + + UUID operatorId = new UUID(0, 0); + if (context.getSource().isPlayer()) + operatorId = context.getSource().getPlayer().getUUID(); + + var message = StringArgumentType.getString(context, "message"); + + var note = new Note(message, operatorId); + var notes = module.getNotes(user.getId()); + + notes.add(note); + var index = notes.size() - 1; + + context.getSource().sendSuccess(() -> module.locale().get("noteAdded"), false); + + var checkButton = Components.button( + module.locale().raw("checkButton"), + module.locale().raw("hoverCheck"), + "/notes " + user.getName() + " check " + index + ); + final var text = module.locale().get("addedNotification", Map.of( + "operator", context.getSource().getDisplayName(), + "user", Component.nullToEmpty(user.getName()), + "checkButton", checkButton + )); + + context.getSource().getServer().getPlayerList().getPlayers().forEach(pl -> { + if (Permissions.check(pl, getPermissionNode("notify"), 2)) { + pl.sendSystemMessage(text); + } + }); + + return 1; + } + + private int clearNotes(CommandContext context) throws CommandSyntaxException { + var user = LocalGameProfile.getProfile(context, "user"); + + var notes = module.getNotes(user.getId()); + notes.clear(); + + context.getSource().sendSuccess(() -> module.locale().get("notesCleared", Map.of( + "user", Component.nullToEmpty(user.getName()) + )), true); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/data/Note.java b/common/src/main/java/me/alexdevs/solstice/modules/note/data/Note.java new file mode 100644 index 0000000..751a7a0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/data/Note.java @@ -0,0 +1,16 @@ +package me.alexdevs.solstice.modules.note.data; + +import java.util.Date; +import java.util.UUID; +import net.minecraft.server.level.ServerPlayer; + +public class Note { + public UUID createdBy; + public Date creationDate = new Date(); + public String note; + + public Note(String note, UUID createdBy) { + this.note = note; + this.createdBy = createdBy; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteConfig.java new file mode 100644 index 0000000..8899e49 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteConfig.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.note.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class NoteConfig { + @Comment("Show a player's note when they login to users with the permission 'solstice.note.showonlogin'") + public boolean showLogin = true; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteLocale.java new file mode 100644 index 0000000..fcafafd --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NoteLocale.java @@ -0,0 +1,23 @@ +package me.alexdevs.solstice.modules.note.data; + +import java.util.Map; + +public class NoteLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("deleteButton", "Delete"), + Map.entry("checkButton", "Check"), + Map.entry("hoverDelete", "Click to delete the note"), + Map.entry("hoverCheck", "Click to check the note"), + Map.entry("userNotFound", "User ${user} not found!"), + Map.entry("noteAdded", "Note added!"), + Map.entry("noteDetails", "From ${operator} on ${date}\n ${message}\n\n ${deleteButton}"), + Map.entry("noteListHeader", "${user}'s notes:"), + Map.entry("noteListEntry", "[${date}] ${operator}: ${message} ${checkButton}"), + Map.entry("notFound", "Note not found"), + Map.entry("noteDeleted", "Note deleted!"), + Map.entry("emptyNotes", "There are no notes for this user."), + Map.entry("notesCleared", "All ${user}'s notes cleared!"), + Map.entry("loginInfo", "${user} has ${notes} notes! ${checkButton}"), + Map.entry("addedNotification", "${operator} added a note to ${user}! ${checkButton}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/note/data/NotePlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NotePlayerData.java new file mode 100644 index 0000000..8f2d181 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/note/data/NotePlayerData.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.note.data; + +import java.util.ArrayList; +import java.util.List; + +public class NotePlayerData { + public List notes = new ArrayList<>(); + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/NotificationsModule.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/NotificationsModule.java new file mode 100644 index 0000000..1e89593 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/NotificationsModule.java @@ -0,0 +1,110 @@ +package me.alexdevs.solstice.modules.notifications; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.afk.AfkModule; +import me.alexdevs.solstice.modules.notifications.commands.NotificationsCommand; +import me.alexdevs.solstice.modules.notifications.data.NotificationsConfig; +import me.alexdevs.solstice.modules.notifications.data.NotificationsLocale; +import me.alexdevs.solstice.modules.notifications.data.NotificationsPlayerData; +import me.alexdevs.solstice.modules.notifications.data.PlayerNotificationSettings; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; + +public class NotificationsModule extends ModuleBase.Toggleable { + public static final String ID = "notifications"; + + public NotificationsModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, NotificationsConfig.class, NotificationsConfig::new); + Solstice.localeManager.registerModule(ID, NotificationsLocale.MODULE); + Solstice.playerData.registerData(ID, NotificationsPlayerData.class, NotificationsPlayerData::new); + + commands.add(new NotificationsCommand(this)); + + ServerMessageEvents.CHAT_MESSAGE.register((message, sender, parameters) -> { + var content = message.decoratedContent().getString().toLowerCase(); + + sender.getServer().getPlayerList().getPlayers().forEach(player -> { + if(player.equals(sender)) { + return; + } + + var playerName = player.getGameProfile().getName().toLowerCase(); + if (content.contains(playerName)) { + var settings = getPlayerSettings(player); + if (settings.onChat()) { + notifyPlayer(player); + } + } + }); + }); + } + + public static void notify(ServerPlayer player) { + var module = Solstice.modules.getModule(NotificationsModule.class); + if (!module.isEnabled()) + return; + + module.notifyPlayer(player); + } + + public NotificationsConfig getConfig() { + return Solstice.configManager.getData(NotificationsConfig.class); + } + + public NotificationsPlayerData getPlayerData(ServerPlayer player) { + return Solstice.playerData.get(player).getData(NotificationsPlayerData.class); + } + + public PlayerNotificationSettings getPlayerSettings(ServerPlayer player) { + var data = getPlayerData(player); + var config = getConfig(); + + return new PlayerNotificationSettings( + data.soundId != null ? data.soundId : config.defaultValues.soundId, + data.pitch != null ? data.pitch : config.defaultValues.pitch, + data.volume != null ? data.volume : config.defaultValues.volume, + data.afkOnly != null ? data.afkOnly : config.defaultValues.afkOnly, + data.onChat != null ? data.onChat : config.defaultValues.onChat + ); + } + + public boolean shouldNotify(ServerPlayer player) { + if (!isEnabled()) + return false; + + var data = getPlayerData(player); + var settings = getPlayerSettings(player); + + if (!data.enable) + return false; + + var afkModule = Solstice.modules.getModule(AfkModule.class); + if (afkModule.isEnabled()) { + return afkModule.isPlayerAfk(player) || !settings.afkOnly(); + } + + return true; + } + + public void notifyPlayer(ServerPlayer player) { + if (!shouldNotify(player)) + return; + + var settings = getPlayerSettings(player); + var id = ResourceLocation.tryParse(settings.soundId()); + if (id == null) { + return; + } + + player.playNotifySound(SoundEvent.createVariableRangeEvent(id), SoundSource.MASTER, settings.volume(), settings.pitch()); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/commands/NotificationsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/commands/NotificationsCommand.java new file mode 100644 index 0000000..c640aed --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/commands/NotificationsCommand.java @@ -0,0 +1,208 @@ +package me.alexdevs.solstice.modules.notifications.commands; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.notifications.NotificationsModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +public class NotificationsCommand extends ModCommand { + + public NotificationsCommand(NotificationsModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("notifications"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(true)) + .then(Commands.literal("set") + .then(Commands.literal("sound") + .then(Commands.argument("sound", ResourceLocationArgument.id()) + .suggests(SuggestionProviders.AVAILABLE_SOUNDS) + .executes(this::setSound) + ) + ) + .then(Commands.literal("pitch") + .then(Commands.argument("pitch", FloatArgumentType.floatArg(0f, 2f)) + .executes(this::setPitch) + ) + ) + .then(Commands.literal("volume") + .then(Commands.argument("volume", IntegerArgumentType.integer(0, 200)) + .executes(this::setVolume) + ) + ) + .then(Commands.literal("afk-only") + .then(Commands.argument("afk-only", BoolArgumentType.bool()) + .executes(this::setAfkOnly) + ) + ) + .then(Commands.literal("on-chat") + .then(Commands.argument("on-chat", BoolArgumentType.bool()) + .executes(this::setOnChat) + ) + ) + ) + .then(Commands.literal("get") + .executes(this::getSettings)) + .then(Commands.literal("toggle") + .executes(this::toggle)) + .then(Commands.literal("reset") + .executes(this::reset)); + } + + private int setSound(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var soundId = ResourceLocationArgument.getId(context, "sound"); + + var data = module.getPlayerData(player); + data.soundId = soundId.toString(); + + var map = Map.of( + "sound", Component.nullToEmpty(soundId.toString()) + ); + context.getSource().sendSuccess(() -> module.locale().get("setSound", map), false); + + return 1; + } + + private int setPitch(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var pitch = FloatArgumentType.getFloat(context, "pitch"); + + var data = module.getPlayerData(player); + data.pitch = pitch; + + var map = Map.of( + "pitch", Component.nullToEmpty(String.valueOf(pitch)) + ); + context.getSource().sendSuccess(() -> module.locale().get("setPitch", map), false); + + return 1; + } + + private int setVolume(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var volume = IntegerArgumentType.getInteger(context, "volume"); + + var data = module.getPlayerData(player); + data.volume = volume / 100f; + + var map = Map.of( + "volume", Component.nullToEmpty(volume + "%") + ); + context.getSource().sendSuccess(() -> module.locale().get("setVolume", map), false); + + return 1; + } + + private int setAfkOnly(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var afkOnly = BoolArgumentType.getBool(context, "afk-only"); + + var data = module.getPlayerData(player); + data.afkOnly = afkOnly; + + if (afkOnly) { + context.getSource().sendSuccess(() -> module.locale().get("setAfkOnlyEnabled"), false); + } else { + context.getSource().sendSuccess(() -> module.locale().get("setAfkOnlyDisabled"), false); + } + + return 1; + } + + private int setOnChat(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var onChat = BoolArgumentType.getBool(context, "on-chat"); + + var data = module.getPlayerData(player); + data.onChat = onChat; + + if (onChat) { + context.getSource().sendSuccess(() -> module.locale().get("setOnChatEnabled"), false); + } else { + context.getSource().sendSuccess(() -> module.locale().get("setOnChatDisabled"), false); + } + + return 1; + } + + private int getSettings(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var data = module.getPlayerData(player); + var settings = module.getPlayerSettings(player); + + var map = Map.of( + "sound", Component.nullToEmpty(settings.soundId()), + "pitch", Component.nullToEmpty(String.valueOf(settings.pitch())), + "volume", Component.nullToEmpty(settings.volume() * 100 + "%") + ); + + var text = Component.empty(); + text.append(module.locale().get("getHeader")); + text.append("\n"); + + text.append(module.locale().get(data.enable ? "getEnabled.true" : "getEnabled.false")); + text.append("\n"); + text.append(module.locale().get("getSound", map)); + text.append("\n"); + text.append(module.locale().get("getPitch", map)); + text.append("\n"); + text.append(module.locale().get("getVolume", map)); + text.append("\n"); + text.append(module.locale().get(settings.afkOnly() ? "getAfkOnly.true" : "getAfkOnly.false")); + text.append("\n"); + text.append(module.locale().get(settings.onChat() ? "getOnChat.true" : "getOnChat.false")); + + context.getSource().sendSuccess(() -> text, false); + + return 1; + } + + private int toggle(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var data = module.getPlayerData(player); + data.enable = !data.enable; + + if (data.enable) { + context.getSource().sendSuccess(() -> module.locale().get("toggleEnabled"), false); + } else { + context.getSource().sendSuccess(() -> module.locale().get("toggleDisabled"), false); + } + + return 1; + } + + private int reset(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var data = module.getPlayerData(player); + data.soundId = null; + data.pitch = null; + data.volume = null; + data.afkOnly = null; + + context.getSource().sendSuccess(() -> module.locale().get("reset"), false); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsConfig.java new file mode 100644 index 0000000..91bff7a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsConfig.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.notifications.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public class NotificationsConfig { + public DefaultValues defaultValues = new DefaultValues(); + + @ConfigSerializable + public static class DefaultValues { + public String soundId = "minecraft:block.note_block.bell"; + public float pitch = 1f; + public float volume = 1f; + public boolean afkOnly = true; + public boolean onChat = true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsLocale.java new file mode 100644 index 0000000..3bf2798 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsLocale.java @@ -0,0 +1,31 @@ +package me.alexdevs.solstice.modules.notifications.data; + +import java.util.Map; + +public class NotificationsLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("setSound", "Notifications sound set to ${sound}."), + Map.entry("setPitch", "Notifications pitch set to ${pitch}."), + Map.entry("setVolume", "Notifications volume set to ${volume}."), + Map.entry("setAfkOnlyEnabled", "Notifications enabled only while AFK."), + Map.entry("setAfkOnlyDisabled", "Notifications always enabled."), + Map.entry("setOnChatEnabled", "Chat pings enabled."), + Map.entry("setOnChatDisabled", "Chat pings disabled."), + + Map.entry("toggleEnabled", "Notifications enabled."), + Map.entry("toggleDisabled", "Notifications disabled."), + + Map.entry("reset", "Notification settings cleared."), + + Map.entry("getHeader", "Your notifications settings:"), + Map.entry("getEnabled.true", " Enabled: enabled"), + Map.entry("getEnabled.false", " Enabled: disabled"), + Map.entry("getSound", " Sound: ${sound}"), + Map.entry("getPitch", " Pitch: ${pitch}"), + Map.entry("getVolume", " Volume: ${volume}"), + Map.entry("getAfkOnly.true", " AFK-Only: enabled"), + Map.entry("getAfkOnly.false", " AFK-Only: disabled"), + Map.entry("getOnChat.true", " On chat: enabled"), + Map.entry("getOnChat.false", " On chat: disabled") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsPlayerData.java new file mode 100644 index 0000000..c85dfa4 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/NotificationsPlayerData.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.notifications.data; + +import org.jetbrains.annotations.Nullable; + +public class NotificationsPlayerData { + public boolean enable = true; + @Nullable + public Boolean afkOnly = null; + @Nullable + public String soundId = null; + @Nullable + public Float pitch = null; + @Nullable + public Float volume = null; + @Nullable + public Boolean onChat = null; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/PlayerNotificationSettings.java b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/PlayerNotificationSettings.java new file mode 100644 index 0000000..6a95cfd --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/notifications/data/PlayerNotificationSettings.java @@ -0,0 +1,4 @@ +package me.alexdevs.solstice.modules.notifications.data; + +public record PlayerNotificationSettings(String soundId, float pitch, float volume, boolean afkOnly, boolean onChat) { +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/placeholders/PlaceholdersModule.java b/common/src/main/java/me/alexdevs/solstice/modules/placeholders/PlaceholdersModule.java new file mode 100644 index 0000000..c8e9487 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/placeholders/PlaceholdersModule.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.placeholders; + +import eu.pb4.placeholders.api.PlaceholderResult; +import eu.pb4.placeholders.api.Placeholders; +import me.alexdevs.solstice.api.module.ModuleBase; +import net.minecraft.resources.ResourceLocation; + +public class PlaceholdersModule extends ModuleBase.Toggleable { + public static final String ID = "placeholders"; + public static final String ENTITY = "entity"; + + public PlaceholdersModule() { + super(ID); + } + + @Override + public void init() { + Placeholders.register(ResourceLocation.fromNamespaceAndPath(ENTITY, "name"), (context, str) -> { + if(!context.hasEntity()) { + return PlaceholderResult.invalid("No entity!"); + } + var entity = context.entity(); + return PlaceholderResult.value(entity.getName()); + }); + + Placeholders.register(ResourceLocation.fromNamespaceAndPath(ENTITY, "displayname"), (context, str) -> { + if(!context.hasEntity()) { + return PlaceholderResult.invalid("No entity!"); + } + var entity = context.entity(); + return PlaceholderResult.value(entity.getDisplayName()); + }); + + Placeholders.register(ResourceLocation.fromNamespaceAndPath(ENTITY, "uuid"), (context, str) -> { + if(!context.hasEntity()) { + return PlaceholderResult.invalid("No entity!"); + } + var entity = context.entity(); + return PlaceholderResult.value(entity.getStringUUID()); + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/powertool/Action.java b/common/src/main/java/me/alexdevs/solstice/modules/powertool/Action.java new file mode 100644 index 0000000..cc9c918 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/powertool/Action.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.powertool; + +import java.util.Arrays; +import net.minecraft.util.StringRepresentable; + +public enum Action implements StringRepresentable { + USE, + ATTACK_BLOCK, + ATTACK_ENTITY, + INTERACT_BLOCK, + INTERACT_ENTITY; + + @Override + public String getSerializedName() { + return this.name().toLowerCase(); + } + + public static String[] stringValues() { + return Arrays.stream(values()).map(Action::getSerializedName).toArray(String[]::new); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/powertool/PowerToolModule.java b/common/src/main/java/me/alexdevs/solstice/modules/powertool/PowerToolModule.java new file mode 100644 index 0000000..83948df --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/powertool/PowerToolModule.java @@ -0,0 +1,154 @@ +package me.alexdevs.solstice.modules.powertool; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import eu.pb4.placeholders.api.Placeholders; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.powertool.commands.PowerToolCommand; +import me.alexdevs.solstice.modules.powertool.data.PowerToolLocale; +import me.alexdevs.solstice.modules.powertool.data.PowerToolPlayerData; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.player.*; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.item.ItemStack; +import java.util.UUID; + +public class PowerToolModule extends ModuleBase.Toggleable { + public static final String ID = "powertool"; + + private CommandDispatcher dispatcher; + + public PowerToolModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, PowerToolLocale.MODULE); + Solstice.playerData.registerData(ID, PowerToolPlayerData.class, PowerToolPlayerData::new); + + commands.add(new PowerToolCommand(this)); + + CommandRegistrationCallback.EVENT.register((dispatcher, registry, environment) -> this.dispatcher = dispatcher); + + // USE + UseItemCallback.EVENT.register((player, world, hand) -> { + var stack = player.getItemInHand(hand); + if (!stack.isEmpty()) { + var data = getData(player.getUUID()); + var itemId = getStackId(stack); + if (data.powerTools.containsKey(itemId)) { + var powertool = data.powerTools.get(itemId); + if (powertool.containsKey(Action.USE)) { + var source = player.createCommandSourceStack(); + execute(source, powertool.get(Action.USE), PlaceholderContext.of(player)); + + return InteractionResultHolder.consume(stack); + } + } + } + return InteractionResultHolder.pass(stack); + }); + + // ATTACK_BLOCK + AttackBlockCallback.EVENT.register((player, world, hand, blockPos, direction) -> { + var stack = player.getItemInHand(hand); + if (!stack.isEmpty()) { + var data = getData(player.getUUID()); + var itemId = getStackId(stack); + if (data.powerTools.containsKey(itemId)) { + var powertool = data.powerTools.get(itemId); + if (powertool.containsKey(Action.ATTACK_BLOCK)) { + var source = player.createCommandSourceStack(); + execute(source, powertool.get(Action.ATTACK_BLOCK), PlaceholderContext.of(player)); + + return InteractionResult.CONSUME; + } + } + } + return InteractionResult.PASS; + }); + + // ATTACK_ENTITY + AttackEntityCallback.EVENT.register((player, world, hand, entity, entityHitResult) -> { + var stack = player.getItemInHand(hand); + if (!stack.isEmpty()) { + var data = getData(player.getUUID()); + var itemId = getStackId(stack); + if (data.powerTools.containsKey(itemId)) { + var powertool = data.powerTools.get(itemId); + if (powertool.containsKey(Action.ATTACK_ENTITY)) { + var source = player.createCommandSourceStack(); + execute(source, powertool.get(Action.ATTACK_ENTITY), PlaceholderContext.of(entity)); + + return InteractionResult.CONSUME; + } + } + } + return InteractionResult.PASS; + }); + + // INTERACT_BLOCK + UseBlockCallback.EVENT.register((player, world, hand, blockHitResult) -> { + var stack = player.getItemInHand(hand); + if (!stack.isEmpty()) { + var data = getData(player.getUUID()); + var itemId = getStackId(stack); + if (data.powerTools.containsKey(itemId)) { + var powertool = data.powerTools.get(itemId); + if (powertool.containsKey(Action.INTERACT_BLOCK)) { + var source = player.createCommandSourceStack(); + execute(source, powertool.get(Action.INTERACT_BLOCK), PlaceholderContext.of(player)); + + return InteractionResult.CONSUME; + } + } + } + return InteractionResult.PASS; + }); + + // INTERACT_ENTITY + UseEntityCallback.EVENT.register((player, world, hand, entity, hit) -> { + var stack = player.getItemInHand(hand); + if (!stack.isEmpty()) { + var data = getData(player.getUUID()); + var itemId = getStackId(stack); + if (data.powerTools.containsKey(itemId)) { + var powertool = data.powerTools.get(itemId); + if (powertool.containsKey(Action.INTERACT_ENTITY)) { + var source = player.createCommandSourceStack(); + execute(source, powertool.get(Action.INTERACT_ENTITY), PlaceholderContext.of(entity)); + + return InteractionResult.CONSUME; + } + } + } + return InteractionResult.PASS; + }); + } + + public void execute(CommandSourceStack source, String command, PlaceholderContext context) { + try { + dispatcher.execute(resolveCommand(command, context), source); + } catch (CommandSyntaxException e) { + source.sendFailure(Component.nullToEmpty(e.getMessage())); + } + } + + private String resolveCommand(String command, PlaceholderContext context) { + return Placeholders.parseText(Component.nullToEmpty(command), context).getString(); + } + + public String getStackId(ItemStack stack) { + return stack.getItemHolder().unwrapKey().get().location().toString(); + } + + public PowerToolPlayerData getData(UUID uuid) { + return Solstice.playerData.get(uuid).getData(PowerToolPlayerData.class); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/powertool/commands/PowerToolCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/powertool/commands/PowerToolCommand.java new file mode 100644 index 0000000..2770288 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/powertool/commands/PowerToolCommand.java @@ -0,0 +1,181 @@ +package me.alexdevs.solstice.modules.powertool.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.powertool.Action; +import me.alexdevs.solstice.modules.powertool.PowerToolModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PowerToolCommand extends ModCommand { + public PowerToolCommand(PowerToolModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("powertool", "pt"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(2)) + .then(Commands.literal("set") + .then(Commands.argument("action", StringArgumentType.word()) + .suggests((context, builder) -> SharedSuggestionProvider.suggest(Action.stringValues(), builder)) + .then(Commands.argument("command", StringArgumentType.greedyString()) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var actionName = StringArgumentType.getString(context, "action"); + var command = StringArgumentType.getString(context, "command"); + + var action = Action.valueOf(actionName.toUpperCase()); + + var hand = player.getUsedItemHand(); + var item = player.getItemInHand(hand); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyHand"), false); + return 0; + } + + var data = module.getData(player.getUUID()); + + var itemId = module.getStackId(item); + + var powerTool = data.powerTools.computeIfAbsent(itemId, s -> new HashMap<>()); + powerTool.put(action, command); + + var map = Map.of( + "item", Component.nullToEmpty(itemId), + "action", Component.nullToEmpty(actionName), + "command", Component.nullToEmpty(command) + ); + + context.getSource().sendSuccess(() -> module.locale().get("actionSet", map), false); + + return 1; + }) + ) + )) + .then(Commands.literal("clear") + .then(Commands.argument("action", StringArgumentType.word()) + .suggests((context, builder) -> SharedSuggestionProvider.suggest(Action.stringValues(), builder)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var actionName = StringArgumentType.getString(context, "action"); + + var action = Action.valueOf(actionName.toUpperCase()); + + var hand = player.getUsedItemHand(); + var item = player.getItemInHand(hand); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyHand"), false); + return 0; + } + + var data = module.getData(player.getUUID()); + + var itemId = module.getStackId(item); + + var map = Map.of( + "item", Component.nullToEmpty(itemId), + "action", Component.nullToEmpty(actionName) + ); + + if(!data.powerTools.containsKey(itemId)) { + context.getSource().sendSuccess(() -> module.locale().get("noAction", map), false); + return 0; + } + + data.powerTools.get(itemId).remove(action); + + context.getSource().sendSuccess(() -> module.locale().get("actionCleared", map), false); + + return 1; + }) + ) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var hand = player.getUsedItemHand(); + var item = player.getItemInHand(hand); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyHand"), false); + return 0; + } + + var data = module.getData(player.getUUID()); + + var itemId = module.getStackId(item); + + data.powerTools.remove(itemId); + + var map = Map.of( + "item", Component.nullToEmpty(itemId) + ); + + context.getSource().sendSuccess(() -> module.locale().get("allCleared", map), false); + + return 1; + }) + ) + .then(Commands.literal("check") + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + var hand = player.getUsedItemHand(); + var item = player.getItemInHand(hand); + + if (item.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get("emptyHand"), false); + return 0; + } + + var data = module.getData(player.getUUID()); + + var itemId = module.getStackId(item); + + var powertool = data.powerTools.getOrDefault(itemId, Map.of()); + + var itemMap = Map.of( + "item", Component.nullToEmpty(itemId) + ); + + var text = Component.empty(); + text.append(module.locale().get("check", itemMap)); + + for(var action : Action.values()) { + text.append("\n"); + + if(powertool.containsKey(action)) { + var map = Map.of( + "item", Component.nullToEmpty(itemId), + "action", Component.nullToEmpty(action.getSerializedName()), + "command", Component.nullToEmpty(powertool.get(action)) + ); + text.append(module.locale().get("checkEntry", map)); + } else { + var map = Map.of( + "item", Component.nullToEmpty(itemId), + "action", Component.nullToEmpty(action.getSerializedName()) + ); + text.append(module.locale().get("checkEntryNotSet", map)); + } + } + + context.getSource().sendSuccess(() -> text, false); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolLocale.java new file mode 100644 index 0000000..d8962fe --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolLocale.java @@ -0,0 +1,16 @@ +package me.alexdevs.solstice.modules.powertool.data; + +import java.util.Map; + +public class PowerToolLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("emptyHand", "You are not holding any item."), + Map.entry("actionSet", "Command bound to ${item} on ${action}."), + Map.entry("actionCleared", "Command cleared from ${item} on ${action}."), + Map.entry("allCleared", "All commands cleared from ${item}!"), + Map.entry("noAction", "This action is not bound for this item."), + Map.entry("check", "Actions for ${item}:"), + Map.entry("checkEntry", " - ${action}: ${command}"), + Map.entry("checkEntryNotSet", " - ${action}: Not set") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolPlayerData.java b/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolPlayerData.java new file mode 100644 index 0000000..4e48ee9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/powertool/data/PowerToolPlayerData.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.powertool.data; + +import me.alexdevs.solstice.modules.powertool.Action; + +import java.util.HashMap; +import java.util.Map; + +public class PowerToolPlayerData { + public Map> powerTools = new HashMap<>(); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/restart/RestartModule.java b/common/src/main/java/me/alexdevs/solstice/modules/restart/RestartModule.java new file mode 100644 index 0000000..e9001ac --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/restart/RestartModule.java @@ -0,0 +1,219 @@ +package me.alexdevs.solstice.modules.restart; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.RestartEvents; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.events.TimeBarEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.restart.commands.RestartCommand; +import me.alexdevs.solstice.modules.restart.data.RestartConfig; +import me.alexdevs.solstice.modules.restart.data.RestartLocale; +import me.alexdevs.solstice.modules.timeBar.TimeBar; +import me.alexdevs.solstice.modules.timeBar.TimeBarModule; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.BossEvent; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class RestartModule extends ModuleBase.Toggleable { + public static final String ID = "restart"; + + private static final BossEvent.BossBarColor fallbackBarColor = BossEvent.BossBarColor.RED; + private static final BossEvent.BossBarOverlay fallbackBarStyle = BossEvent.BossBarOverlay.NOTCHED_10; + + private TimeBar restartBar = null; + private SoundEvent sound; + private ScheduledFuture currentSchedule = null; + + public RestartModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, RestartConfig.class, RestartConfig::new); + Solstice.localeManager.registerModule(ID, RestartLocale.MODULE); + + commands.add(new RestartCommand(this)); + + SolsticeEvents.READY.register((instance, server) -> { + setup(); + if (getConfig().enable) { + scheduleNextRestart(); + } + }); + + TimeBarEvents.PROGRESS.register((timeBar, server) -> { + if (restartBar == null || !timeBar.getUuid().equals(restartBar.getUuid())) + return; + + var notificationTimes = getConfig().restartNotifications; + + var remainingSeconds = restartBar.getRemainingSeconds(); + if (notificationTimes.contains(remainingSeconds)) { + notifyRestart(server, restartBar); + } + + }); + + // Shutdown + TimeBarEvents.END.register((timeBar, server) -> { + if (restartBar == null || !timeBar.getUuid().equals(restartBar.getUuid())) + return; + + restart(); + }); + + SolsticeEvents.RELOAD.register(instance -> setup()); + } + + @Override + public boolean isEnabled() { + if(!Solstice.modules.getModule(TimeBarModule.class).isEnabled()) + return false; + + return super.isEnabled(); + } + + public RestartConfig getConfig() { + return Solstice.configManager.getData(RestartConfig.class); + } + + public BossEvent.BossBarOverlay getBarStyle() { + var styleName = getConfig().barStyle; + try { + return BossEvent.BossBarOverlay.valueOf(styleName); + } catch (IllegalArgumentException e) { + Solstice.LOGGER.error("Invalid value in `restart -> bar-style` setting."); + return fallbackBarStyle; + } + } + + public BossEvent.BossBarColor getBarColor() { + var colorName = getConfig().barColor; + try { + return BossEvent.BossBarColor.valueOf(colorName); + } catch (IllegalArgumentException e) { + Solstice.LOGGER.error("Invalid value in `restart -> bar-color` setting."); + return fallbackBarColor; + } + } + + public void restart() { + Solstice.server.getPlayerList().getPlayers().forEach(player -> player.connection.disconnect(locale().get("kickMessage"))); + + Solstice.nextTick(() -> Solstice.server.halt(false)); + } + + private void setup() { + var soundName = getConfig().restartSound; + var id = ResourceLocation.tryParse(soundName); + if (id == null) { + Solstice.LOGGER.error("Invalid restart notification sound name {}", soundName); + sound = SoundEvents.NOTE_BLOCK_BELL.value(); + } else { + sound = SoundEvent.createVariableRangeEvent(id); + } + } + + public void schedule(int seconds, String message, RestartEvents.RestartType restartType) { + if(isRunning()) { + Solstice.LOGGER.warn("Could not start a new restart countdown because there is one already running."); + return; + } + + var timeBar = Solstice.modules.getModule(TimeBarModule.class); + restartBar = timeBar.startTimeBar( + message, + seconds, + getBarColor(), + getBarStyle(), + true + ); + + RestartEvents.SCHEDULED.invoker().onSchedule(restartBar, restartType); + } + + public boolean isScheduled() { + return restartBar != null || currentSchedule != null && !currentSchedule.isCancelled(); + } + + public boolean isRunning() { + return restartBar != null; + } + + public void cancel() { + var timeBar = Solstice.modules.getModule(TimeBarModule.class); + if (restartBar != null) { + timeBar.cancelTimeBar(restartBar); + RestartEvents.CANCELED.invoker().onCancel(restartBar); + restartBar = null; + } + + if (currentSchedule != null) { + currentSchedule.cancel(false); + currentSchedule = null; + } + } + + private void notifyRestart(MinecraftServer server, TimeBar bar) { + var solstice = Solstice.getInstance(); + var text = bar.parseLabel(locale().raw("chatMessage")); + solstice.broadcast(text); + + var pitch = getConfig().restartSoundPitch; + server.getPlayerList().getPlayers().forEach(player -> player.playNotifySound(sound, SoundSource.MASTER, 1f, pitch)); + } + + @Nullable + public Long scheduleNextRestart() { + var delay = getNextDelay(); + if (delay == null) + return null; + + var barTime = getConfig().restartNotifications.stream().max(Integer::compareTo).orElse(600); + var barStartTime = delay - barTime; + + currentSchedule = Solstice.scheduler.schedule(() -> schedule(barTime, locale().raw("barLabel"), RestartEvents.RestartType.AUTOMATIC), barStartTime, TimeUnit.SECONDS); + + Solstice.LOGGER.info("Restart scheduled for in {} seconds", delay); + return delay; + } + + @Nullable + private Long getNextDelay() { + var restartTimeStrings = getConfig().restartAt; + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRunTime = null; + long shortestDelay = Long.MAX_VALUE; + + for (var timeString : restartTimeStrings) { + LocalTime targetTime = LocalTime.parse(timeString); + LocalDateTime targetDateTime = now.with(targetTime); + + if (targetDateTime.isBefore(now)) { + targetDateTime = targetDateTime.plusDays(1); + } + + long delay = Duration.between(now, targetDateTime).toSeconds(); + if (delay < shortestDelay) { + shortestDelay = delay; + nextRunTime = targetDateTime; + } + } + + if (nextRunTime != null) { + return shortestDelay; + } + return null; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/restart/commands/RestartCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/restart/commands/RestartCommand.java new file mode 100644 index 0000000..cb8a355 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/restart/commands/RestartCommand.java @@ -0,0 +1,97 @@ +package me.alexdevs.solstice.modules.restart.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.events.RestartEvents; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.restart.RestartModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class RestartCommand extends ModCommand { + public RestartCommand(RestartModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("restart"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(4)) + .then(literal("now") + .executes(context -> { + context.getSource().sendSuccess(() -> Component.nullToEmpty("Restarting server"), true); + module.restart(); + return 1; + })) + .then(literal("schedule") + .then(argument("timespan", StringArgumentType.word()) + .suggests(TimeSpan::suggest) + .executes(context -> schedule(context, TimeSpan.getTimeSpan(context, "timespan"), null)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(context -> schedule(context, TimeSpan.getTimeSpan(context, "timespan"), StringArgumentType.getString(context, "message"))))) + .then(literal("next") + .executes(this::scheduleNext)) + ) + .then(literal("cancel") + .executes(this::cancel)); + } + + private int schedule(CommandContext context, int seconds, @Nullable String message) { + if (module.isRunning()) { + context.getSource().sendFailure(Component.nullToEmpty("There is already a running restart.")); + return 0; + } + + if (message == null) { + message = Solstice.localeManager.getLocale(RestartModule.ID).raw("barLabel"); + } + module.schedule(seconds, message, RestartEvents.RestartType.MANUAL); + + context.getSource().sendSuccess(() -> Component.nullToEmpty("Manual restart scheduled in " + seconds + " seconds."), true); + + return 1; + } + + private int scheduleNext(CommandContext context) { + if (module.isScheduled()) { + context.getSource().sendFailure(Component.nullToEmpty("There is already a scheduled restart.")); + return 0; + } + + var delay = module.scheduleNextRestart(); + + if (delay == null) { + context.getSource().sendFailure(Component.nullToEmpty("Could not schedule next automatic restart.")); + return 0; + } else { + context.getSource().sendSuccess(() -> Component.literal("Next automatic restart scheduled in " + delay + " seconds."), true); + } + + return 1; + } + + private int cancel(CommandContext context) { + if (!module.isScheduled()) { + context.getSource().sendFailure(Component.nullToEmpty("There is no scheduled restart.")); + return 0; + } + + module.cancel(); + context.getSource().sendSuccess(() -> Component.literal("Restart schedule canceled."), true); + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartConfig.java new file mode 100644 index 0000000..22b04be --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartConfig.java @@ -0,0 +1,48 @@ +package me.alexdevs.solstice.modules.restart.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class RestartConfig { + @Comment("Enable auto restart functionality.") + public boolean enable = true; + + @Comment("Restart the server at exactly the following times. Time is local.") + public ArrayList restartAt = new ArrayList<>(List.of( + "06:00", + "18:00" + )); + + @Comment("Sound to play when sending the restart notification in chat.") + public String restartSound = "minecraft:block.note_block.bell"; + + @Comment("Pitch of the sound.") + public float restartSoundPitch = 0.9f; + + @Comment("Style of the restart bar.") + public String barStyle = "NOTCHED_10"; + + @Comment("Color of the restart bar.") + public String barColor = "RED"; + + @Comment("Milestones of the restart notifications in seconds.") + public ArrayList restartNotifications = new ArrayList<>(List.of( + 600, + 300, + 120, + 60, + 30, + 15, + 10, + 5, + 4, + 3, + 2, + 1 + )); + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartLocale.java new file mode 100644 index 0000000..65e4a7e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/restart/data/RestartLocale.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.restart.data; + +import java.util.Map; + +public class RestartLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("barLabel", "Server restarting in ${remaining_time}"), + Map.entry("kickMessage", "The server is restarting!"), + Map.entry("chatMessage", "The server is restarting in ${remaining_time}") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/rtp/RTPModule.java b/common/src/main/java/me/alexdevs/solstice/modules/rtp/RTPModule.java new file mode 100644 index 0000000..b1a869d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/rtp/RTPModule.java @@ -0,0 +1,49 @@ +package me.alexdevs.solstice.modules.rtp; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.rtp.commands.RTPCommand; +import me.alexdevs.solstice.modules.rtp.core.Locator; +import me.alexdevs.solstice.modules.rtp.data.RTPConfig; +import me.alexdevs.solstice.modules.rtp.data.RTPLocale; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.biome.Biome; +import java.util.ArrayList; + +public class RTPModule extends ModuleBase.Toggleable { + public static final String ID = "rtp"; + + private final ArrayList locators = new ArrayList<>(); + + public RTPModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, RTPLocale.MODULE); + Solstice.configManager.registerData(ID, RTPConfig.class, RTPConfig::new); + + commands.add(new RTPCommand(this)); + + ServerTickEvents.END_SERVER_TICK.register(server -> locators.removeIf(Locator::tick)); + } + + public RTPConfig getConfig() { + return Solstice.configManager.getData(RTPConfig.class); + } + + public Locator createLocator(ServerPlayer player) { + var locator = new Locator(player, player.serverLevel(), getConfig()); + locators.add(locator); + return locator; + } + + public Locator createLocatorWithBiome(ServerPlayer player, ResourceKey biome) { + var locator = new Locator(player, player.serverLevel(), getConfig(), biome); + locators.add(locator); + return locator; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/rtp/commands/RTPCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/rtp/commands/RTPCommand.java new file mode 100644 index 0000000..bb9e4fd --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/rtp/commands/RTPCommand.java @@ -0,0 +1,166 @@ +package me.alexdevs.solstice.modules.rtp.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.rtp.RTPModule; +import me.alexdevs.solstice.modules.rtp.core.Locator; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.ResourceArgument; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class RTPCommand extends ModCommand { + public RTPCommand(RTPModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("rtp"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> execute(context, false)) + .then(argument("biome", ResourceArgument.resource(commandRegistry, Registries.BIOME)) + .requires(require("biome.base", 2)) + .suggests((context, builder) -> { + if (Permissions.check(context.getSource(), getPermissionNode("exempt.biome"), 2)) { + var biomeRegistry = this.commandRegistry.lookup(Registries.BIOME); + var biomes = biomeRegistry.stream().map(r -> r.key().location().toString()).toList(); + return SharedSuggestionProvider.suggest(biomes, builder); + } + + var biomes = getAllowedBiomes(context.getSource(), context.getSource().getLevel()); + return SharedSuggestionProvider.suggest(biomes, builder); + }) + .executes(context -> execute(context, true)) + ); + } + + private int execute(CommandContext context, boolean withBiome) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var config = module.getConfig(); + + if (config.requireWorldPermission) { + var worldName = player.serverLevel().dimension().location().toString(); + if (!Permissions.check(context.getSource(), getPermissionNode("worlds." + worldName), 2)) { + context.getSource().sendSuccess(() -> module.locale().get("noWorldPermission", Map.of("world", Component.nullToEmpty(worldName))), false); + return 0; + } + } + + ResourceKey biome = null; + if (withBiome) { + var biomeEntry = ResourceArgument.getResource(context, "biome", Registries.BIOME); + biome = biomeEntry.unwrapKey().orElse(null); + + if (biomeEntry.unwrapKey().isPresent()) { + if (!Permissions.check(context.getSource(), getPermissionNode("exempt.biome"), 2)) { + var biomeId = biome.location().toString(); + var allowedBiomes = getAllowedBiomes(context.getSource(), context.getSource().getLevel()); + if (!allowedBiomes.contains(biomeId)) { + context.getSource().sendSuccess(() -> module.locale().get("noBiomePermission"), false); + return 0; + } + } + } + } + + if (config.cooldown.enable) { + if (!Solstice.cooldown.trigger(player, module.getPermissionNode(), config.cooldown.cooldown)) { + context.getSource().sendSuccess(() -> Solstice.cooldown.getMessage(player, module.getPermissionNode()), false); + return 0; + } + } + + final var server = context.getSource().getServer(); + final var uuid = player.getUUID(); + + Locator locator; + if (!withBiome) { + locator = module.createLocator(player); + } else { + locator = module.createLocatorWithBiome(player, biome); + } + + locator.locate(result -> { + var newPlayer = server.getPlayerList().getPlayer(uuid); + if (newPlayer == null) { + Solstice.LOGGER.info("RTP spot found, but player left."); + return; + } + if (result.position().isPresent() && result.type() == Locator.Result.Type.SUCCESS) { + player.sendSystemMessage(module.locale().get("success")); + result.position().get().teleport(player); + } else { + final var text = switch (result.type()) { + case TOO_MANY_ATTEMPTS -> module.locale().get("tooManyAttempts"); + case TIMEOUT -> module.locale().get("timeout"); + case UNSAFE -> module.locale().get("unsafe"); + default -> Component.nullToEmpty(result.type().toString()); + }; + player.sendSystemMessage(text); + + if (config.cooldown.cancelOnFail) { + Solstice.cooldown.clear(player, module.getPermissionNode()); + } + } + }); + + context.getSource().sendSuccess(() -> module.locale().get("searching"), false); + + return 1; + } + + private List getAllowedBiomes(CommandSourceStack source, ServerLevel world) { + var groups = getAllowedGroups(source, world); + var biomes = new ArrayList(); + groups.forEach(group -> biomes.addAll(getBiomesInGroup(world, group))); + return biomes; + } + + private List getBiomesInGroup(ServerLevel world, String group) { + var config = module.getConfig(); + var worlds = config.biomeGroups; + var worldId = world.dimension().location().toString(); + if (!worlds.containsKey(worldId)) { + return List.of(); + } + + var groups = worlds.get(worldId); + if (!groups.containsKey(group)) { + return List.of(); + } + + return groups.get(group); + } + + private List getAllowedGroups(CommandSourceStack source, ServerLevel world) { + var worldId = world.dimension().location().toString(); + if (Permissions.check(source, getPermissionNode("biomes." + worldId + ".base"), 2)) { + var config = module.getConfig(); + return config.biomeGroups.getOrDefault(worldId, Map.of()) + .keySet().stream() + .filter(name -> Permissions.check(source, getPermissionNode("biomes." + worldId + "." + name), 2)) + .toList(); + } + return List.of(); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/rtp/core/Locator.java b/common/src/main/java/me/alexdevs/solstice/modules/rtp/core/Locator.java new file mode 100644 index 0000000..2b4ceb6 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/rtp/core/Locator.java @@ -0,0 +1,249 @@ +package me.alexdevs.solstice.modules.rtp.core; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.modules.rtp.data.RTPConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.Heightmap; +import org.jetbrains.annotations.Nullable; + +import java.util.Comparator; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class Locator { + + public static final TicketType RTP_TICKET = TicketType.create("rtp", Comparator.comparingLong(ChunkPos::asLong), 300); + + public final ServerPlayer player; + public final ServerLevel world; + public final RTPConfig config; + public @Nullable + final ResourceKey biome; + + private Consumer callback; + private final Stopwatch stopwatch = Stopwatch.createUnstarted(); + + private ChunkAccess chunk; + private BlockPos attemptPos; + private boolean failed = false; + + private static final ImmutableList unsafeBlocks = ImmutableList.of( + Blocks.LAVA, + Blocks.MAGMA_BLOCK, + Blocks.CACTUS, + Blocks.FIRE, + Blocks.CAMPFIRE, + Blocks.LAVA_CAULDRON, + Blocks.SWEET_BERRY_BUSH, + Blocks.POWDER_SNOW + ); + + private static final ImmutableList nonIdealBlocks = ImmutableList.of( + Blocks.WATER + ); + + public Locator(ServerPlayer player, ServerLevel world, RTPConfig config) { + this(player, world, config, null); + } + + public Locator(ServerPlayer player, ServerLevel world, RTPConfig config, @Nullable ResourceKey biome) { + this.player = player; + this.world = world; + this.config = config; + this.biome = biome; + } + + public void locate(Consumer callback) { + this.callback = callback; + stopwatch.start(); + attempt(config.attempts); + } + + private void attempt(int remainingAttempts) { + if (remainingAttempts == 0) { + failed = true; + callback.accept(new Result(Result.Type.TOO_MANY_ATTEMPTS, Optional.empty())); + return; + } + + var pos = getRandomPos(); + + if (isValid(pos)) { + Solstice.LOGGER.info("RTP spot found at attempt {} for {}", config.attempts - remainingAttempts, player.getName()); + attemptPos = pos; + load(); + } else { + attempt(remainingAttempts - 1); + } + } + + public boolean tick() { + if (failed) return true; + + if (stopwatch.elapsed(TimeUnit.MILLISECONDS) >= config.timeout) { + callback.accept(new Result(Result.Type.TIMEOUT, Optional.empty())); + return true; + } + + // Not yet started + if (attemptPos == null) { + return false; + } + + var chunk = getChunk(new ChunkPos(attemptPos)); + if (chunk.isPresent()) { + this.chunk = chunk.get(); + findValidPlacement(); + return true; + } + + return false; + } + + private BlockPos getTopBlock(BlockPos pos) { + return world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, pos); + } + + private BlockPos getEmptySpace(BlockPos pos) { + var bottom = chunk.getMinBuildHeight(); + var top = world.getLogicalHeight(); + var blockPos = new BlockPos.MutableBlockPos(pos.getX(), top, pos.getZ()); + + var isAir = false; + var isAirBelow = false; + while (blockPos.getY() >= bottom && isAirBelow || !isAir) { + isAir = isAirBelow; + isAirBelow = chunk.getBlockState(blockPos.move(Direction.DOWN)).isAir(); + } + return blockPos.above().immutable(); + } + + private void findValidPlacement() { + BlockPos pos = attemptPos; + for (var i = 0; i <= 256; i++) { + if (world.dimensionType().hasCeiling()) { + pos = getEmptySpace(pos); + } else { + pos = getTopBlock(pos); + } + var bs = chunk.getBlockState(pos); + var bsBelow = chunk.getBlockState(pos.below()); + if (!unsafeBlocks.contains(bs.getBlock()) && !unsafeBlocks.contains(bsBelow.getBlock())) { + break; + } + + var dx = i % 16; + var dz = i / 16; + pos = chunk.getPos().getBlockAt(dx, chunk.getMinBuildHeight(), dz); + } + + if (pos.getY() <= chunk.getMinBuildHeight()) { + callback.accept(new Result(Result.Type.UNSAFE, Optional.empty())); + return; + } + + var vec = pos.getCenter(); + + callback.accept(new Result(Result.Type.SUCCESS, Optional.of(new ServerLocation( + vec.x(), vec.y(), vec.z(), 0, 0, world + )))); + } + + private void load() { + world.getChunkSource().addRegionTicket(RTP_TICKET, new ChunkPos(attemptPos), 0, attemptPos); + } + + private Optional getChunk(ChunkPos pos) { + var holder = world.getChunkSource().getVisibleChunkIfPresent(pos.toLong()); + if (holder == null) { + return Optional.empty(); + } else { + var chunk = holder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null); + if (chunk == null) { + return Optional.empty(); + } + return Optional.of(chunk); + } + } + + public boolean isValid(BlockPos pos) { + if (pos == null) + return false; + + if(this.biome != null) { + return isInBiome(pos, this.biome); + } + + var biome = world.getBiome(pos); + return !config.parseBiomes().contains(biome.unwrapKey().orElse(null)); + } + + public boolean isInBiome(BlockPos pos, ResourceKey biome) { + var biomeAtPos = world.getBiome(pos); + return biomeAtPos.unwrapKey().get().equals(biome); + } + + public BlockPos getRandomPos() { + var worldBorder = world.getWorldBorder(); + var size = worldBorder.getSize(); + + double centerX, centerZ; + if (config.aroundPlayer) { + centerX = player.getX(); + centerZ = player.getZ(); + } else { + centerX = worldBorder.getCenterX(); + centerZ = worldBorder.getCenterZ(); + } + + var maxDiameter = config.maxRadius * 2; + var minDiameter = config.minRadius * 2; + + var max = Math.min((int) size, maxDiameter); + var min = Math.max(0, minDiameter); + + int x = 0; + int z = 0; + var limit = 256; + for (var i = 0; i <= limit; i++) { + var dist = world.getRandom().nextDouble() * (max - min) + min; + var angle = world.getRandom().nextDouble() * Math.PI * 2d; + x = (int) (Math.cos(angle) * dist + centerX); + z = (int) (Math.sin(angle) * dist + centerZ); + + if (worldBorder.isWithinBounds(x, z)) + break; + + if (i == limit) { + return null; + } + } + + return new BlockPos(x, world.getLogicalHeight(), z); + } + + public record Result(Type type, Optional position) { + public enum Type { + SUCCESS, + TOO_MANY_ATTEMPTS, + TIMEOUT, + UNSAFE + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPConfig.java new file mode 100644 index 0000000..5cdda53 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPConfig.java @@ -0,0 +1,88 @@ +package me.alexdevs.solstice.modules.rtp.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.List; +import java.util.Map; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.biome.Biome; + +@ConfigSerializable +public class RTPConfig { + @Comment("How many times to try to find a valid spot to teleport to before failing.") + public int attempts = 10; + + @Comment("How much time in milliseconds /rtp can take at most before failing.") + public int timeout = 10000; + + @Comment("Minimum radius from the center of the world border.") + public int minRadius = 0; + + @Comment("Maximum radius from the center of the world border. It caps to world border size.") + public int maxRadius = 30000; + + @Comment("Use player as the center of the radius instead of the world border.") + public boolean aroundPlayer = false; + + @Comment("List of biomes an attempt should fail at.") + public List prohibitedBiomes = List.of( + "minecraft:ocean", + "minecraft:cold_ocean", + "minecraft:deep_cold_ocean", + "minecraft:deep_frozen_ocean", + "minecraft:deep_lukewarm_ocean", + "minecraft:deep_ocean", + "minecraft:frozen_ocean", + "minecraft:lukewarm_ocean", + "minecraft:warm_ocean", + "minecraft:river", + "minecraft:frozen_river", + "minecraft:small_end_islands" + ); + + @Comment("Require that the player has the permission of the world 'solstice.rtp.worlds.' to initiate the random teleport in the world.") + public boolean requireWorldPermission = true; + + @Comment("Groups of biomes the player is allowed to RTP to.\nUse 'solstice.rtp.biomes..' to assign.\nRequires 'solstice.rtp.biome.base'.") + public Map>> biomeGroups = Map.of( + "minecraft:overworld", Map.of( + "forests", List.of( + "minecraft:forest", + "minecraft:flower_forest", + "minecraft:taiga", + "minecraft:old_growth_spruce_taiga", + "minecraft:snowy_taiga", + "minecraft:birch_forest", + "minecraft:old_growth_birch_forest", + "minecraft:dark_forest", + "minecraft:pale_garden", + "minecraft:jungle", + "minecraft:sparse_jungle", + "minecraft:bamboo_jungle" + ) + ) + ); + + @Comment("Cooldown configuration") + public Cooldown cooldown = new Cooldown(); + + @ConfigSerializable + public static class Cooldown { + @Comment("This setting makes it so players have to wait before running the command a second time.") + public boolean enable = true; + + @Comment("Seconds to wait for the cooldown to expire.") + public int cooldown = 600; + + @Comment("Cancel the cooldown if /rtp fails.") + public boolean cancelOnFail = true; + } + + + public List> parseBiomes() { + return prohibitedBiomes.stream().map(biomeId -> ResourceKey.create(Registries.BIOME, ResourceLocation.parse(biomeId))).toList(); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPLocale.java new file mode 100644 index 0000000..b34116b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/rtp/data/RTPLocale.java @@ -0,0 +1,15 @@ +package me.alexdevs.solstice.modules.rtp.data; + +import java.util.Map; + +public class RTPLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("searching", "Finding a good spot..."), + Map.entry("timeout", "Could not find a valid spot in a timely manner."), + Map.entry("tooManyAttempts", "Too many failed attempts at locating a valid spot."), + Map.entry("unsafe", "Could not place you in a safe spot."), + Map.entry("success", "Teleporting to a random location..."), + Map.entry("noWorldPermission", "You do not have permission to run this command in this world."), + Map.entry("noBiomePermission", "You do not have permission to use this biome.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/seen/SeenModule.java b/common/src/main/java/me/alexdevs/solstice/modules/seen/SeenModule.java new file mode 100644 index 0000000..123f775 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/seen/SeenModule.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.seen; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.seen.commands.SeenCommand; +import me.alexdevs.solstice.modules.seen.data.SeenLocale; + +public class SeenModule extends ModuleBase.Toggleable { + public static final String ID = "seen"; + + public SeenModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, SeenLocale.MODULE); + + commands.add(new SeenCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/seen/commands/SeenCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/seen/commands/SeenCommand.java new file mode 100644 index 0000000..0d5389a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/seen/commands/SeenCommand.java @@ -0,0 +1,106 @@ +package me.alexdevs.solstice.modules.seen.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.core.coreModule.CoreModule; +import me.alexdevs.solstice.modules.seen.SeenModule; +import me.alexdevs.solstice.api.text.Format; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SeenCommand extends ModCommand { + public SeenCommand(SeenModule module) { + super(module); + } + + public static String getPositionAsString(@Nullable ServerLocation pos) { + if (pos == null) + return "Unknown position"; + + return String.format("%.01f %.01f %.01f, %s", pos.getX(), pos.getY(), pos.getZ(), pos.getWorld()); + } + + @Override + public List getNames() { + return List.of("seen", "playerinfo"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var source = context.getSource(); + + var profile = LocalGameProfile.getProfile(context, "player"); + + boolean extended; + if (context.getSource().isPlayer()) { + extended = Permissions.check(context.getSource().getPlayer(), getPermissionNode("extended"), 2); + } else { + extended = true; + } + + var config = CoreModule.getConfig(); + + var dateFormatter = new SimpleDateFormat(config.dateTimeFormat); + var player = source.getServer().getPlayerList().getPlayer(profile.getId()); + var playerData = CoreModule.getPlayerData(profile.getId()); + + if(playerData.firstJoinedDate == null) { + source.sendSuccess(() -> module.locale().get("playerNotFound"), false); + return 0; + } + + ServerLocation location; + if (player == null) { + location = playerData.logoffPosition; + } else { + location = new ServerLocation(player); + } + + var firstSeenDate = playerData.firstJoinedDate != null ? dateFormatter.format(playerData.firstJoinedDate) : module.locale().raw("neverJoined"); + var lastSeenDate = playerData.lastSeenDate != null ? dateFormatter.format(playerData.lastSeenDate) : module.locale().raw("unknown"); + var ipAddress = playerData.ipAddress != null ? playerData.ipAddress : module.locale().raw("unknown"); + + Map map = Map.of( + "username", Component.nullToEmpty(profile.getName()), + "uuid", Component.nullToEmpty(profile.getId().toString()), + "firstSeenDate", Component.nullToEmpty(firstSeenDate), + "lastSeenDate", Component.nullToEmpty(player != null ? module.locale().raw("online") : lastSeenDate), + "ipAddress", Component.nullToEmpty(ipAddress), + "location", Component.nullToEmpty(getPositionAsString(location)) + ); + + var outputString = module.locale().raw("base"); + if (extended) { + outputString += "\n"; + outputString += module.locale().raw("extended"); + } + + final var finalOutput = outputString; + if (player != null) { + source.sendSuccess(() -> Format.parse(finalOutput, PlaceholderContext.of(player), map), false); + } else { + source.sendSuccess(() -> Format.parse(finalOutput, PlaceholderContext.of(source.getServer()), map), false); + } + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/seen/data/SeenLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/seen/data/SeenLocale.java new file mode 100644 index 0000000..c47f22f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/seen/data/SeenLocale.java @@ -0,0 +1,32 @@ +package me.alexdevs.solstice.modules.seen.data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SeenLocale { + public static final Map MODULE; + private static final ArrayList base = new ArrayList<>(List.of( + "${username}'s information:", + " UUID: ${uuid}", + " First seen: ${firstSeenDate}", + " Last seen: ${lastSeenDate}" + )); + private static final ArrayList extended = new ArrayList<>(List.of( + " IP Address: ${ipAddress}", + " Location: ${location}" + )); + + static { + var map = new HashMap(); + map.put("playerNotFound", "Could not find this player"); + map.put("base", String.join("\n", base)); + map.put("extended", String.join("\n", extended)); + map.put("online", "online"); + map.put("neverJoined", "Never joined"); + map.put("unknown", "unknown"); + + MODULE = Map.copyOf(map); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/sign/SignModule.java b/common/src/main/java/me/alexdevs/solstice/modules/sign/SignModule.java new file mode 100644 index 0000000..94443ed --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/sign/SignModule.java @@ -0,0 +1,34 @@ +package me.alexdevs.solstice.modules.sign; + +import eu.pb4.placeholders.api.parsers.LegacyFormattingParser; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.network.FilteredText; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.entity.SignText; +import java.util.List; + +public class SignModule extends ModuleBase.Toggleable { + public static final String ID = "sign"; + + public SignModule() { + super(ID); + } + + @Override + public void init() { + } + + public static SignText formatSign(List messages, SignText text) { + for (var i = 0; i < messages.size(); i++) { + var message = messages.get(i); + var line = message.raw(); + text = text.setMessage(i, LegacyFormattingParser.ALL.parseNode(line).toText()); + } + return text; + } + + public boolean canFormatSign(Player player) { + return Permissions.check(player, getPermissionNode("format"), 2); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/skull/SkullModule.java b/common/src/main/java/me/alexdevs/solstice/modules/skull/SkullModule.java new file mode 100644 index 0000000..036f251 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/skull/SkullModule.java @@ -0,0 +1,47 @@ +package me.alexdevs.solstice.modules.skull; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.PropertyMap; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.skull.commands.SkullCommand; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.StringTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.PlayerHeadItem; +import net.minecraft.world.item.component.ResolvableProfile; + +import java.util.Optional; +import java.util.UUID; + +public class SkullModule extends ModuleBase.Toggleable { + public static final String ID = "skull"; + + public SkullModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new SkullCommand(this)); + } + + public ItemStack createSkull(String name) { + var skull = Items.PLAYER_HEAD.getDefaultInstance(); + name = name.substring(0, Math.min(name.length(), 16)); + skull.set(DataComponents.PROFILE, new ResolvableProfile(Optional.of(name), Optional.empty(), new PropertyMap())); + return skull; + } + + public ItemStack createSkull(UUID uuid) { + var skull = Items.PLAYER_HEAD.getDefaultInstance(); + skull.set(DataComponents.PROFILE, new ResolvableProfile(Optional.empty(), Optional.of(uuid), new PropertyMap())); + return skull; + } + + public ItemStack createSkull(GameProfile profile) { + var skull = Items.PLAYER_HEAD.getDefaultInstance(); + skull.set(DataComponents.PROFILE, new ResolvableProfile(profile)); + return skull; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/skull/commands/SkullCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/skull/commands/SkullCommand.java new file mode 100644 index 0000000..ec23f98 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/skull/commands/SkullCommand.java @@ -0,0 +1,41 @@ +package me.alexdevs.solstice.modules.skull.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.skull.SkullModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SkullCommand extends ModCommand { + public SkullCommand(SkullModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("skull"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> execute(context, context.getSource().getPlayerOrException().getGameProfile().getName())) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String skullName) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var skull = module.createSkull(skullName); + + player.getInventory().add(skull); + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/smite/SmiteModule.java b/common/src/main/java/me/alexdevs/solstice/modules/smite/SmiteModule.java new file mode 100644 index 0000000..d988656 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/smite/SmiteModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.smite; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.smite.commands.SmiteCommand; + +public class SmiteModule extends ModuleBase.Toggleable { + public static final String ID = "smite"; + + public SmiteModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new SmiteCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/smite/commands/SmiteCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/smite/commands/SmiteCommand.java new file mode 100644 index 0000000..c266587 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/smite/commands/SmiteCommand.java @@ -0,0 +1,91 @@ +package me.alexdevs.solstice.modules.smite.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.smite.SmiteModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.phys.HitResult; +import me.alexdevs.solstice.api.Raycast; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SmiteCommand extends ModCommand { + public static final EntityType entityType = EntityType.LIGHTNING_BOLT; + public static final int maxTimes = 1024; + public static final int maxDistance = 512; + + public SmiteCommand(SmiteModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("smite"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(this::executePos) + .then(argument("target", EntityArgument.entities()) + .executes(context -> + execute(context, 1) + ) + .then(argument("times", IntegerArgumentType.integer(0, maxTimes)) + .executes(context -> + execute(context, IntegerArgumentType.getInteger(context, "times")) + ))); + } + + private int executePos(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var result = Raycast.cast(player, maxDistance); + if (result.getType() == HitResult.Type.MISS) { + return 0; + } + + summon(player.serverLevel(), result.getBlockPos().above()); + + return 1; + } + + private int execute(CommandContext context, int times) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var targets = EntityArgument.getEntities(context, "target"); + var timesToSummon = targets.size() * times; + if (timesToSummon > maxTimes) { + times = maxTimes / targets.size(); + if (times == 0) + times = 1; + } + for (var i = 0; i < times; i++) { + targets.forEach(target -> + summon(player.serverLevel(), target.blockPosition()) + ); + } + + return targets.size(); + } + + private void summon(ServerLevel world, BlockPos pos) { + entityType.create( + world, + world::addFreshEntity, + pos, + MobSpawnType.COMMAND, + false, + false); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/SpawnModule.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/SpawnModule.java new file mode 100644 index 0000000..1531c6d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/SpawnModule.java @@ -0,0 +1,124 @@ +package me.alexdevs.solstice.modules.spawn; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.spawn.commands.FirstSpawnCommand; +import me.alexdevs.solstice.modules.spawn.commands.SetFirstSpawnCommand; +import me.alexdevs.solstice.modules.spawn.commands.SetSpawnCommand; +import me.alexdevs.solstice.modules.spawn.commands.SpawnCommand; +import me.alexdevs.solstice.modules.spawn.data.SpawnConfig; +import me.alexdevs.solstice.modules.spawn.data.SpawnLocale; +import me.alexdevs.solstice.modules.spawn.data.SpawnServerData; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +public class SpawnModule extends ModuleBase.Toggleable { + public static final String ID = "spawn"; + + public SpawnModule() { + super(ID); + } + + @SuppressWarnings("deprecation") + @Override + public void init() { + Solstice.localeManager.registerModule(ID, SpawnLocale.MODULE); + Solstice.serverData.registerData(ID, SpawnServerData.class, SpawnServerData::new); + Solstice.configManager.registerData(ID, SpawnConfig.class, SpawnConfig::new); + + commands.add(new SpawnCommand(this)); + commands.add(new SetSpawnCommand(this)); + commands.add(new FirstSpawnCommand(this)); + commands.add(new SetFirstSpawnCommand(this)); + + SolsticeEvents.WELCOME.register((player, server) -> { + var firstSpawn = getFirstSpawn(); + if (firstSpawn != null) { + // Send next tick, twice, so it does not conflict with "on-login" spawn setting. + Solstice.nextTick(() -> Solstice.nextTick(() -> firstSpawn.teleport(player))); + } + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + var config = getConfig(); + if (config.globalSpawn.onLogin) { + Solstice.nextTick(() -> { + getGlobalSpawnPosition().teleport(handler.getPlayer(), false); + }); + } + }); + + SolsticeEvents.READY.register((instance, server) -> { + var spawnData = getServerData(); + if (spawnData.spawn != null) { + var legacy = spawnData.spawn; + var world = legacy.getWorld(server); + world.setDefaultSpawnPos(new BlockPos((int) legacy.getX(), (int) legacy.getY(), (int) legacy.getZ()), legacy.getYaw()); + spawnData.spawn = null; + } + }); + } + + @Deprecated + public ServerLocation getSpawn() { + var serverData = getServerData(); + var spawnPosition = serverData.spawn; + if (spawnPosition == null) { + var server = Solstice.server; + var spawnPos = server.overworld().getSharedSpawnPos(); + spawnPosition = new ServerLocation(spawnPos.getX(), spawnPos.getY(), spawnPos.getZ(), 0, 0, server.overworld()); + } + return spawnPosition; + } + + public ServerLevel getGlobalSpawnWorld() { + var targetWorld = getConfig().globalSpawn.targetSpawnWorld; + + var key = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(targetWorld)); + return Solstice.server.getLevel(key); + } + + public ServerLocation getGlobalSpawnPosition() { + var world = getGlobalSpawnWorld(); + var worldSpawnPos = world.getSharedSpawnPos().getCenter(); + var worldSpawnRot = world.getSharedSpawnAngle(); + return new ServerLocation( + worldSpawnPos.x(), worldSpawnPos.y(), worldSpawnPos.z(), worldSpawnRot, 0, world + ); + } + + public ServerLocation getWorldSpawn(ServerLevel world) { + var spawnPos = world.getSharedSpawnPos().getCenter(); + var yaw = world.getSharedSpawnAngle(); + return new ServerLocation(spawnPos.x(), spawnPos.y(), spawnPos.z(), yaw, 0, world); + } + + public SpawnConfig getConfig() { + return Solstice.configManager.getData(SpawnConfig.class); + } + + public SpawnServerData getServerData() { + return Solstice.serverData.getData(SpawnServerData.class); + } + + public void sendToSpawn(ServerPlayer player) { + sendToSpawn(player, player.serverLevel()); + } + + public void sendToSpawn(ServerPlayer player, ServerLevel world) { + var pos = getWorldSpawn(world); + pos.teleport(player); + } + + public @Nullable ServerLocation getFirstSpawn() { + return Solstice.serverData.getData(SpawnServerData.class).firstSpawn; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/FirstSpawnCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/FirstSpawnCommand.java new file mode 100644 index 0000000..2c75123 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/FirstSpawnCommand.java @@ -0,0 +1,69 @@ +package me.alexdevs.solstice.modules.spawn.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class FirstSpawnCommand extends ModCommand { + public FirstSpawnCommand(SpawnModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("firstspawn"); + } + + private int execute(CommandContext context, @Nullable Collection players) throws CommandSyntaxException { + if (module.getFirstSpawn() == null) { + context.getSource().sendSuccess(() -> module.locale().get("noFirstSpawn"), false); + return 0; + } + if (players == null) { + var player = context.getSource().getPlayerOrException(); + sendToFirstSpawn(context, player); + return 1; + } else { + for (ServerPlayer player : players) { + sendToFirstSpawn(context, player); + context.getSource().sendSuccess(() -> Component.literal("Sent ").append(player.getDisplayName()).append(" to first spawn."), true); + } + return players.size(); + } + } + + private void sendToFirstSpawn(CommandContext context, ServerPlayer player) { + var playerContext = PlaceholderContext.of(player); + context.getSource().sendSuccess(() -> module.locale().get( + "teleporting", + playerContext + ), false); + + module.getFirstSpawn().teleport(player); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("firstspawn", true)) + .executes(context -> execute(context, null)) + .then(argument("players", EntityArgument.players()) + .executes(context -> execute(context, EntityArgument.getPlayers(context, "players")))); + } + + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetFirstSpawnCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetFirstSpawnCommand.java new file mode 100644 index 0000000..6c1f510 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetFirstSpawnCommand.java @@ -0,0 +1,51 @@ +package me.alexdevs.solstice.modules.spawn.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class SetFirstSpawnCommand extends ModCommand { + public SetFirstSpawnCommand(SpawnModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("setfirstspawn"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("firstspawn.set", 2)) + .then(literal("delete") + .executes(this::executeDel)) + .executes(this::executeSet); + } + + private int executeDel(CommandContext context) { + var data = module.getServerData(); + data.firstSpawn = null; + + context.getSource().sendSuccess(() -> module.locale().get("firstSpawnDeleted"), true); + + return 1; + } + + private int executeSet(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var data = module.getServerData(); + data.firstSpawn = new ServerLocation(player); + + context.getSource().sendSuccess(() -> module.locale().get("firstSpawnSet"), true); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetSpawnCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetSpawnCommand.java new file mode 100644 index 0000000..b23f110 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SetSpawnCommand.java @@ -0,0 +1,47 @@ +package me.alexdevs.solstice.modules.spawn.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.literal; + +public class SetSpawnCommand extends ModCommand { + public SetSpawnCommand(SpawnModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("setspawn"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("set", 3)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var spawnPosition = new ServerLocation(player); + var world = player.serverLevel(); + + + world.setDefaultSpawnPos( + player.blockPosition(), + spawnPosition.getYaw() + ); + + context.getSource().sendSuccess(() -> module.locale().get("worldSpawnSet", Map.of( + "world", Component.nullToEmpty(world.dimension().location().toString()), + "coordinates", Component.nullToEmpty(String.format("%.1f %.1f %.1f", spawnPosition.getX(), spawnPosition.getY(), spawnPosition.getZ())) + )), true); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SpawnCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SpawnCommand.java new file mode 100644 index 0000000..f4f77de --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/commands/SpawnCommand.java @@ -0,0 +1,92 @@ +package me.alexdevs.solstice.modules.spawn.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.spawn.SpawnModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SpawnCommand extends ModCommand { + public SpawnCommand(SpawnModule module) { + super(module); + } + + private int execute(CommandContext context, @Nullable ServerLevel world, @Nullable Collection players) throws CommandSyntaxException { + var config = module.getConfig(); + var skipPermCheck = false; + if (world == null) { + if (config.globalSpawn.onSpawnCommand) { + world = module.getGlobalSpawnWorld(); + skipPermCheck = true; + } else { + world = context.getSource().getLevel(); + } + } + + var worldName = world.dimension().location().toString(); + + if(config.requireWorldPermission && !skipPermCheck) { + if (!Permissions.check(context.getSource(), getPermissionNode("worlds." + worldName), 2)) { + context.getSource().sendSuccess(() -> module.locale().get("noWorldPermission", Map.of("world", Component.nullToEmpty(worldName))), false); + return 0; + } + } + + if (players == null) { + var player = context.getSource().getPlayerOrException(); + sendToSpawn(context, player, world); + return 1; + } else { + for (ServerPlayer player : players) { + sendToSpawn(context, player, world); + context.getSource().sendSuccess(() -> Component.literal("Sent ").append(player.getDisplayName()).append(" to " + worldName + " spawn."), true); + } + return players.size(); + } + } + + private void sendToSpawn(CommandContext context, ServerPlayer player, ServerLevel world) { + var playerContext = PlaceholderContext.of(player); + context.getSource().sendSuccess(() -> module.locale().get( + "teleporting", + playerContext + ), false); + + module.sendToSpawn(player, world); + } + + @Override + public List getNames() { + return List.of("spawn"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> execute(context, null, null)) + .then(argument("world", DimensionArgument.dimension()) + .requires(require("worlds.base", true)) + .executes(context -> execute(context, DimensionArgument.getDimension(context, "world"), null)) + .then(argument("players", EntityArgument.players()) + .requires(require("others", 2)) + .executes(context -> execute(context, DimensionArgument.getDimension(context, "world"), EntityArgument.getPlayers(context, "players")))) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnConfig.java new file mode 100644 index 0000000..26499d9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnConfig.java @@ -0,0 +1,29 @@ +package me.alexdevs.solstice.modules.spawn.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class SpawnConfig { + + @Comment("Require that the player has the permission of the world 'solstice.spawn.worlds.' to warp to its spawn.\nMind that 'solstice.spawn.worlds.base' is required to be able to use the `world` argument.") + public boolean requireWorldPermission = true; + + @Comment("This setting defines whether `/spawn` and respawning work on a per world or global server basis.") + public GlobalSpawn globalSpawn = new GlobalSpawn(); + + @ConfigSerializable + public static class GlobalSpawn { + @Comment("Send the player to the global spawn instead of the world spawn when using the /spawn command.") + public boolean onSpawnCommand = false; + + @Comment("Send the player to the global spawn instead of their bed or anchor when respawning.") + public boolean onRespawn = false; + + @Comment("Send the player to the global spawn when logging in.") + public boolean onLogin = false; + + @Comment("ID of the world to use as global spawn. Minecraft dimensions: 'minecraft:overworld', 'minecraft:the_nether', 'minecraft:the_end'") + public String targetSpawnWorld = "minecraft:overworld"; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnLocale.java new file mode 100644 index 0000000..00d18cf --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnLocale.java @@ -0,0 +1,14 @@ +package me.alexdevs.solstice.modules.spawn.data; + +import java.util.Map; + +public class SpawnLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("teleporting", "Teleporting to spawn..."), + Map.entry("noWorldPermission", "You do not have permission to teleport to this world spawn."), + Map.entry("noFirstSpawn", "There is no first spawn yet."), + Map.entry("firstSpawnSet", "First spawn set!"), + Map.entry("firstSpawnDeleted", "First spawn deleted!"), + Map.entry("worldSpawnSet", "${world} spawn point set to ${coordinates}.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnServerData.java b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnServerData.java new file mode 100644 index 0000000..34a10a9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/spawn/data/SpawnServerData.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.spawn.data; + +import me.alexdevs.solstice.api.ServerLocation; +import org.jetbrains.annotations.Nullable; + +public class SpawnServerData { + @Deprecated + public @Nullable ServerLocation spawn; + + public @Nullable ServerLocation firstSpawn; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/staffChat/StaffChatModule.java b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/StaffChatModule.java new file mode 100644 index 0000000..c856a8b --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/StaffChatModule.java @@ -0,0 +1,69 @@ +package me.alexdevs.solstice.modules.staffChat; + +import eu.pb4.placeholders.api.node.TextNode; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.parser.MarkdownParser; +import me.alexdevs.solstice.modules.staffChat.commands.StaffChatCommand; +import me.alexdevs.solstice.modules.staffChat.data.StaffChatLocale; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class StaffChatModule extends ModuleBase.Toggleable { + public static final String ID = "staffchat"; + private final ConcurrentHashMap stickyStaffChat = new ConcurrentHashMap<>(); + + public StaffChatModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, StaffChatLocale.MODULE); + + commands.add(new StaffChatCommand(this)); + + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, player, pars) -> { + if (stickyStaffChat.getOrDefault(player.getUUID(), false) + && canUseStaffChat(player)) { + + sendStaffChatMessage(player.getDisplayName(), message.decoratedContent()); + + return false; + } + return true; + }); + } + + public boolean canUseStaffChat(ServerPlayer player) { + return Permissions.check(player, getPermissionNode("base"), 1); + } + + public void sendStaffChatMessage(Component sourceName, final Component message) { + var formattedMessage = MarkdownParser.defaultParser.parseNode(TextNode.convert(message)).toText(); + + + var text = Solstice.localeManager.getLocale(ID).get("message", Map.of( + "name", sourceName, + "message", formattedMessage + )); + + Solstice.server.sendSystemMessage(text); + Solstice.server.getPlayerList().getPlayers().forEach(player -> { + if (canUseStaffChat(player)) { + player.displayClientMessage(text, false); + } + }); + } + + public boolean toggleStaffChat(UUID uuid) { + var val = !stickyStaffChat.getOrDefault(uuid, false); + stickyStaffChat.put(uuid, val); + return val; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/staffChat/commands/StaffChatCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/commands/StaffChatCommand.java new file mode 100644 index 0000000..2045c59 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/commands/StaffChatCommand.java @@ -0,0 +1,46 @@ +package me.alexdevs.solstice.modules.staffChat.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.staffChat.StaffChatModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.MessageArgument; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class StaffChatCommand extends ModCommand { + public StaffChatCommand(StaffChatModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("staffchat", "sc"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(1)) + .executes(context -> { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var enabled = module.toggleStaffChat(player.getUUID()); + if (enabled) { + source.sendSuccess(() -> module.locale().get("enabled"), false); + } else { + source.sendSuccess(() -> module.locale().get("disabled"), false); + } + return 1; + }) + .then(argument("message", MessageArgument.message()) + .executes(context -> { + var message = MessageArgument.getMessage(context, "message"); + module.sendStaffChatMessage(context.getSource().getDisplayName(), message); + + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/staffChat/data/StaffChatLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/data/StaffChatLocale.java new file mode 100644 index 0000000..3a2f1b9 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/staffChat/data/StaffChatLocale.java @@ -0,0 +1,11 @@ +package me.alexdevs.solstice.modules.staffChat.data; + +import java.util.Map; + +public class StaffChatLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("message", "\uD83D\uDD27 ${name}: ${message}"), + Map.entry("enabled", "Staff chat enabled. All your messages will be sent to staff chat instead."), + Map.entry("disabled", "Staff chat disabled.") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/CustomSentMessage.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/CustomSentMessage.java new file mode 100644 index 0000000..68e3536 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/CustomSentMessage.java @@ -0,0 +1,64 @@ +package me.alexdevs.solstice.modules.styling; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.styling.formatters.ChatFormatter; +import me.alexdevs.solstice.modules.styling.formatters.EmoteFormatter; +import net.minecraft.network.chat.ChatType; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.OutgoingChatMessage; +import net.minecraft.network.chat.PlayerChatMessage; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +public interface CustomSentMessage extends OutgoingChatMessage { + + static OutgoingChatMessage of(PlayerChatMessage message, @Nullable ServerPlayer sender) { + if (message.isSystem() && sender == null) { + return new Profileless(message.decoratedContent()); + } + return new Chat(message, sender); + } + + record Profileless(Component getContent) implements OutgoingChatMessage { + @Override + public Component content() { + return getContent; + } + + @Override + public void sendToPlayer(ServerPlayer sender, boolean filterMaskEnabled, ChatType.Bound params) { + sender.connection.sendDisguisedChatMessage(this.getContent, params); + } + } + + record Chat(PlayerChatMessage message, ServerPlayer sender) implements OutgoingChatMessage { + @Override + public Component content() { + return this.message.decoratedContent(); + } + + @Override + public void sendToPlayer(ServerPlayer receiver, boolean filterMaskEnabled, ChatType.Bound params) { + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + if (ignoreModule.isEnabled() && ignoreModule.isIgnoring(receiver, sender)) { + return; + } + PlayerChatMessage signedMessage = this.message.filter(filterMaskEnabled); + if (!signedMessage.isFullyFiltered()) { + switch (params.chatType().value().chat().translationKey()) { + case "chat.type.text": + ChatFormatter.sendChatMessage(receiver, message, params, sender); + break; + case "chat.type.emote": + EmoteFormatter.sendEmoteMessage(receiver, message, params, sender); + break; + default: + receiver.connection.sendDisguisedChatMessage(this.message.decoratedContent(), params); + break; + } + } + + } + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/StylingModule.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/StylingModule.java new file mode 100644 index 0000000..550e914 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/StylingModule.java @@ -0,0 +1,41 @@ +package me.alexdevs.solstice.modules.styling; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.styling.data.StylingConfig; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.ChatType; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import me.alexdevs.solstice.api.text.Format; + +public class StylingModule extends ModuleBase.Toggleable { + public static final String ID = "styling"; + public static final String ADVANCED_CHAT_FORMATTING_PERMISSION = "solstice.chat.advanced"; + public static final ResourceKey CHAT_TYPE = ResourceKey.create(Registries.CHAT_TYPE, ResourceLocation.fromNamespaceAndPath(Solstice.MOD_ID, "chat")); + + public StylingModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, StylingConfig.class, StylingConfig::new); + + SolsticeEvents.WELCOME.register((player, server) -> { + var config = Solstice.configManager.getData(StylingConfig.class); + if (config.welcomeNewPlayers) { + var playerContext = PlaceholderContext.of(player); + Solstice.nextTick(() -> { + Solstice.getInstance().broadcast(Format.parse(getConfig().welcome, playerContext)); + }); + } + }); + } + + public StylingConfig getConfig() { + return Solstice.configManager.getData(StylingConfig.class); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/data/StylingConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/data/StylingConfig.java new file mode 100644 index 0000000..61e5dca --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/data/StylingConfig.java @@ -0,0 +1,51 @@ +package me.alexdevs.solstice.modules.styling.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.HashMap; +import java.util.Map; + +@ConfigSerializable +public class StylingConfig { + @Comment("Enable Markdown support in chat.") + public boolean enableMarkdown = true; + + @Comment("Whether to broadcast a welcome message to everyone when a player joins for the first time.") + public boolean welcomeNewPlayers = true; + + @Comment("Replace text chunks in chat messages.") + public HashMap replacements = new HashMap<>(Map.of( + ":shrug:", "¯\\\\_(ツ)_/¯" + )); + + @Comment("Task advancement format.") + public String advancementTask = " %player:displayname% completed the task ${title}"; + + @Comment("Challenge advancement format.") + public String advancementChallenge = "\uD83C\uDF86 %player:displayname% completed the challenge ${title}"; + + @Comment("Goal advancement format.") + public String advancementGoal = "\uD83C\uDF96 %player:displayname% completed the goal ${title}"; + + @Comment("Player chat format.") + public String chatFormat = "%player:displayname%: ${message}"; + + @Comment("Emote format (/me)") + public String emoteFormat = "\uD83D\uDC64 %player:displayname% ${message}"; + + @Comment("Player join format") + public String joinFormat = "+ %player:displayname% joined!"; + + @Comment("Player joined with a new username format") + public String joinRenamedFormat = "+ %player:displayname% joined! (Previously known as ${previousName})"; + + @Comment("Player quit format") + public String leaveFormat = "- %player:displayname% left!"; + + @Comment("Player death format") + public String deathFormat = "\u2620 ${message}"; + + @Comment("New player welcome message format") + public String welcome = "Welcome %player:displayname% to the server!"; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/AdvancementFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/AdvancementFormatter.java new file mode 100644 index 0000000..e798c2f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/AdvancementFormatter.java @@ -0,0 +1,38 @@ +package me.alexdevs.solstice.modules.styling.formatters; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.styling.StylingModule; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.advancements.AdvancementType; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import me.alexdevs.solstice.api.text.Format; +import java.util.Map; + +public class AdvancementFormatter { + public static Component getText(ServerPlayer player, AdvancementHolder entry, AdvancementType frame) { + var locale = Solstice.localeManager.getLocale(StylingModule.ID); + + var title = entry.value().display().get().getTitle(); + var description = entry.value().display().get().getDescription(); + + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + + String advancementFormat = switch (frame) { + case GOAL -> config.advancementGoal; + case CHALLENGE -> config.advancementChallenge; + case TASK -> config.advancementTask; + }; + + var playerContext = PlaceholderContext.of(player); + + Map placeholders = Map.of( + "frame", frame.getDisplayName(), + "title", title, + "description", description + ); + + return Format.parse(advancementFormat, playerContext, placeholders); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ChatFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ChatFormatter.java new file mode 100644 index 0000000..fcbe0e1 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ChatFormatter.java @@ -0,0 +1,36 @@ +package me.alexdevs.solstice.modules.styling.formatters; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.styling.StylingModule; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.ChatType; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.PlayerChatMessage; +import net.minecraft.server.level.ServerPlayer; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.api.text.Format; +import java.util.Map; + +public class ChatFormatter { + public static void sendChatMessage(ServerPlayer receiver, PlayerChatMessage message, ChatType.Bound params, ServerPlayer sender) { + var text = getFormattedMessage(message, sender); + + receiver.sendSystemMessage(text); + } + + public static Component getFormattedMessage(PlayerChatMessage message, ServerPlayer player) { + Component messageText = Components.chat(message, player); + + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + + var playerContext = PlaceholderContext.of(player); + return Format.parse( + config.chatFormat, + playerContext, + Map.of( + "message", messageText + ) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ConnectionActivityFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ConnectionActivityFormatter.java new file mode 100644 index 0000000..47fb572 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/ConnectionActivityFormatter.java @@ -0,0 +1,39 @@ +package me.alexdevs.solstice.modules.styling.formatters; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.styling.StylingModule; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import me.alexdevs.solstice.api.text.Format; +import java.util.Map; + +public class ConnectionActivityFormatter { + public static Component onJoin(ServerPlayer player) { + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + var playerContext = PlaceholderContext.of(player); + return Format.parse( + config.joinFormat, + playerContext + ); + } + + public static Component onJoinRenamed(ServerPlayer player, String previousName) { + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + var playerContext = PlaceholderContext.of(player); + return Format.parse( + config.joinRenamedFormat, + playerContext, + Map.of("previousName", Component.nullToEmpty(previousName)) + ); + } + + public static Component onLeave(ServerPlayer player) { + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + var playerContext = PlaceholderContext.of(player); + return Format.parse( + config.leaveFormat, + playerContext + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/DeathFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/DeathFormatter.java new file mode 100644 index 0000000..fc4fee3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/DeathFormatter.java @@ -0,0 +1,24 @@ +package me.alexdevs.solstice.modules.styling.formatters; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.styling.StylingModule; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.CombatTracker; +import me.alexdevs.solstice.api.text.Format; +import java.util.Map; + +public class DeathFormatter { + public static Component onDeath(ServerPlayer player, CombatTracker instance) { + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + var deathMessage = instance.getDeathMessage(); + var playerContext = PlaceholderContext.of(player); + + return Format.parse( + config.deathFormat, + playerContext, + Map.of("message", deathMessage) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/EmoteFormatter.java b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/EmoteFormatter.java new file mode 100644 index 0000000..8206fb7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/styling/formatters/EmoteFormatter.java @@ -0,0 +1,32 @@ +package me.alexdevs.solstice.modules.styling.formatters; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.modules.styling.StylingModule; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.ChatType; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.PlayerChatMessage; +import net.minecraft.server.level.ServerPlayer; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.api.text.Format; +import java.util.Map; + +public class EmoteFormatter { + public static void sendEmoteMessage(ServerPlayer receiver, PlayerChatMessage message, ChatType.Bound params, ServerPlayer sender) { + var config = Solstice.modules.getModule(StylingModule.class).getConfig(); + var playerContext = PlaceholderContext.of(sender); + + Component messageText = Components.chat(message, sender); + + var text = Format.parse( + config.emoteFormat, + playerContext, + Map.of( + "message", messageText + ) + ); + + receiver.sendSystemMessage(text); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/sudo/SudoModule.java b/common/src/main/java/me/alexdevs/solstice/modules/sudo/SudoModule.java new file mode 100644 index 0000000..d64c570 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/sudo/SudoModule.java @@ -0,0 +1,19 @@ +package me.alexdevs.solstice.modules.sudo; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.sudo.commands.DoAsCommand; +import me.alexdevs.solstice.modules.sudo.commands.SudoCommand; + +public class SudoModule extends ModuleBase.Toggleable { + public static final String ID = "sudo"; + + public SudoModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new SudoCommand(this)); + commands.add(new DoAsCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/DoAsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/DoAsCommand.java new file mode 100644 index 0000000..a287c21 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/DoAsCommand.java @@ -0,0 +1,93 @@ +package me.alexdevs.solstice.modules.sudo.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.sudo.SudoModule; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class DoAsCommand extends ModCommand { + public DoAsCommand(SudoModule module) { + super(module); + } + + public static void execute(CommandDispatcher dispatcher, String command, CommandSourceStack source, CommandSourceStack output) { + try { + dispatcher.execute(command, source); + } catch (Exception e) { + output.sendFailure(Component.nullToEmpty(String.format("[%s] %s", source.getTextName(), e.getMessage()))); + } + } + + public static CommandSourceStack buildPlayerSource(CommandSource commandOutput, MinecraftServer server, ServerPlayer player) { + var opList = server.getPlayerList().getOps(); + var operator = opList.get(player.getGameProfile()); + int opLevel = 0; + if (operator != null) { + opLevel = operator.getLevel(); + } + return new CommandSourceStack( + commandOutput, + player.position(), + player.getRotationVector(), + player.serverLevel(), + opLevel, + player.getScoreboardName(), + player.getDisplayName(), + server, + player + ); + } + + @Override + public List getNames() { + return List.of("doas"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("doas", 4)) + .then(argument("player", EntityArgument.players()) + .then(argument("command", StringArgumentType.greedyString()) + .executes(context -> { + var players = EntityArgument.getPlayers(context, "player"); + var profileArgRange = context.getNodes().get(1).getRange(); + var stringProfiles = context.getInput().substring( + profileArgRange.getStart(), + profileArgRange.getEnd() + ); + + var command = StringArgumentType.getString(context, "command"); + + context.getSource().sendSuccess(() -> Component.literal(String.format("Executing '%s' as %s", command, stringProfiles)), true); + + CommandSource commandOutput; + if (context.getSource().isPlayer()) { + commandOutput = context.getSource().getPlayer(); + } else { + commandOutput = context.getSource().getServer(); + } + + var server = context.getSource().getServer(); + for (var player : players) { + var source = buildPlayerSource(commandOutput, server, player); + execute(dispatcher, command, source, context.getSource()); + } + + return 1; + }) + ) + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/SudoCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/SudoCommand.java new file mode 100644 index 0000000..da8f1c4 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/sudo/commands/SudoCommand.java @@ -0,0 +1,82 @@ +package me.alexdevs.solstice.modules.sudo.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.sudo.SudoModule; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.phys.Vec2; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SudoCommand extends ModCommand { + public SudoCommand(SudoModule module) { + super(module); + } + + public static void execute(CommandDispatcher dispatcher, String command, CommandSourceStack source, CommandSourceStack output) { + try { + dispatcher.execute(command, source); + } catch (Exception e) { + output.sendFailure(Component.nullToEmpty(String.format("[%s] %s", source.getTextName(), e.getMessage()))); + } + } + + @Override + public List getNames() { + return List.of("sudo"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .then(argument("command", StringArgumentType.greedyString()) + .executes(context -> { + if (!Permissions.check(context.getSource(), getPermissionNode("sudo"), 4)) { + context.getSource().sendFailure(Component.literal(String.format("%s is not in the sudoers file. This incident will be reported.", context.getSource().getTextName())) + .setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://xkcd.com/838/")))); + return 1; + } + var command = StringArgumentType.getString(context, "command"); + + context.getSource().sendSuccess(() -> Component.literal(String.format("Executing '%s' as Server", command)), true); + + CommandSource commandOutput; + if (context.getSource().isPlayer()) { + commandOutput = context.getSource().getPlayer(); + } else { + commandOutput = context.getSource().getServer(); + } + + var server = context.getSource().getServer(); + var source = buildServerSource(commandOutput, server); + execute(dispatcher, command, source, context.getSource()); + + return 1; + }) + ); + } + + public CommandSourceStack buildServerSource(CommandSource commandOutput, MinecraftServer server) { + return new CommandSourceStack( + commandOutput, + server.overworld().getSharedSpawnPos().getCenter(), + Vec2.ZERO, + server.overworld(), + 4, + "Server", + Component.nullToEmpty("Server"), + server, + null + ); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/suicide/SuicideModule.java b/common/src/main/java/me/alexdevs/solstice/modules/suicide/SuicideModule.java new file mode 100644 index 0000000..f38b5ca --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/suicide/SuicideModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.suicide; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.suicide.commands.SuicideCommand; + +public class SuicideModule extends ModuleBase.Toggleable { + public static final String ID = "suicide"; + + public SuicideModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new SuicideCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/suicide/commands/SuicideCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/suicide/commands/SuicideCommand.java new file mode 100644 index 0000000..d1b7943 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/suicide/commands/SuicideCommand.java @@ -0,0 +1,33 @@ +package me.alexdevs.solstice.modules.suicide.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.suicide.SuicideModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class SuicideCommand extends ModCommand { + public SuicideCommand(SuicideModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("suicide"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + player.kill(); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tablist/TabListModule.java b/common/src/main/java/me/alexdevs/solstice/modules/tablist/TabListModule.java new file mode 100644 index 0000000..4067cbc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tablist/TabListModule.java @@ -0,0 +1,73 @@ +package me.alexdevs.solstice.modules.tablist; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.SolsticeEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Format; +import me.alexdevs.solstice.api.text.RawPlaceholder; +import me.alexdevs.solstice.modules.tablist.data.TabListConfig; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.network.protocol.game.ClientboundTabListPacket; +import net.minecraft.server.MinecraftServer; + +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class TabListModule extends ModuleBase.Toggleable { + public static final String ID = "tablist"; + + private MinecraftServer server; + private ScheduledFuture scheduledFuture = null; + + public TabListModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, TabListConfig.class, TabListConfig::new); + + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + this.server = server; + schedule(); + }); + + SolsticeEvents.RELOAD.register(instance -> { + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + schedule(); + }); + } + + private void schedule() { + var config = Solstice.configManager.getData(TabListConfig.class); + if (!config.enable) + return; + + scheduledFuture = Solstice.scheduler.scheduleAtFixedRate(this::updateTab, 0, config.delay, TimeUnit.MILLISECONDS); + } + + public void updateTab() { + var config = Solstice.configManager.getData(TabListConfig.class); + var period = Math.max(config.phasePeriod, 1); + + var phase = (float) (Math.sin((server.getTickCount() * Math.PI * 2) / period) + 1) / 2f; + + var placeholders = Map.of( + "phase", String.valueOf(phase) + ); + + server.getPlayerList().getPlayers().forEach(player -> { + var playerContext = PlaceholderContext.of(player); + var header = RawPlaceholder.parse(String.join("\n", config.header), placeholders); + var footer = RawPlaceholder.parse(String.join("\n", config.footer), placeholders); + player.connection.send(new ClientboundTabListPacket( + Format.parse(header, playerContext), + Format.parse(footer, playerContext) + )); + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tablist/data/TabListConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/tablist/data/TabListConfig.java new file mode 100644 index 0000000..c7b60b2 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tablist/data/TabListConfig.java @@ -0,0 +1,32 @@ +package me.alexdevs.solstice.modules.tablist.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; + +@ConfigSerializable +public class TabListConfig { + @Comment("Enable the custom tab list functionality.") + public boolean enable = true; + + @Comment("Send tab list updates every X milliseconds. Defaults to 250 ms.") + public int delay = 250; + + @Comment("How fast the phase is. Lower is faster. Defaults to 300") + public double phasePeriod = 300; + + @Comment("Header lines") + public ArrayList header = new ArrayList<>(List.of( + " " + )); + + @Comment("Footer lines") + public ArrayList footer = new ArrayList<>(List.of( + " " + )); + + @Comment("Format to use when displaying the player name in the tab list.") + public String playerTabName = "%solstice:afk%%player:displayname_visual%"; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/TeleportHereModule.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/TeleportHereModule.java new file mode 100644 index 0000000..b7e59fc --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/TeleportHereModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.teleportHere; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.teleportHere.commands.TeleportHereCommand; + +public class TeleportHereModule extends ModuleBase.Toggleable { + public static final String ID = "teleporthere"; + + public TeleportHereModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new TeleportHereCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/commands/TeleportHereCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/commands/TeleportHereCommand.java new file mode 100644 index 0000000..1042e75 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportHere/commands/TeleportHereCommand.java @@ -0,0 +1,59 @@ +package me.alexdevs.solstice.modules.teleportHere.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.teleportHere.TeleportHereModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.PathfinderMob; +import java.util.List; +import java.util.Set; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportHereCommand extends ModCommand { + public TeleportHereCommand(TeleportHereModule module) { + super(module); + } + + public List getNames() { + return List.of("tphere"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("targets", EntityArgument.entities()) + .executes(context -> { + var source = context.getSource(); + var player = source.getPlayerOrException(); + var world = player.serverLevel(); + var vec3d = player.position(); + var yaw = player.getYRot(); + var pitch = player.getXRot(); + + var targets = EntityArgument.getEntities(context, "targets"); + + targets.forEach(target -> { + target.teleportTo(world, vec3d.x, vec3d.y, vec3d.z, Set.of(), yaw, pitch); + target.setDeltaMovement(target.getDeltaMovement().multiply(1.0, 0.0, 1.0)); + target.setOnGround(true); + + if (target instanceof PathfinderMob pathAwareEntity) { + pathAwareEntity.getNavigation().stop(); + } + }); + + if (targets.size() == 1) { + source.sendSuccess(() -> Component.translatable("commands.teleport.success.entity.single", targets.iterator().next().getDisplayName(), player.getDisplayName()), true); + } else { + source.sendSuccess(() -> Component.translatable("commands.teleport.success.entity.multiple", targets.size(), player.getDisplayName()), true); + } + + return targets.size(); + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/TeleportOfflineModule.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/TeleportOfflineModule.java new file mode 100644 index 0000000..5de4327 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/TeleportOfflineModule.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.teleportOffline; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.teleportOffline.commands.TeleportOfflineCommand; + +public class TeleportOfflineModule extends ModuleBase.Toggleable { + public static final String ID = "teleportoffline"; + + public TeleportOfflineModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new TeleportOfflineCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/commands/TeleportOfflineCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/commands/TeleportOfflineCommand.java new file mode 100644 index 0000000..457aad3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportOffline/commands/TeleportOfflineCommand.java @@ -0,0 +1,52 @@ +package me.alexdevs.solstice.modules.teleportOffline.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.LocalGameProfile; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.core.coreModule.data.CorePlayerData; +import me.alexdevs.solstice.modules.teleportOffline.TeleportOfflineModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; + +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportOfflineCommand extends ModCommand { + public TeleportOfflineCommand(TeleportOfflineModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tpoffline", "tpoff"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .then(argument("player", StringArgumentType.word()) + .suggests(LocalGameProfile::suggest) + .executes(context -> { + var source = context.getSource(); + var player = source.getPlayerOrException(); + + var gameProfile = LocalGameProfile.getProfile(context, "player"); + + var targetData = Solstice.playerData.get(gameProfile).getData(CorePlayerData.class); + if (targetData == null || targetData.logoffPosition == null) { + source.sendFailure(Component.nullToEmpty("Could not find location of offline player")); + return 0; + } + + source.sendSuccess(() -> Component.translatable("commands.teleport.success.entity.single", player.getDisplayName(), Component.nullToEmpty(gameProfile.getName())), true); + + targetData.logoffPosition.teleport(player, true); + return 1; + })); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/TeleportPositionModule.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/TeleportPositionModule.java new file mode 100644 index 0000000..d7ddba7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/TeleportPositionModule.java @@ -0,0 +1,16 @@ +package me.alexdevs.solstice.modules.teleportPosition; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.teleportPosition.commands.TeleportPositionCommand; + +public class TeleportPositionModule extends ModuleBase.Toggleable { + public static final String ID = "teleportposition"; + public TeleportPositionModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new TeleportPositionCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/commands/TeleportPositionCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/commands/TeleportPositionCommand.java new file mode 100644 index 0000000..6f7a384 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportPosition/commands/TeleportPositionCommand.java @@ -0,0 +1,74 @@ +package me.alexdevs.solstice.modules.teleportPosition.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.teleportPosition.TeleportPositionModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import java.util.List; +import java.util.Locale; + +public class TeleportPositionCommand extends ModCommand { + public static final SimpleCommandExceptionType INVALID_POSITION_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.teleport.invalidPosition")); + + public TeleportPositionCommand(TeleportPositionModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tppos"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return Commands.literal(name) + .requires(require(2)) + .then(Commands.argument("coordinates", Vec3Argument.vec3()) + .executes(context -> execute(context, context.getSource().getLevel())) + .then(Commands.argument("dimension", DimensionArgument.dimension()) + .executes(context -> execute(context, DimensionArgument.getDimension(context, "dimension"))) + ) + ); + } + + private static String formatFloat(double d) { + return String.format(Locale.ROOT, "%f", d); + } + + private int execute(CommandContext context, ServerLevel world) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var coords = Vec3Argument.getVec3(context, "coordinates"); + + var blockPos = BlockPos.containing(coords.x(), coords.y(), coords.z()); + if (!Level.isInSpawnableBounds(blockPos)) { + throw INVALID_POSITION_EXCEPTION.create(); + } + + var location = new ServerLocation( + coords.x(), coords.y(), coords.z(), + player.getYRot(), player.getXRot(), + world + ); + + context.getSource().sendSuccess(() -> + Component.translatable("commands.teleport.success.location.single", + player.getDisplayName(), + formatFloat(coords.x), formatFloat(coords.y), formatFloat(coords.z)), + true); + + location.teleport(player); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/TeleportRequestModule.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/TeleportRequestModule.java new file mode 100644 index 0000000..6768ac0 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/TeleportRequestModule.java @@ -0,0 +1,172 @@ +package me.alexdevs.solstice.modules.teleportRequest; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.api.text.Components; +import me.alexdevs.solstice.modules.notifications.NotificationsModule; +import me.alexdevs.solstice.modules.teleportRequest.commands.TeleportAcceptCommand; +import me.alexdevs.solstice.modules.teleportRequest.commands.TeleportAskCommand; +import me.alexdevs.solstice.modules.teleportRequest.commands.TeleportAskHereCommand; +import me.alexdevs.solstice.modules.teleportRequest.commands.TeleportDenyCommand; +import me.alexdevs.solstice.modules.teleportRequest.data.Request; +import me.alexdevs.solstice.modules.teleportRequest.data.TeleportConfig; +import me.alexdevs.solstice.modules.teleportRequest.data.TeleportLocale; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.level.ServerPlayer; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.TimeUnit; + +public class TeleportRequestModule extends ModuleBase.Toggleable { + public static final String ID = "teleportrequest"; + + private final Map> requests = new ConcurrentHashMap<>(); + + public TeleportRequestModule() { + super(ID); + } + + @Override + public void init() { + Solstice.configManager.registerData(ID, TeleportConfig.class, TeleportConfig::new); + Solstice.localeManager.registerModule(ID, TeleportLocale.MODULE); + + commands.add(new TeleportAcceptCommand(this)); + commands.add(new TeleportAskCommand(this)); + commands.add(new TeleportAskHereCommand(this)); + commands.add(new TeleportDenyCommand(this)); + + Solstice.scheduler.scheduleAtFixedRate(this::tickDown, 0, 1, TimeUnit.SECONDS); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> requests.put(handler.getPlayer().getUUID(), new ConcurrentLinkedDeque<>())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> Solstice.nextTick(() -> requests.remove(handler.getPlayer().getUUID()))); + } + + private void tickDown() { + for (var entry : requests.entrySet()) { + entry.getValue().removeIf(Request::tickDown); + } + } + + public TeleportConfig getConfig() { + return Solstice.configManager.getData(TeleportConfig.class); + } + + public Request getRequestFromSource(ServerPlayer player, ServerPlayer source) { + return requests.get(player.getUUID()) + .stream() + .filter(r -> r.getSource().getUUID().equals(source.getUUID())) + .findFirst() + .orElse(null); + } + + public Request getLatestRequest(ServerPlayer player) { + var reqs = requests.get(player.getUUID()); + if (reqs.isEmpty()) + return null; + + return reqs.getLast(); + } + + public void acceptRequest(ServerPlayer player, Request request) { + requests.get(player.getUUID()).remove(request); + var source = request.getSource(); + var direction = request.getDirection(); + + var map = Map.of( + "player", player.getDisplayName() + ); + + player.sendSystemMessage(locale().get("targetAccepted")); + source.sendSystemMessage(locale().get("sourceAccepted", map)); + + if (direction == Request.Direction.SOURCE_TO_TARGET) { + var location = new ServerLocation(player); + location.teleport(source); + } else { + var location = new ServerLocation(source); + location.teleport(player); + } + } + + public void refuseRequest(ServerPlayer player, Request request) { + requests.get(player.getUUID()).remove(request); + var source = request.getSource(); + + var map = Map.of( + "player", player.getDisplayName() + ); + + player.sendSystemMessage(locale().get("targetRefused")); + source.sendSystemMessage(locale().get("sourceRefused", map)); + } + + public void requestTo(ServerPlayer source, ServerPlayer target) { + var request = new Request(source, getConfig().teleportRequestTimeout, Request.Direction.SOURCE_TO_TARGET); + requests.computeIfAbsent(target.getUUID(), uuid -> new ConcurrentLinkedDeque<>()).add(request); + + var sourceContext = PlaceholderContext.of(source); + var targetContext = PlaceholderContext.of(target); + + var placeholders = Map.of( + "requesterPlayer", source.getDisplayName(), + "acceptButton", Components.button( + locale().raw("~accept"), + locale().raw("~accept.hover"), + "/tpaccept " + source.getGameProfile().getName()), + "refuseButton", Components.button( + locale().raw("~refuse"), + locale().raw("~refuse.hover"), + "/tpdeny " + source.getGameProfile().getName()) + ); + + target.sendSystemMessage(locale().get( + "pendingTeleport", + targetContext, + placeholders + )); + + source.sendSystemMessage(locale().get( + "requestSent", + sourceContext + )); + + NotificationsModule.notify(target); + } + + public void requestToHere(ServerPlayer source, ServerPlayer target) { + var request = new Request(source, getConfig().teleportRequestTimeout, Request.Direction.TARGET_TO_SOURCE); + requests.computeIfAbsent(target.getUUID(), uuid -> new ConcurrentLinkedDeque<>()).add(request); + + var sourceContext = PlaceholderContext.of(source); + var targetContext = PlaceholderContext.of(target); + var placeholders = Map.of( + "requesterPlayer", source.getDisplayName(), + "acceptButton", Components.button( + locale().raw("~accept"), + locale().raw("~accept.hover"), + "/tpaccept " + source.getGameProfile().getName()), + "refuseButton", Components.button( + locale().raw("~refuse"), + locale().raw("~refuse.hover"), + "/tpdeny " + source.getGameProfile().getName()) + ); + + target.sendSystemMessage(locale().get( + "pendingTeleportHere", + targetContext, + placeholders + )); + + source.sendSystemMessage(locale().get( + "requestSent", + sourceContext + )); + + NotificationsModule.notify(target); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAcceptCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAcceptCommand.java new file mode 100644 index 0000000..b31883c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAcceptCommand.java @@ -0,0 +1,61 @@ +package me.alexdevs.solstice.modules.teleportRequest.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.teleportRequest.TeleportRequestModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportAcceptCommand extends ModCommand { + public TeleportAcceptCommand(TeleportRequestModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tpaccept", "tpyes"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(this::execute) + .then(argument("player", EntityArgument.player()) + .executes(context -> this.execute(context, EntityArgument.getPlayer(context, "player"))) + ); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var request = module.getLatestRequest(player); + if (request == null) { + context.getSource().sendSuccess(() -> module.locale().get("noPending"), false); + return 0; + } + module.acceptRequest(player, request); + + return 1; + } + + private int execute(CommandContext context, ServerPlayer source) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var request = module.getRequestFromSource(player, source); + if (request == null) { + context.getSource().sendSuccess(() -> module.locale().get("unavailable"), false); + return 0; + } + module.acceptRequest(player, request); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskCommand.java new file mode 100644 index 0000000..3cdcaba --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskCommand.java @@ -0,0 +1,48 @@ +package me.alexdevs.solstice.modules.teleportRequest.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.teleportRequest.TeleportRequestModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportAskCommand extends ModCommand { + public TeleportAskCommand(TeleportRequestModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tpa", "tpask"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("ask", true)) + .then(argument("player", EntityArgument.player()) + .executes(this::execute)); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var target = EntityArgument.getPlayer(context, "player"); + + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + if (ignoreModule.getPlayerData(target.getUUID()).ignoredPlayers.contains(player.getUUID())) { + return 0; + } + + module.requestTo(player, target); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskHereCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskHereCommand.java new file mode 100644 index 0000000..174a37d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportAskHereCommand.java @@ -0,0 +1,48 @@ +package me.alexdevs.solstice.modules.teleportRequest.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.teleportRequest.TeleportRequestModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportAskHereCommand extends ModCommand { + public TeleportAskHereCommand(TeleportRequestModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tpahere", "tpaskhere"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("here", true)) + .then(argument("player", EntityArgument.player()) + .executes(this::execute)); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var target = EntityArgument.getPlayer(context, "player"); + + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + if (ignoreModule.getPlayerData(target.getUUID()).ignoredPlayers.contains(player.getUUID())) { + return 0; + } + + module.requestToHere(player, target); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportDenyCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportDenyCommand.java new file mode 100644 index 0000000..f033cde --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/commands/TeleportDenyCommand.java @@ -0,0 +1,61 @@ +package me.alexdevs.solstice.modules.teleportRequest.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.teleportRequest.TeleportRequestModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TeleportDenyCommand extends ModCommand { + public TeleportDenyCommand(TeleportRequestModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("tpdeny", "tpno", "tprefuse"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(this::execute) + .then(argument("player", EntityArgument.player()) + .executes(context -> this.execute(context, EntityArgument.getPlayer(context, "player"))) + ); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var request = module.getLatestRequest(player); + if (request == null) { + context.getSource().sendSuccess(() -> module.locale().get("noPending"), false); + return 0; + } + module.refuseRequest(player, request); + + return 1; + } + + private int execute(CommandContext context, ServerPlayer source) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + + var request = module.getRequestFromSource(player, source); + if (request == null) { + context.getSource().sendSuccess(() -> module.locale().get("unavailable"), false); + return 0; + } + module.refuseRequest(player, request); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/Request.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/Request.java new file mode 100644 index 0000000..d6df428 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/Request.java @@ -0,0 +1,37 @@ +package me.alexdevs.solstice.modules.teleportRequest.data; + +import net.minecraft.server.level.ServerPlayer; + +public class Request { + public enum Direction { + SOURCE_TO_TARGET, + TARGET_TO_SOURCE, + } + + private final ServerPlayer source; + private int remainingTime; + private final Direction direction; + + public Request(ServerPlayer source, int remainingTime, Direction direction) { + this.source = source; + this.remainingTime = remainingTime; + this.direction = direction; + } + + public ServerPlayer getSource() { + return source; + } + + public int getRemainingTime() { + return remainingTime; + } + + public boolean tickDown() { + return remainingTime-- <= 0; + } + + public Direction getDirection() { + return direction; + } + +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportConfig.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportConfig.java new file mode 100644 index 0000000..ac45139 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportConfig.java @@ -0,0 +1,10 @@ +package me.alexdevs.solstice.modules.teleportRequest.data; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class TeleportConfig { + @Comment("The teleport request times out after the following seconds. Defaults to 120 seconds.") + public int teleportRequestTimeout = 120; +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportLocale.java new file mode 100644 index 0000000..8ae1bd4 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/teleportRequest/data/TeleportLocale.java @@ -0,0 +1,20 @@ +package me.alexdevs.solstice.modules.teleportRequest.data; + +import java.util.Map; + +public class TeleportLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("teleporting", "Teleporting..."), + Map.entry("requestSent", "Teleport request sent."), + Map.entry("pendingTeleport", "${requesterPlayer} requested to teleport to you.\n ${acceptButton} ${refuseButton}"), + Map.entry("pendingTeleportHere", "${requesterPlayer} requested you to teleport to them.\n ${acceptButton} ${refuseButton}"), + Map.entry("noPending", "There are no pending teleport requests for you."), + Map.entry("unavailable", "This request expired or is no longer available."), + Map.entry("playerUnavailable", "The other player is no longer available."), + + Map.entry("targetAccepted", "Teleport request accepted."), + Map.entry("sourceAccepted", "${player} accepted your teleport request!"), + Map.entry("targetRefused", "Teleport request refused."), + Map.entry("sourceRefused", "${player} refused your teleport request!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tell/TellModule.java b/common/src/main/java/me/alexdevs/solstice/modules/tell/TellModule.java new file mode 100644 index 0000000..b09b042 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tell/TellModule.java @@ -0,0 +1,143 @@ +package me.alexdevs.solstice.modules.tell; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.ignore.IgnoreModule; +import me.alexdevs.solstice.modules.notifications.NotificationsModule; +import me.alexdevs.solstice.modules.tell.commands.ReplyCommand; +import me.alexdevs.solstice.modules.tell.commands.TellCommand; +import me.alexdevs.solstice.modules.tell.data.TellLocale; +import me.alexdevs.solstice.api.text.Components; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.util.HashMap; +import java.util.Map; + +public class TellModule extends ModuleBase.Toggleable { + public static final String ID = "tell"; + public final HashMap lastSender = new HashMap<>(); + + public TellModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, TellLocale.MODULE); + + commands.add(new TellCommand(this)); + commands.add(new ReplyCommand(this)); + } + + public void sendDirectMessage(String targetName, CommandSourceStack source, String message) { + var locale = Solstice.localeManager.getLocale(ID); + Component targetDisplayName; + ServerPlayer targetPlayer = null; + if (targetName.equalsIgnoreCase("server")) { + targetDisplayName = Component.nullToEmpty("Server"); + } else { + targetPlayer = source.getServer().getPlayerList().getPlayerByName(targetName); + if (targetPlayer == null) { + var placeholders = Map.of( + "targetPlayer", Component.nullToEmpty(targetName) + ); + var sourceContext = PlaceholderContext.of(source); + + source.sendSuccess(() -> locale.get( + "playerNotFound", + sourceContext, + placeholders + ), false); + return; + } + targetDisplayName = targetPlayer.getDisplayName(); + } + + var parsedMessage = Components.chat(message, source); + + var serverContext = PlaceholderContext.of(source.getServer()); + var sourceContext = PlaceholderContext.of(source); + PlaceholderContext targetContext; + if (targetPlayer == null) { + targetContext = serverContext; + } else { + targetContext = PlaceholderContext.of(targetPlayer); + } + + + var you = locale.get("you"); + + var placeholdersToSource = Map.of( + "sourcePlayer", you, + "targetPlayer", targetDisplayName, + "message", parsedMessage + ); + + var placeholdersToTarget = Map.of( + "sourcePlayer", source.getDisplayName(), + "targetPlayer", you, + "message", parsedMessage + ); + + var placeholders = Map.of( + "sourcePlayer", source.getDisplayName(), + "targetPlayer", targetDisplayName, + "message", parsedMessage + ); + + var sourceText = locale.get( + "message", + sourceContext, + placeholdersToSource + ); + var targetText = locale.get( + "message", + targetContext, + placeholdersToTarget + ); + var genericText = locale.get( + "message", + serverContext, + placeholders + ); + var spyText = locale.get( + "messageSpy", + serverContext, + placeholders + ); + + lastSender.put(targetName, source.getTextName()); + lastSender.put(source.getTextName(), targetName); + + if (!source.getTextName().equals(targetName)) { + source.sendSystemMessage(sourceText); + } + if (targetPlayer != null) { + var ignoreModule = Solstice.modules.getModule(IgnoreModule.class); + if (!source.isPlayer() || !ignoreModule.isIgnoring(targetPlayer, source.getPlayer())) { + targetPlayer.sendSystemMessage(targetText); + NotificationsModule.notify(targetPlayer); + } + + if (source.isPlayer()) { + source.getServer().sendSystemMessage(genericText); + } + } else { + // avoid duped message + source.getServer().sendSystemMessage(targetText); + } + + source.getServer().getPlayerList().getPlayers().forEach(player -> { + var playerName = player.getGameProfile().getName(); + if (playerName.equals(targetName) || playerName.equals(source.getTextName())) { + return; + } + if (Permissions.check(player, getPermissionNode("spy"))) { + player.sendSystemMessage(spyText); + } + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/ReplyCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/ReplyCommand.java new file mode 100644 index 0000000..5b6c5b3 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/ReplyCommand.java @@ -0,0 +1,54 @@ +package me.alexdevs.solstice.modules.tell.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.tell.TellModule; +import net.minecraft.commands.CommandSourceStack; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + + +public class ReplyCommand extends ModCommand { + public ReplyCommand(TellModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("reply", "r"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(this::execute)); + } + + private int execute(CommandContext context) { + var source = context.getSource(); + var senderName = source.getTextName(); + var message = StringArgumentType.getString(context, "message"); + + if (!module.lastSender.containsKey(senderName)) { + var playerContext = PlaceholderContext.of(context.getSource()); + source.sendSuccess(() -> module.locale().get( + "noLastSenderReply", + playerContext + ), false); + return 0; + } + + var targetName = module.lastSender.get(senderName); + + module.sendDirectMessage(targetName, source, message); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/TellCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/TellCommand.java new file mode 100644 index 0000000..7fe1708 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tell/commands/TellCommand.java @@ -0,0 +1,59 @@ +package me.alexdevs.solstice.modules.tell.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.api.module.Utils; +import me.alexdevs.solstice.modules.tell.TellModule; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import java.util.List; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + + +public class TellCommand extends ModCommand { + public TellCommand(TellModule module) { + super(module); + } + + @Override + public void register(CommandDispatcher dispatcher, CommandBuildContext commandRegistry, Commands.CommandSelection environment) { + Utils.removeCommands(dispatcher, "msg", "tell", "w"); + super.register(dispatcher, commandRegistry, environment); + } + + @Override + public List getNames() { + return List.of("tell", "msg", "w", "dm"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("player", StringArgumentType.word()) + .suggests((context, builder) -> { + var playerManager = context.getSource().getServer().getPlayerList(); + return SharedSuggestionProvider.suggest( + playerManager.getPlayerNamesArray(), + builder); + }) + .then(argument("message", StringArgumentType.greedyString()) + .executes(this::execute))); + } + + private int execute(CommandContext context) { + var source = context.getSource(); + var targetName = StringArgumentType.getString(context, "player"); + var message = StringArgumentType.getString(context, "message"); + + module.sendDirectMessage(targetName, source, message); + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/tell/data/TellLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/tell/data/TellLocale.java new file mode 100644 index 0000000..40dbe8f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/tell/data/TellLocale.java @@ -0,0 +1,13 @@ +package me.alexdevs.solstice.modules.tell.data; + +import java.util.Map; + +public class TellLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("playerNotFound", "Player ${targetPlayer} not found!"), + Map.entry("you", "You"), + Map.entry("message", "[${sourcePlayer} ${targetPlayer}] ${message}"), + Map.entry("messageSpy", "\uD83D\uDC41 [${sourcePlayer} → ${targetPlayer}] ${message}"), + Map.entry("noLastSenderReply", "You have no one to reply to.") // relatable + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBar.java b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBar.java new file mode 100644 index 0000000..70262f5 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBar.java @@ -0,0 +1,105 @@ +package me.alexdevs.solstice.modules.timeBar; + +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.text.Format; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.bossevents.CustomBossEvent; +import net.minecraft.world.BossEvent; +import java.util.Map; +import java.util.UUID; + +public class TimeBar { + private final UUID uuid = UUID.randomUUID(); + private final CustomBossEvent bossBar; + private final String label; + private final int time; + private final boolean countdown; + private int elapsedSeconds = 0; + + public TimeBar(String label, int time, boolean countdown, BossEvent.BossBarColor color, BossEvent.BossBarOverlay style) { + this.bossBar = new CustomBossEvent(ResourceLocation.tryBuild(Solstice.MOD_ID, uuid.toString()), Component.nullToEmpty(label)); + this.bossBar.setColor(color); + this.bossBar.setOverlay(style); + this.label = label; + this.time = time; + this.countdown = countdown; + updateName(); + updateProgress(); + } + + public void updateName() { + var text = parseLabel(label); + bossBar.setName(text); + } + + public Component parseLabel(String labelString) { + var totalTime = TimeSpan.toLongString(this.time); + var elapsedTime = TimeSpan.toLongString(this.elapsedSeconds); + + var remaining = getRemainingSeconds(); + var remainingTime = TimeSpan.toLongString(remaining); + + var placeholders = Map.of( + "total_time", Component.nullToEmpty(totalTime), + "elapsed_time", Component.nullToEmpty(elapsedTime), + "remaining_time", Component.nullToEmpty(remainingTime) + ); + + var serverContext = PlaceholderContext.of(Solstice.server); + + return Format.parse(labelString, serverContext, placeholders); + } + + public UUID getUuid() { + return uuid; + } + + public CustomBossEvent getBossBar() { + return bossBar; + } + + public String getLabel() { + return label; + } + + public int getTime() { + return time; + } + + public int getElapsedSeconds() { + return elapsedSeconds; + } + + public int getRemainingSeconds() { + return time - elapsedSeconds; + } + + public boolean isCountdown() { + return countdown; + } + + public boolean elapse() { + this.elapsedSeconds++; + + updateProgress(); + updateName(); + + return this.elapsedSeconds >= this.time; + } + + private void updateProgress() { + float progress = (float) elapsedSeconds / (float) time; + if (countdown) { + progress = 1f - progress; + } + + bossBar.setProgress(Math.min( + Math.max( + progress, + 0f), + 1f)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBarModule.java b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBarModule.java new file mode 100644 index 0000000..604f70f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/TimeBarModule.java @@ -0,0 +1,90 @@ +package me.alexdevs.solstice.modules.timeBar; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.events.TimeBarEvents; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.timeBar.commands.TimeBarCommand; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.BossEvent; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.TimeUnit; + +public class TimeBarModule extends ModuleBase.Toggleable { + public static final String ID = "timebar"; + private static final ConcurrentLinkedDeque timeBars = new ConcurrentLinkedDeque<>(); + + public TimeBarModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new TimeBarCommand(this)); + + Solstice.scheduler.scheduleAtFixedRate(this::updateBars, 0, 1, TimeUnit.SECONDS); + } + + public void updateBars() { + for (var timeBar : timeBars) { + var remove = timeBar.elapse(); + TimeBarEvents.PROGRESS.invoker().onProgress(timeBar, Solstice.server); + + var players = Solstice.server.getPlayerList().getPlayers(); + showBar(players, timeBar); + + if (remove) { + timeBars.remove(timeBar); + TimeBarEvents.END.invoker().onEnd(timeBar, Solstice.server); + hideBar(players, timeBar); + } + } + } + + private void showBar(Collection players, TimeBar timeBar) { + timeBar.getBossBar().setPlayers(players); + } + + private void hideBar(Collection players, TimeBar timeBar) { + players.forEach(player -> { + timeBar.getBossBar().removePlayer(player); + + }); + } + + public TimeBar startTimeBar(String label, int seconds, BossEvent.BossBarColor color, BossEvent.BossBarOverlay style, boolean countdown) { + var timeBar = new TimeBar(label, seconds, countdown, color, style); + + Solstice.scheduler.schedule(() -> { + timeBars.add(timeBar); + + var players = Solstice.server.getPlayerList().getPlayers(); + showBar(players, timeBar); + + TimeBarEvents.START.invoker().onStart(timeBar, Solstice.server); + TimeBarEvents.PROGRESS.invoker().onProgress(timeBar, Solstice.server); + }, 0, TimeUnit.SECONDS); + + return timeBar; + } + + public boolean cancelTimeBar(TimeBar timeBar) { + var success = timeBars.remove(timeBar); + if (success) { + var players = Solstice.server.getPlayerList().getPlayers(); + hideBar(players, timeBar); + TimeBarEvents.CANCEL.invoker().onCancel(timeBar, Solstice.server); + } + return success; + } + + public boolean cancelTimeBar(UUID uuid) { + var progressBar = timeBars.stream().filter(p -> p.getUuid().equals(uuid)).findFirst().orElse(null); + if (progressBar == null) { + return false; + } + + return cancelTimeBar(progressBar); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/timeBar/commands/TimeBarCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/commands/TimeBarCommand.java new file mode 100644 index 0000000..b49c793 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/timeBar/commands/TimeBarCommand.java @@ -0,0 +1,134 @@ +package me.alexdevs.solstice.modules.timeBar.commands; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.command.TimeSpan; +import me.alexdevs.solstice.api.events.TimeBarEvents; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.timeBar.TimeBarModule; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.UuidArgument; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.world.BossEvent; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TimeBarCommand extends ModCommand { + private static final ConcurrentHashMap runningBars = new ConcurrentHashMap<>(); + + public TimeBarCommand(TimeBarModule module) { + super(module); + + TimeBarEvents.END.register((timeBar, server) -> { + if (runningBars.containsKey(timeBar.getUuid())) { + var barCommand = runningBars.get(timeBar.getUuid()); + final var command = barCommand.command(); + final var source = barCommand.source(); + runningBars.remove(timeBar.getUuid()); + Solstice.nextTick(() -> { + try { + dispatcher.execute(command, source); + } catch (CommandSyntaxException e) { + source.sendSuccess(() -> Component.literal(e.toString()).withStyle(ChatFormatting.RED), false); + } + }); + } + }); + } + + @Override + public List getNames() { + return List.of("timebar"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(3)) + .then(literal("start") + .then(argument("duration", StringArgumentType.word()) + .suggests(TimeSpan::suggest) + .then(argument("color", StringArgumentType.word()) + .suggests((context, builder) -> { + var colors = Arrays.stream(BossEvent.BossBarColor.values()).map(Enum::toString).toList(); + return SharedSuggestionProvider.suggest(colors, builder); + }) + .then(argument("style", StringArgumentType.word()) + .suggests((context, builder) -> { + var styles = Arrays.stream(BossEvent.BossBarOverlay.values()).map(Enum::toString).toList(); + return SharedSuggestionProvider.suggest(styles, builder); + }) + .then(argument("countdown", BoolArgumentType.bool()) + .then(argument("label", StringArgumentType.string()) + .then(argument("command", StringArgumentType.greedyString()) + .suggests((context, builder) -> dispatcher.getRoot().listSuggestions(context, builder)) + .executes(this::execute)) + + ) + ) + ) + ) + ) + ) + .then(literal("cancel") + .then(argument("uuid", UuidArgument.uuid()) + .executes(this::executeCancel))); + } + + private int execute(CommandContext context) throws CommandSyntaxException { + var seconds = TimeSpan.getTimeSpan(context, "duration"); + var colorName = StringArgumentType.getString(context, "color"); + var styleName = StringArgumentType.getString(context, "style"); + var countdown = BoolArgumentType.getBool(context, "countdown"); + var label = StringArgumentType.getString(context, "label"); + var command = StringArgumentType.getString(context, "command"); + + var color = BossEvent.BossBarColor.valueOf(colorName); + var style = BossEvent.BossBarOverlay.valueOf(styleName); + + var bar = module.startTimeBar(label, seconds, color, style, countdown); + + runningBars.put(bar.getUuid(), new BarCommand(context.getSource(), command)); + + context.getSource().sendSuccess(() -> Component + .literal("New time bar created with UUID ") + .append(Component.literal(bar.getUuid().toString()).setStyle(Style.EMPTY + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.nullToEmpty("Click to copy"))) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, bar.getUuid().toString())))), true); + + return 1; + } + + private int executeCancel(CommandContext context) { + var uuid = UuidArgument.getUuid(context, "uuid"); + + if (!runningBars.containsKey(uuid)) { + context.getSource().sendSuccess(() -> Component.literal("Time bar not found!").withStyle(ChatFormatting.RED), false); + return 1; + } + + runningBars.remove(uuid); + module.cancelTimeBar(uuid); + + context.getSource().sendSuccess(() -> Component.literal("Time bar canceled"), true); + + return 1; + } + + private record BarCommand(CommandSourceStack source, String command) { + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/trash/TrashModule.java b/common/src/main/java/me/alexdevs/solstice/modules/trash/TrashModule.java new file mode 100644 index 0000000..5019ffb --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/trash/TrashModule.java @@ -0,0 +1,21 @@ +package me.alexdevs.solstice.modules.trash; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.trash.commands.TrashCommand; +import me.alexdevs.solstice.modules.trash.data.TrashLocale; + +public class TrashModule extends ModuleBase.Toggleable { + public static final String ID = "trash"; + + public TrashModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, TrashLocale.MODULE); + + commands.add(new TrashCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/trash/commands/TrashCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/trash/commands/TrashCommand.java new file mode 100644 index 0000000..35b793e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/trash/commands/TrashCommand.java @@ -0,0 +1,38 @@ +package me.alexdevs.solstice.modules.trash.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.trash.TrashModule; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ChestMenu; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class TrashCommand extends ModCommand { + public TrashCommand(TrashModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("trash"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + + player.openMenu( + new SimpleMenuProvider((syncId, inventory, playerx) -> + ChestMenu.threeRows(syncId, inventory), + module.locale().get("trash"))); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/trash/data/TrashLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/trash/data/TrashLocale.java new file mode 100644 index 0000000..76701e8 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/trash/data/TrashLocale.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.trash.data; + +import java.util.Map; + +public class TrashLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("trash", "Trash Bin") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/UtilitiesModule.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/UtilitiesModule.java new file mode 100644 index 0000000..73dcef7 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/UtilitiesModule.java @@ -0,0 +1,23 @@ +package me.alexdevs.solstice.modules.utilities; + +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.utilities.commands.*; + +public class UtilitiesModule extends ModuleBase.Toggleable { + public static final String ID = "utilities"; + + public UtilitiesModule() { + super(ID); + } + + @Override + public void init() { + commands.add(new AnvilCommand(this)); + commands.add(new CartographyCommand(this)); + commands.add(new GrindstoneCommand(this)); + commands.add(new LoomCommand(this)); + commands.add(new SmithingCommand(this)); + commands.add(new StonecutterCommand(this)); + commands.add(new WorkbenchCommand(this)); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/AnvilCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/AnvilCommand.java new file mode 100644 index 0000000..9d86a9c --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/AnvilCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualAnvilScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class AnvilCommand extends ModCommand { + public AnvilCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("anvil"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("anvil", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualAnvilScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.repair")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_ANVIL); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/CartographyCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/CartographyCommand.java new file mode 100644 index 0000000..9f92260 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/CartographyCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualCartographyTableScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class CartographyCommand extends ModCommand { + public CartographyCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("cartography"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("cartography", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualCartographyTableScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.cartography_table")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_CARTOGRAPHY_TABLE); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/GrindstoneCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/GrindstoneCommand.java new file mode 100644 index 0000000..d2967da --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/GrindstoneCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualGrindstoneScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class GrindstoneCommand extends ModCommand { + public GrindstoneCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("grindstone"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("grindstone", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualGrindstoneScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.grindstone_title")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_GRINDSTONE); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/LoomCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/LoomCommand.java new file mode 100644 index 0000000..9365b2d --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/LoomCommand.java @@ -0,0 +1,41 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualLoomScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class LoomCommand extends ModCommand { + public LoomCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("loom"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("loom", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualLoomScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.loom")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_LOOM); + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/SmithingCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/SmithingCommand.java new file mode 100644 index 0000000..844d282 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/SmithingCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualSmithingScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class SmithingCommand extends ModCommand { + public SmithingCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("smithing"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("smithing", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualSmithingScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.upgrade")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_SMITHING_TABLE); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/StonecutterCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/StonecutterCommand.java new file mode 100644 index 0000000..f749e08 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/StonecutterCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualStonecutterScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class StonecutterCommand extends ModCommand { + public StonecutterCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("stonecutter"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("stonecutter", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualStonecutterScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.stonecutter")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_STONECUTTER); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/WorkbenchCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/WorkbenchCommand.java new file mode 100644 index 0000000..62190af --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/commands/WorkbenchCommand.java @@ -0,0 +1,42 @@ +package me.alexdevs.solstice.modules.utilities.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.utilities.UtilitiesModule; +import me.alexdevs.solstice.modules.utilities.virtualScreenHandlers.VirtualCraftingScreenHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ContainerLevelAccess; +import java.util.List; + +import static net.minecraft.commands.Commands.literal; + +public class WorkbenchCommand extends ModCommand { + public WorkbenchCommand(UtilitiesModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("workbench", "craft"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("workbench", 2)) + .executes(context -> { + var player = context.getSource().getPlayerOrException(); + var screen = new SimpleMenuProvider( + (syncId, inventory, p) -> + new VirtualCraftingScreenHandler(syncId, inventory, ContainerLevelAccess.create(player.level(), player.blockPosition())), + Component.translatable("container.crafting")); + player.openMenu(screen); + player.awardStat(Stats.INTERACT_WITH_CRAFTING_TABLE); + + return 1; + }); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualAnvilScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualAnvilScreenHandler.java new file mode 100644 index 0000000..a7fcb39 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualAnvilScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AnvilMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; + +public class VirtualAnvilScreenHandler extends AnvilMenu { + public VirtualAnvilScreenHandler(int syncId, Inventory inventory, ContainerLevelAccess context) { + super(syncId, inventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCartographyTableScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCartographyTableScreenHandler.java new file mode 100644 index 0000000..d85381f --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCartographyTableScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.CartographyTableMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; + +public class VirtualCartographyTableScreenHandler extends CartographyTableMenu { + public VirtualCartographyTableScreenHandler(int syncId, Inventory inventory, ContainerLevelAccess context) { + super(syncId, inventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCraftingScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCraftingScreenHandler.java new file mode 100644 index 0000000..fb66969 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualCraftingScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.CraftingMenu; + +public class VirtualCraftingScreenHandler extends CraftingMenu { + public VirtualCraftingScreenHandler(int syncId, Inventory playerInventory, ContainerLevelAccess context) { + super(syncId, playerInventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualGrindstoneScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualGrindstoneScreenHandler.java new file mode 100644 index 0000000..b5a125e --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualGrindstoneScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.GrindstoneMenu; + +public class VirtualGrindstoneScreenHandler extends GrindstoneMenu { + public VirtualGrindstoneScreenHandler(int syncId, Inventory playerInventory, ContainerLevelAccess context) { + super(syncId, playerInventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualLoomScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualLoomScreenHandler.java new file mode 100644 index 0000000..23f4e43 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualLoomScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.LoomMenu; + +public class VirtualLoomScreenHandler extends LoomMenu { + public VirtualLoomScreenHandler(int syncId, Inventory playerInventory, ContainerLevelAccess context) { + super(syncId, playerInventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualSmithingScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualSmithingScreenHandler.java new file mode 100644 index 0000000..f52748a --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualSmithingScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.SmithingMenu; + +public class VirtualSmithingScreenHandler extends SmithingMenu { + public VirtualSmithingScreenHandler(int syncId, Inventory playerInventory, ContainerLevelAccess context) { + super(syncId, playerInventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualStonecutterScreenHandler.java b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualStonecutterScreenHandler.java new file mode 100644 index 0000000..398ca69 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/utilities/virtualScreenHandlers/VirtualStonecutterScreenHandler.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.utilities.virtualScreenHandlers; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.StonecutterMenu; + +public class VirtualStonecutterScreenHandler extends StonecutterMenu { + public VirtualStonecutterScreenHandler(int syncId, Inventory playerInventory, ContainerLevelAccess context) { + super(syncId, playerInventory, context); + } + + @Override + public boolean stillValid(Player player) { + return true; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/WarpModule.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/WarpModule.java new file mode 100644 index 0000000..87b1684 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/WarpModule.java @@ -0,0 +1,35 @@ +package me.alexdevs.solstice.modules.warp; + +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModuleBase; +import me.alexdevs.solstice.modules.warp.commands.DeleteWarpCommand; +import me.alexdevs.solstice.modules.warp.commands.SetWarpCommand; +import me.alexdevs.solstice.modules.warp.commands.WarpCommand; +import me.alexdevs.solstice.modules.warp.commands.WarpsCommand; +import me.alexdevs.solstice.modules.warp.data.WarpLocale; +import me.alexdevs.solstice.modules.warp.data.WarpServerData; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.level.ServerPlayer; + +public class WarpModule extends ModuleBase.Toggleable { + public static final String ID = "warp"; + + public WarpModule() { + super(ID); + } + + @Override + public void init() { + Solstice.localeManager.registerModule(ID, WarpLocale.MODULE); + Solstice.serverData.registerData(ID, WarpServerData.class, WarpServerData::new); + + commands.add(new WarpCommand(this)); + commands.add(new WarpsCommand(this)); + commands.add(new SetWarpCommand(this)); + commands.add(new DeleteWarpCommand(this)); + } + + public boolean canUseWarp(ServerPlayer player, String warpName) { + return Permissions.check(player, getPermissionNode("warps." + warpName), true); + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/DeleteWarpCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/DeleteWarpCommand.java new file mode 100644 index 0000000..49ad9ac --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/DeleteWarpCommand.java @@ -0,0 +1,69 @@ +package me.alexdevs.solstice.modules.warp.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.warp.WarpModule; +import me.alexdevs.solstice.modules.warp.data.WarpServerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class DeleteWarpCommand extends ModCommand { + public DeleteWarpCommand(WarpModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("delwarp"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("set", 2)) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isPlayer()) + return SharedSuggestionProvider.suggest(new String[]{}, builder); + + var serverData = Solstice.serverData.getData(WarpServerData.class); + return SharedSuggestionProvider.suggest(serverData.warps.keySet().stream(), builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String name) { + var serverData = Solstice.serverData.getData(WarpServerData.class); + var warps = serverData.warps; + + if (!warps.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get( + "warpNotFound", + Map.of( + "warp", Component.nullToEmpty(name) + ) + ), true); + return 0; + } + + warps.remove(name); + + context.getSource().sendSuccess(() -> module.locale().get( + "deleted", + Map.of( + "warp", Component.nullToEmpty(name) + ) + ), true); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/SetWarpCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/SetWarpCommand.java new file mode 100644 index 0000000..03b2022 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/SetWarpCommand.java @@ -0,0 +1,57 @@ +package me.alexdevs.solstice.modules.warp.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.ServerLocation; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.warp.WarpModule; +import me.alexdevs.solstice.modules.warp.data.WarpServerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class SetWarpCommand extends ModCommand { + public SetWarpCommand(WarpModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("setwarp"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require("set", 2)) + .then(argument("name", StringArgumentType.word()) + .executes(context -> execute(context, + StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String name) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var serverData = Solstice.serverData.getData(WarpServerData.class); + + var warps = serverData.warps; + + var warpPosition = new ServerLocation(player); + warps.put(name, warpPosition); + + context.getSource().sendSuccess(() -> module.locale().get( + "created", + Map.of( + "warp", Component.nullToEmpty(name) + ) + ), true); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpCommand.java new file mode 100644 index 0000000..58b0004 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpCommand.java @@ -0,0 +1,89 @@ +package me.alexdevs.solstice.modules.warp.commands; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.warp.WarpModule; +import me.alexdevs.solstice.modules.warp.data.WarpServerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class WarpCommand extends ModCommand { + public WarpCommand(WarpModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("warp"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .then(argument("name", StringArgumentType.word()) + .suggests((context, builder) -> { + if (!context.getSource().isPlayer()) + return SharedSuggestionProvider.suggest(new String[]{}, builder); + + var serverData = Solstice.serverData.getData(WarpServerData.class); + var player = context.getSource().getPlayer(); + var warps = serverData.warps.keySet().stream().filter(serverPosition -> module.canUseWarp(player, serverPosition)); + return SharedSuggestionProvider.suggest(warps, builder); + }) + .executes(context -> execute(context, StringArgumentType.getString(context, "name")))); + } + + private int execute(CommandContext context, String name) throws CommandSyntaxException { + var player = context.getSource().getPlayerOrException(); + var serverDate = Solstice.serverData.getData(WarpServerData.class); + var warps = serverDate.warps; + var playerContext = PlaceholderContext.of(player); + + var placeholders = Map.of( + "warp", Component.nullToEmpty(name) + ); + + if (!warps.containsKey(name)) { + context.getSource().sendSuccess(() -> module.locale().get( + "warpNotFound", + playerContext, + placeholders + + ), false); + return 0; + } + + if (!module.canUseWarp(player, name)) { + context.getSource().sendSuccess(() -> module.locale().get( + "noPermission", + playerContext, + placeholders + ), false); + + return 0; + } + + context.getSource().sendSuccess(() -> module.locale().get( + "teleporting", + playerContext, + placeholders + ), false); + + var warpPosition = warps.get(name); + warpPosition.teleport(player); + + return 1; + } +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpsCommand.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpsCommand.java new file mode 100644 index 0000000..e2387b2 --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/commands/WarpsCommand.java @@ -0,0 +1,78 @@ +package me.alexdevs.solstice.modules.warp.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import eu.pb4.placeholders.api.PlaceholderContext; +import me.alexdevs.solstice.Solstice; +import me.alexdevs.solstice.api.module.ModCommand; +import me.alexdevs.solstice.modules.warp.WarpModule; +import me.alexdevs.solstice.modules.warp.data.WarpServerData; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import java.util.List; +import java.util.Map; + +import static net.minecraft.commands.Commands.literal; + +public class WarpsCommand extends ModCommand { + public WarpsCommand(WarpModule module) { + super(module); + } + + @Override + public List getNames() { + return List.of("warps"); + } + + @Override + public LiteralArgumentBuilder command(String name) { + return literal(name) + .requires(require(true)) + .executes(context -> { + var source = context.getSource(); + var serverDate = Solstice.serverData.getData(WarpServerData.class); + var warpList = serverDate.warps.keySet().stream().toList(); + var sourceContext = PlaceholderContext.of(source); + + if(source.isPlayer()) { + var player = source.getPlayer(); + warpList = warpList.stream().filter(warp -> module.canUseWarp(player, warp)).toList(); + } + + if (warpList.isEmpty()) { + context.getSource().sendSuccess(() -> module.locale().get( + "noWarps", + sourceContext + ), false); + return 1; + } + + var listText = Component.empty(); + var comma = module.locale().get("warpsComma"); + for (var i = 0; i < warpList.size(); i++) { + if (i > 0) { + listText = listText.append(comma); + } + var placeholders = Map.of( + "warp", Component.nullToEmpty(warpList.get(i)) + ); + + listText = listText.append(module.locale().get( + "warpsFormat", + sourceContext, + placeholders + )); + } + + var placeholders = Map.of( + "warpList", (Component) listText + ); + context.getSource().sendSuccess(() -> module.locale().get( + "warpList", + sourceContext, + placeholders + ), false); + + return 1; + }); + } +} \ No newline at end of file diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpLocale.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpLocale.java new file mode 100644 index 0000000..fcda3fb --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpLocale.java @@ -0,0 +1,17 @@ +package me.alexdevs.solstice.modules.warp.data; + +import java.util.Map; + +public class WarpLocale { + public static final Map MODULE = Map.ofEntries( + Map.entry("teleporting", "Warping to ${warp}..."), + Map.entry("warpNotFound", "The warp ${warp} does not exist!"), + Map.entry("warpList", "Server warps: ${warpList}"), + Map.entry("warpsFormat", "${warp}"), + Map.entry("warpsComma", ", "), + Map.entry("noWarps", "There are no warps so far."), + Map.entry("noPermission", "You do not have permission to warp to ${warp}."), + Map.entry("deleted", "Warp ${warp} deleted!"), + Map.entry("created", "New warp ${warp} created!") + ); +} diff --git a/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpServerData.java b/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpServerData.java new file mode 100644 index 0000000..eb93acb --- /dev/null +++ b/common/src/main/java/me/alexdevs/solstice/modules/warp/data/WarpServerData.java @@ -0,0 +1,9 @@ +package me.alexdevs.solstice.modules.warp.data; + +import me.alexdevs.solstice.api.ServerLocation; + +import java.util.concurrent.ConcurrentHashMap; + +public class WarpServerData { + public ConcurrentHashMap warps = new ConcurrentHashMap<>(); +}