From a10784e26c3380dde9894bdf4311f2cfe472c641 Mon Sep 17 00:00:00 2001 From: sakurawald Date: Sun, 19 May 2024 04:42:24 +0800 Subject: [PATCH] refactor: mojang official mappings -> yarn mappings --- src/main/java/assets/fuji/Cat.java | 6 + src/main/java/io/github/sakurawald/Fuji.java | 44 ++ .../io/github/sakurawald/config/Configs.java | 16 + .../config/handler/ConfigHandler.java | 128 +++++ .../config/handler/ObjectConfigHandler.java | 77 +++ .../config/handler/ResourceConfigHandler.java | 72 +++ .../sakurawald/config/model/ChatModel.java | 17 + .../sakurawald/config/model/ConfigModel.java | 520 ++++++++++++++++++ .../sakurawald/config/model/HeadModel.java | 9 + .../sakurawald/config/model/HomeModel.java | 11 + .../sakurawald/config/model/PvPModel.java | 9 + .../config/model/SchedulerModel.java | 20 + .../sakurawald/config/model/SeenModel.java | 7 + .../sakurawald/config/model/WorksModel.java | 11 + .../sakurawald/module/ModuleManager.java | 99 ++++ .../module/initializer/ModuleInitializer.java | 29 + .../module/initializer/afk/AfkModule.java | 84 +++ .../afk/ServerPlayerAccessor_afk.java | 12 + .../module/initializer/anvil/AnvilModule.java | 34 ++ .../module/initializer/back/BackModule.java | 54 ++ .../module/initializer/bed/BedModule.java | 41 ++ .../BetterFakePlayerModule.java | 218 ++++++++ .../better_info/BetterInfoModule.java | 22 + .../biome_lookup_cache/ChunkManager.java | 70 +++ .../module/initializer/chat/ChatModule.java | 187 +++++++ .../chat/display/DisplayHelper.java | 59 ++ .../chat/display/SoftReferenceMap.java | 35 ++ .../chat/display/gui/DisplayGuiBuilder.java | 43 ++ .../display/gui/EnderChestDisplayGui.java | 42 ++ .../chat/display/gui/InventoryDisplayGui.java | 61 ++ .../chat/display/gui/ItemDisplayGui.java | 35 ++ .../display/gui/ShulkerBoxDisplayGui.java | 62 +++ .../chat/mention/MentionPlayersJob.java | 46 ++ .../CommandCooldownModule.java | 35 ++ .../initializer/config/ConfigModule.java | 41 ++ .../initializer/deathlog/DeathLogModule.java | 254 +++++++++ .../enchantment/EnchantmentModule.java | 34 ++ .../enderchest/EnderChestModule.java | 34 ++ .../extinguish/ExtinguishModule.java | 29 + .../module/initializer/feed/FeedModule.java | 36 ++ .../module/initializer/fly/FlyModule.java | 35 ++ .../module/initializer/god/GodModule.java | 34 ++ .../grindstone/GrindStoneModule.java | 34 ++ .../module/initializer/hat/HatModule.java | 38 ++ .../module/initializer/head/HeadModule.java | 93 ++++ .../module/initializer/head/api/Category.java | 83 +++ .../module/initializer/head/api/Head.java | 66 +++ .../initializer/head/api/HeadDatabaseAPI.java | 83 +++ .../module/initializer/head/gui/HeadGui.java | 48 ++ .../initializer/head/gui/PagedHeadsGui.java | 146 +++++ .../initializer/head/gui/PlayerInputGui.java | 123 +++++ .../initializer/head/gui/SearchInputGui.java | 36 ++ .../module/initializer/heal/HealModule.java | 31 ++ .../module/initializer/home/HomeModule.java | 135 +++++ .../initializer/language/LanguageModule.java | 14 + .../initializer/main_stats/MainStats.java | 127 +++++ .../main_stats/MainStatsModule.java | 103 ++++ .../module/initializer/more/MoreModule.java | 31 ++ .../module/initializer/motd/MotdModule.java | 68 +++ .../MultiObsidianPlatformModule.java | 95 ++++ .../newbie_welcome/NewbieWelcomeModule.java | 19 + .../random_teleport/HeightFinder.java | 15 + .../HeightFindingStrategy.java | 110 ++++ .../random_teleport/RandomTeleport.java | 159 ++++++ .../module/initializer/ping/PingModule.java | 41 ++ .../initializer/profiler/ProfilerModule.java | 156 ++++++ .../module/initializer/pvp/PvpModule.java | 96 ++++ .../initializer/repair/RepairModule.java | 31 ++ .../module/initializer/reply/ReplyModule.java | 51 ++ .../resource_world/FilteredRegistry.java | 180 ++++++ .../resource_world/ResourceWorld.java | 34 ++ .../resource_world/ResourceWorldManager.java | 120 ++++ .../resource_world/ResourceWorldModule.java | 265 +++++++++ .../ResourceWorldProperties.java | 22 + .../resource_world/SafeIterator.java | 24 + .../VoidWorldGenerationProgressListener.java | 31 ++ .../DimensionOptionsMixinInterface.java | 15 + .../SimpleRegistryMixinInterface.java | 26 + .../initializer/scheduler/ScheduleJob.java | 35 ++ .../scheduler/SchedulerModule.java | 91 +++ .../scheduler/SpecializedCommand.java | 67 +++ .../initializer/seen/GameProfileCacheEx.java | 9 + .../module/initializer/seen/SeenModule.java | 53 ++ .../module/initializer/skin/SkinRestorer.java | 129 +++++ .../initializer/skin/command/SkinModule.java | 100 ++++ .../initializer/skin/enums/SkinVariant.java | 18 + .../module/initializer/skin/io/SkinIO.java | 42 ++ .../initializer/skin/io/SkinStorage.java | 56 ++ .../skin/provider/MineSkinSkinProvider.java | 29 + .../skin/provider/MojangSkinProvider.java | 32 ++ .../stonecutter/StoneCutterModule.java | 34 ++ .../initializer/suicide/SuicideModule.java | 30 + .../initializer/teleport_warmup/Position.java | 62 +++ .../teleport_warmup/ServerPlayerAccessor.java | 7 + .../teleport_warmup/TeleportTicket.java | 24 + .../teleport_warmup/TeleportWarmupModule.java | 74 +++ .../module/initializer/test/TestModule.java | 66 +++ .../ITickableChunkSource.java | 10 + .../initializer/top_chunks/ChunkScore.java | 132 +++++ .../top_chunks/TopChunksModule.java | 113 ++++ .../module/initializer/tpa/TpaModule.java | 155 ++++++ .../module/initializer/tpa/TpaRequest.java | 131 +++++ .../workbench/WorkbenchModule.java | 36 ++ .../initializer/works/ScheduleMethod.java | 5 + .../module/initializer/works/WorksCache.java | 44 ++ .../module/initializer/works/WorksModule.java | 283 ++++++++++ .../initializer/works/gui/ConfirmGui.java | 21 + .../initializer/works/gui/InputSignGui.java | 35 ++ .../works/work_type/NonProductionWork.java | 28 + .../works/work_type/ProductionWork.java | 285 ++++++++++ .../initializer/works/work_type/Work.java | 230 ++++++++ .../world_downloader/FileDownloadHandler.java | 76 +++ .../WorldDownloaderModule.java | 178 ++++++ .../ZeroCommandPermissionModule.java | 63 +++ .../module/mixin/ModuleMixinConfigPlugin.java | 59 ++ .../server_instance/MinecraftServerMixin.java | 21 + .../module/mixin/afk/PlayerListMixin.java | 21 + .../module/mixin/afk/ServerPlayerMixin.java | 84 +++ .../module/mixin/back/ServerPlayerMixin.java | 41 ++ .../PlayerCommandMixin.java | 83 +++ .../better_fake_player/PlayerListMixin.java | 28 + .../mixin/better_fake_player/PlayerMixin.java | 51 ++ .../mixin/better_info/InfoCommandMixin.java | 40 ++ .../NaturalSpawnerMixin.java | 29 + .../ServerGamePacketListenerImplMixin.java | 22 + .../DedicatedPlayerManagerMixin.java | 23 + .../ServerGamePacketListenerImplMixin.java | 38 ++ .../module/mixin/chat/PlayerListMixin.java | 27 + .../ServerGamePacketListenerImplMixin.java | 27 + .../mixin/command_cooldown/CommandsMixin.java | 36 ++ .../command_interactive/SignBlockMixin.java | 72 +++ .../mixin/command_spy/CommandsMixin.java | 27 + .../mixin/deathlog/ServerPlayerMixin.java | 26 + .../mixin/language/ServerPlayerMixin.java | 22 + .../module/mixin/main_stats/BlockMixin.java | 31 ++ .../mixin/main_stats/PlayerListMixin.java | 22 + .../ServerPlayNetworkHandlerMixin.java | 25 + .../ServerStatusPacketListenerImplMixin.java | 55 ++ .../multi_obsidian_platform/EntityMixin.java | 51 ++ .../mixin/newbie_welcome/PlayerListMixin.java | 29 + .../ServerPlayNetworkHandlerMixin.java | 28 + .../module/mixin/pvp/PvpToggleMixin.java | 44 ++ .../module/mixin/reply/MsgCommandMixin.java | 30 + .../EndGatewayBlockEntityMixin.java | 18 + .../MinecraftServerAccessor.java | 22 + .../resource_world/MinecraftServerMixin.java | 22 + .../resource_world/ServerWorldMixin.java | 32 ++ .../registry/DimensionOptionsMixin.java | 23 + .../DimensionOptionsRegistryHolderMixin.java | 21 + .../registry/SimpleRegistryMixin.java | 101 ++++ .../mixin/seen/GameProfileCacheMixin.java | 26 + .../module/mixin/seen/PlayerListMixin.java | 26 + .../module/mixin/skin/PlayerListMixin.java | 49 ++ .../skin/ServerLoginNetworkHandlerMixin.java | 65 +++ .../stronger_player_list/PlayerListMixin.java | 35 ++ .../ServerLevelMixin.java | 36 ++ .../mixin/system_message/ComponentMixin.java | 49 ++ .../teleport_warmup/ServerPlayerMixin.java | 89 +++ .../mixin/tick_chunk_cache/ChunkMapMixin.java | 47 ++ .../ServerChunkCacheMixin.java | 19 + .../ThreadedAnvilChunkStorageMixin.java | 12 + .../whitelist_fix/UserWhiteListMixin.java | 38 ++ .../mixin/works/HopperBlockEntityMixin.java | 76 +++ .../CommandNodeAccessor.java | 17 + .../io/github/sakurawald/util/CarpetUtil.java | 11 + .../github/sakurawald/util/CommandUtil.java | 49 ++ .../io/github/sakurawald/util/DateUtil.java | 15 + .../io/github/sakurawald/util/GuiUtil.java | 13 + .../io/github/sakurawald/util/HttpUtil.java | 39 ++ .../io/github/sakurawald/util/LogUtil.java | 22 + .../github/sakurawald/util/MessageUtil.java | 152 +++++ .../github/sakurawald/util/ScheduleUtil.java | 129 +++++ 172 files changed, 10589 insertions(+) create mode 100644 src/main/java/assets/fuji/Cat.java create mode 100644 src/main/java/io/github/sakurawald/Fuji.java create mode 100644 src/main/java/io/github/sakurawald/config/Configs.java create mode 100644 src/main/java/io/github/sakurawald/config/handler/ConfigHandler.java create mode 100644 src/main/java/io/github/sakurawald/config/handler/ObjectConfigHandler.java create mode 100644 src/main/java/io/github/sakurawald/config/handler/ResourceConfigHandler.java create mode 100644 src/main/java/io/github/sakurawald/config/model/ChatModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/ConfigModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/HeadModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/HomeModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/PvPModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/SchedulerModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/SeenModel.java create mode 100644 src/main/java/io/github/sakurawald/config/model/WorksModel.java create mode 100644 src/main/java/io/github/sakurawald/module/ModuleManager.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/ModuleInitializer.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/afk/AfkModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/afk/ServerPlayerAccessor_afk.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/anvil/AnvilModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/back/BackModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/bed/BedModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/better_fake_player/BetterFakePlayerModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/better_info/BetterInfoModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/biome_lookup_cache/ChunkManager.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/ChatModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/DisplayHelper.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/SoftReferenceMap.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/DisplayGuiBuilder.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/EnderChestDisplayGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/InventoryDisplayGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ItemDisplayGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ShulkerBoxDisplayGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/chat/mention/MentionPlayersJob.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/command_cooldown/CommandCooldownModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/config/ConfigModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/deathlog/DeathLogModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/enchantment/EnchantmentModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/enderchest/EnderChestModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/extinguish/ExtinguishModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/feed/FeedModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/fly/FlyModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/god/GodModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/grindstone/GrindStoneModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/hat/HatModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/HeadModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/api/Category.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/api/Head.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/api/HeadDatabaseAPI.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/gui/HeadGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/gui/PagedHeadsGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/gui/PlayerInputGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/head/gui/SearchInputGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/heal/HealModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/home/HomeModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/language/LanguageModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStats.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStatsModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/more/MoreModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/motd/MotdModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/multi_obsidian_platform/MultiObsidianPlatformModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/NewbieWelcomeModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFinder.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFindingStrategy.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/RandomTeleport.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/ping/PingModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/profiler/ProfilerModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/pvp/PvpModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/repair/RepairModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/reply/ReplyModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/FilteredRegistry.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorld.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldManager.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldProperties.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/SafeIterator.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/VoidWorldGenerationProgressListener.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/DimensionOptionsMixinInterface.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/SimpleRegistryMixinInterface.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/scheduler/ScheduleJob.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/scheduler/SchedulerModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/scheduler/SpecializedCommand.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/seen/GameProfileCacheEx.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/seen/SeenModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/SkinRestorer.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/command/SkinModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/enums/SkinVariant.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinIO.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinStorage.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/provider/MineSkinSkinProvider.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/skin/provider/MojangSkinProvider.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/stonecutter/StoneCutterModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/suicide/SuicideModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/Position.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/ServerPlayerAccessor.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportTicket.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportWarmupModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/test/TestModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/tick_chunk_cache/ITickableChunkSource.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/top_chunks/ChunkScore.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/top_chunks/TopChunksModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/tpa/TpaModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/tpa/TpaRequest.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/workbench/WorkbenchModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/ScheduleMethod.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/WorksCache.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/WorksModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/gui/ConfirmGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/gui/InputSignGui.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/work_type/NonProductionWork.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/work_type/ProductionWork.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/works/work_type/Work.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/world_downloader/FileDownloadHandler.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/world_downloader/WorldDownloaderModule.java create mode 100644 src/main/java/io/github/sakurawald/module/initializer/zero_command_permission/ZeroCommandPermissionModule.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/ModuleMixinConfigPlugin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/_internal/server_instance/MinecraftServerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/afk/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/afk/ServerPlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/back/ServerPlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerCommandMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/better_info/InfoCommandMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/biome_lookup_cache/NaturalSpawnerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/bypass_chat_speed/ServerGamePacketListenerImplMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/bypass_max_player_limit/DedicatedPlayerManagerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/bypass_move_speed/ServerGamePacketListenerImplMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/chat/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/chat/ServerGamePacketListenerImplMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/command_cooldown/CommandsMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/command_interactive/SignBlockMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/command_spy/CommandsMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/deathlog/ServerPlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/language/ServerPlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/main_stats/BlockMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/main_stats/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/main_stats/ServerPlayNetworkHandlerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/motd/ServerStatusPacketListenerImplMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/multi_obsidian_platform/EntityMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/newbie_welcome/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/op_protect/ServerPlayNetworkHandlerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/pvp/PvpToggleMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/reply/MsgCommandMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/EndGatewayBlockEntityMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerAccessor.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/ServerWorldMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsRegistryHolderMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/SimpleRegistryMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/seen/GameProfileCacheMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/seen/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/skin/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/skin/ServerLoginNetworkHandlerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/PlayerListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/ServerLevelMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/system_message/ComponentMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/teleport_warmup/ServerPlayerMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ChunkMapMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ServerChunkCacheMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/top_chunks/ThreadedAnvilChunkStorageMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/whitelist_fix/UserWhiteListMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/works/HopperBlockEntityMixin.java create mode 100644 src/main/java/io/github/sakurawald/module/mixin/zero_command_permission/CommandNodeAccessor.java create mode 100644 src/main/java/io/github/sakurawald/util/CarpetUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/CommandUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/DateUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/GuiUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/HttpUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/LogUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/MessageUtil.java create mode 100644 src/main/java/io/github/sakurawald/util/ScheduleUtil.java diff --git a/src/main/java/assets/fuji/Cat.java b/src/main/java/assets/fuji/Cat.java new file mode 100644 index 000000000..439896677 --- /dev/null +++ b/src/main/java/assets/fuji/Cat.java @@ -0,0 +1,6 @@ +package assets.fuji; + + +public class Cat { + // we place a cat here, and you can ask the cat to meow +} diff --git a/src/main/java/io/github/sakurawald/Fuji.java b/src/main/java/io/github/sakurawald/Fuji.java new file mode 100644 index 000000000..7fc9efb1e --- /dev/null +++ b/src/main/java/io/github/sakurawald/Fuji.java @@ -0,0 +1,44 @@ +package io.github.sakurawald; + +import io.github.sakurawald.config.handler.ConfigHandler; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.util.LogUtil; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.Logger; + +import java.nio.file.Path; + + +// TODO: warmup module +// TODO: placeholder module +// TODO: /tppos module +// TODO: command alias module (test priority with ZeroPermissionModule) +// TODO: playtime(every/for) rewards and rank like module +// TODO: kit module +// TODO: luckperms context calculate module +// TODO: /invsee module + +public class Fuji implements ModInitializer { + public static final String MOD_ID = "fuji"; + public static final Logger LOGGER = LogUtil.createLogger("Fuji"); + public static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID).toAbsolutePath(); + public static MinecraftServer SERVER; + + @Override + public void onInitialize() { + /* modules */ + ModuleManager.initializeModules(); + ServerLifecycleEvents.SERVER_STARTED.register(server -> ModuleManager.reportModules()); + + /* scheduler */ + ServerLifecycleEvents.SERVER_STARTED.register(server -> ScheduleUtil.startScheduler()); + ServerLifecycleEvents.SERVER_STOPPING.register(server -> { + ScheduleUtil.triggerJobs(ConfigHandler.ConfigHandlerAutoSaveJob.class.getName()); + ScheduleUtil.shutdownScheduler(); + }); + } +} diff --git a/src/main/java/io/github/sakurawald/config/Configs.java b/src/main/java/io/github/sakurawald/config/Configs.java new file mode 100644 index 000000000..6ae1d3cc9 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/Configs.java @@ -0,0 +1,16 @@ +package io.github.sakurawald.config; + +import io.github.sakurawald.config.handler.ConfigHandler; +import io.github.sakurawald.config.handler.ObjectConfigHandler; +import io.github.sakurawald.config.model.*; + + +public class Configs { + + public static final ConfigHandler configHandler = new ObjectConfigHandler<>("config.json", ConfigModel.class); + public static final ConfigHandler chatHandler = new ObjectConfigHandler<>("chat.json", ChatModel.class); + public static final ConfigHandler pvpHandler = new ObjectConfigHandler<>("pvp.json", PvPModel.class); + public static final ConfigHandler worksHandler = new ObjectConfigHandler<>("works.json", WorksModel.class); + public static final ConfigHandler headHandler = new ObjectConfigHandler<>("head.json", HeadModel.class); + public static final ConfigHandler schedulerHandler = new ObjectConfigHandler<>("scheduler.json", SchedulerModel.class); +} diff --git a/src/main/java/io/github/sakurawald/config/handler/ConfigHandler.java b/src/main/java/io/github/sakurawald/config/handler/ConfigHandler.java new file mode 100644 index 000000000..84c5c7917 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/handler/ConfigHandler.java @@ -0,0 +1,128 @@ +package io.github.sakurawald.config.handler; + +import assets.fuji.Cat; +import com.google.gson.*; +import io.github.sakurawald.module.initializer.works.work_type.Work; +import io.github.sakurawald.util.ScheduleUtil; +import lombok.Cleanup; +import lombok.Getter; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Set; + +import static io.github.sakurawald.Fuji.LOGGER; + + +public abstract class ConfigHandler { + + @Getter + protected static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .serializeNulls() + .registerTypeAdapter(Work.class, new Work.WorkTypeAdapter()) + .create(); + + protected File file; + protected T model; + + protected boolean merged = false; + + public ConfigHandler(File file) { + this.file = file; + } + + public static JsonElement getJsonElement(String resourcePath) { + try { + InputStream inputStream = Cat.class.getResourceAsStream(resourcePath); + assert inputStream != null; + @Cleanup Reader reader = new BufferedReader(new InputStreamReader(inputStream)); + return JsonParser.parseReader(reader); + } catch (Exception e) { + LOGGER.error(e.getMessage()); + } + + return null; + } + + public abstract void loadFromDisk(); + + public abstract void saveToDisk(); + + + public T model() { + return this.model; + } + + public JsonElement toJsonElement() { + return gson.toJsonTree(this.model); + } + + + @SuppressWarnings("unused") + public void backupFromDisk() { + if (!file.exists()) return; + String originalFileName = file.getName(); + String backupFileName = originalFileName + ".bak"; + String backupFilePath = file.getParent() + File.separator + backupFileName; + File backupFile = new File(backupFilePath); + try { + Files.copy(file.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + LOGGER.error("Backup file failed: " + e.getMessage()); + } + } + + public void autoSave(String cron) { + String jobName = this.file.getName(); + String jobGroup = ConfigHandlerAutoSaveJob.class.getName(); + ScheduleUtil.removeJobs(jobGroup, jobName); + ScheduleUtil.addJob(ConfigHandlerAutoSaveJob.class, jobName, jobGroup, cron, new JobDataMap() { + { + this.put(ConfigHandler.class.getName(), ConfigHandler.this); + } + }); + } + + public void mergeJson(JsonElement oldJson, JsonElement newJson) { + if (!oldJson.isJsonObject() || !newJson.isJsonObject()) { + throw new IllegalArgumentException("Both elements must be JSON objects."); + } + mergeFields(oldJson.getAsJsonObject(), newJson.getAsJsonObject()); + } + + private void mergeFields(JsonObject oldJson, JsonObject newJson) { + Set> entrySet = newJson.entrySet(); + for (Map.Entry entry : entrySet) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + + if (oldJson.has(key) && oldJson.get(key).isJsonObject() && value.isJsonObject()) { + mergeFields(oldJson.getAsJsonObject(key), value.getAsJsonObject()); + } else { + // note: for JsonArray, we will not directly set array elements, but we will add new properties for every array element (language default empty-value). e.g. For List, we will never change the size of this list, but we will add missing properties for every ExamplePojo with the language default empty-value. + if (!oldJson.has(key)) { + oldJson.add(key, value); + LOGGER.warn("Add missing json property: file = {}, key = {}, value = {}", this.file.getName(), key, value); + } + } + } + } + + public static class ConfigHandlerAutoSaveJob implements Job { + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + LOGGER.debug("AutoSave ConfigWrapper {}", context.getJobDetail().getKey().getName()); + ConfigHandler configHandler = (ConfigHandler) context.getJobDetail().getJobDataMap().get(ConfigHandler.class.getName()); + configHandler.saveToDisk(); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/config/handler/ObjectConfigHandler.java b/src/main/java/io/github/sakurawald/config/handler/ObjectConfigHandler.java new file mode 100644 index 000000000..14f15492a --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/handler/ObjectConfigHandler.java @@ -0,0 +1,77 @@ +package io.github.sakurawald.config.handler; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonWriter; +import io.github.sakurawald.Fuji; +import lombok.Cleanup; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; + + +public class ObjectConfigHandler extends ConfigHandler { + + Class configClass; + + public ObjectConfigHandler(File file, Class configClass) { + super(file); + this.file = file; + this.configClass = configClass; + } + + public ObjectConfigHandler(String child, Class configClass) { + this(new File(Fuji.CONFIG_PATH.toString(), child), configClass); + } + + public void loadFromDisk() { + // Does the file exist? + try { + if (!file.exists()) { + saveToDisk(); + } else { + // read older json from disk + @Cleanup Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(this.file))); + JsonElement olderJsonElement = JsonParser.parseReader(reader); + + // merge older json with newer json + if (!this.merged) { + this.merged = true; + T newerJsonInstance = configClass.getDeclaredConstructor().newInstance(); + JsonElement newerJsonElement = gson.toJsonTree(newerJsonInstance, configClass); + mergeJson(olderJsonElement, newerJsonElement); + } + + // read merged json + model = gson.fromJson(olderJsonElement, configClass); + + this.saveToDisk(); + } + + } catch (IOException | NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + Fuji.LOGGER.error("Load config failed: " + e.getMessage()); + } + } + + + public void saveToDisk() { + try { + // Should we generate a default config instance ? + if (!file.exists()) { + //noinspection ResultOfMethodCallIgnored + this.file.getParentFile().mkdirs(); + this.model = configClass.getDeclaredConstructor().newInstance(); + } + + // Save. + JsonWriter jsonWriter = gson.newJsonWriter(new BufferedWriter(new FileWriter(this.file))); + gson.toJson(this.model, configClass, jsonWriter); + jsonWriter.close(); + } catch (IOException | InstantiationException | NoSuchMethodException | IllegalAccessException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/config/handler/ResourceConfigHandler.java b/src/main/java/io/github/sakurawald/config/handler/ResourceConfigHandler.java new file mode 100644 index 000000000..826d30929 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/handler/ResourceConfigHandler.java @@ -0,0 +1,72 @@ +package io.github.sakurawald.config.handler; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonWriter; +import io.github.sakurawald.Fuji; +import lombok.Cleanup; + +import java.io.*; + + +public class ResourceConfigHandler extends ConfigHandler { + + final String resourcePath; + + public ResourceConfigHandler(File file, String resourcePath) { + super(file); + this.file = file; + this.resourcePath = resourcePath; + } + + public ResourceConfigHandler(String resourcePath) { + this(Fuji.CONFIG_PATH.resolve(resourcePath).toFile(), resourcePath); + } + + public void loadFromDisk() { + // Does the file exist? + try { + if (!file.exists()) { + saveToDisk(); + } else { + // read older json from disk + @Cleanup Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(this.file))); + JsonElement olderJsonElement = JsonParser.parseReader(reader); + + // merge older json with newer json + if (!this.merged) { + this.merged = true; + JsonElement newerJsonElement = ResourceConfigHandler.getJsonElement(this.resourcePath); + mergeJson(olderJsonElement, newerJsonElement); + } + + // read merged json + model = olderJsonElement; + this.saveToDisk(); + } + + } catch (IOException e) { + Fuji.LOGGER.error("Load config failed: " + e.getMessage()); + } + } + + + @SuppressWarnings("ResultOfMethodCallIgnored") + public void saveToDisk() { + try { + // Should we generate a default config instance ? + if (!file.exists()) { + this.file.getParentFile().mkdirs(); + this.model = ResourceConfigHandler.getJsonElement(this.resourcePath); + } + + // Save. + JsonWriter jsonWriter = gson.newJsonWriter(new BufferedWriter(new FileWriter(this.file))); + gson.toJson(this.model, jsonWriter); + jsonWriter.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/config/model/ChatModel.java b/src/main/java/io/github/sakurawald/config/model/ChatModel.java new file mode 100644 index 000000000..b2c7732da --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/ChatModel.java @@ -0,0 +1,17 @@ +package io.github.sakurawald.config.model; + +import java.util.HashMap; + +@SuppressWarnings("InnerClassMayBeStatic") +public class ChatModel { + + public Format format = new Format(); + + public class Format { + public HashMap player2format = new HashMap<>() { + { + this.put("SakuraWald", "<#FFC7EA>%message%"); + } + }; + } +} diff --git a/src/main/java/io/github/sakurawald/config/model/ConfigModel.java b/src/main/java/io/github/sakurawald/config/model/ConfigModel.java new file mode 100644 index 000000000..8b97c5f7e --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/ConfigModel.java @@ -0,0 +1,520 @@ +package io.github.sakurawald.config.model; + + +import com.mojang.authlib.properties.Property; +import net.fabricmc.loader.api.FabricLoader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("ALL") +public class ConfigModel { + + public Common common = new Common(); + public Modules modules = new Modules(); + + public class Common { + + public Quartz quartz = new Quartz(); + + public class Quartz { + public String logger_level = "WARN"; + } + + } + + public class Modules { + public ResourceWorld resource_world = new ResourceWorld(); + public NewbieWelcome newbie_welcome = new NewbieWelcome(); + public TeleportWarmup teleport_warmup = new TeleportWarmup(); + public MOTD motd = new MOTD(); + public BetterFakePlayer better_fake_player = new BetterFakePlayer(); + public BetterInfo better_info = new BetterInfo(); + public CommandCooldown command_cooldown = new CommandCooldown(); + public TopChunks top_chunks = new TopChunks(); + public Chat chat = new Chat(); + public Skin skin = new Skin(); + public Back back = new Back(); + public Tpa tpa = new Tpa(); + public Works works = new Works(); + public WorldDownloader world_downloader = new WorldDownloader(); + public DeathLog deathlog = new DeathLog(); + public MainStats main_stats = new MainStats(); + public MultiObsidianPlatform multi_obsidian_platform = new MultiObsidianPlatform(); + public OpProtect op_protect = new OpProtect(); + public Pvp pvp = new Pvp(); + public StrongerPlayerList stronger_player_list = new StrongerPlayerList(); + public WhitelistFix whitelist_fix = new WhitelistFix(); + public ZeroCommandPermission zero_command_permission = new ZeroCommandPermission(); + public Head head = new Head(); + public Profiler profiler = new Profiler(); + public CommandSpy command_spy = new CommandSpy(); + public Scheduler scheduler = new Scheduler(); + public BypassChatSpeed bypass_chat_speed = new BypassChatSpeed(); + public BypassMoveSpeed bypass_move_speed = new BypassMoveSpeed(); + public BypassMaxPlayerLimit bypass_max_player_limit = new BypassMaxPlayerLimit(); + public BiomeLookupCache biome_lookup_cache = new BiomeLookupCache(); + public TickChunkCache tick_chunk_cache = new TickChunkCache(); + public Config config = new Config(); + public Test test = new Test(); + public Hat hat = new Hat(); + public Fly fly = new Fly(); + public God god = new God(); + public Language language = new Language(); + public Reply reply = new Reply(); + public Afk afk = new Afk(); + public Suicide suicide = new Suicide(); + public CommandInteractive command_interactive = new CommandInteractive(); + public Heal heal = new Heal(); + public Feed feed = new Feed(); + public Repair repair = new Repair(); + public Seen seen = new Seen(); + public More more = new More(); + public Extinguish extinguish = new Extinguish(); + public Home home = new Home(); + public Ping ping = new Ping(); + public SystemMessage system_message = new SystemMessage(); + public EnderChest enderchest = new EnderChest(); + public Workbench workbench = new Workbench(); + public Enchantment enchantment = new Enchantment(); + public Anvil anvil = new Anvil(); + public GrindStone grindstone = new GrindStone(); + public StoneCutter stonecutter = new StoneCutter(); + + public class ResourceWorld { + public boolean enable = false; + + public ResourceWorlds resource_worlds = new ResourceWorlds(); + public String auto_reset_cron = "0 0 20 * * ?"; + public long seed = 0L; + + public class ResourceWorlds { + public boolean enable_overworld = true; + public boolean enable_the_nether = true; + public boolean enable_the_end = true; + } + + } + + public class MOTD { + public boolean enable = false; + public List descriptions = new ArrayList<>() { + { + this.add("Pure Survival %version% / Up %uptime%H ❤ Discord Group PyzU7Q6unb%playtime%\uD83D\uDD25 %mined%⛏ %placed%\uD83D\uDD33 %killed%\uD83D\uDDE1 %moved%\uD83C\uDF0D"); + } + }; + } + + public class NewbieWelcome { + public boolean enable = false; + public RandomTeleport random_teleport = new RandomTeleport(); + + public class RandomTeleport { + public int max_try_times = 32; + public int min_distance = 5000; + public int max_distance = 40000; + } + } + + public class TeleportWarmup { + public boolean enable = false; + public int warmup_second = 3; + public double interrupt_distance = 1d; + } + + public class BetterFakePlayer { + public boolean enable = false; + public ArrayList> caps_limit_rule = new ArrayList<>() { + { + this.add(List.of(1, 0, 2)); + } + }; + + public int renew_duration_ms = 1000 * 60 * 60 * 12; + public String transform_name = "_fake_%name%"; + public boolean use_local_random_skins_for_fake_player = true; + } + + public class BetterInfo { + public boolean enable = false; + } + + public class CommandCooldown { + public boolean enable = false; + public HashMap command_regex_2_cooldown_ms = new HashMap<>() { + { + this.put("rw tp (overworld|the_nether|the_end)", 120 * 1000L); + this.put("chunks\\s*", 60 * 1000L); + this.put("download\\s*", 120 * 1000L); + } + }; + } + + public class TopChunks { + public boolean enable = false; + + public int rows = 10; + public int columns = 10; + public int nearest_distance = 128; + + public boolean hide_location = true; + public HashMap type2score = new HashMap<>() { + { + this.put("default", 1); + this.put("block.minecraft.chest", 1); + this.put("block.minecraft.trapped_chest", 2); + this.put("block.minecraft.barrel", 1); + this.put("block.minecraft.furnace", 3); + this.put("block.minecraft.blast_furnace", 4); + this.put("block.minecraft.smoker", 3); + this.put("block.minecraft.jukebox", 35); + this.put("block.minecraft.beacon", 35); + this.put("block.minecraft.conduit", 40); + this.put("block.minecraft.hopper", 8); + this.put("block.minecraft.piston", 10); + this.put("block.minecraft.dispenser", 10); + this.put("block.minecraft.dropper", 10); + this.put("block.minecraft.comparator", 5); + this.put("block.minecraft.daylight_detector", 25); + this.put("block.minecraft.beehive", 15); + this.put("block.minecraft.mob_spawner", 100); + this.put("entity.minecraft.player", 15); + this.put("entity.minecraft.falling_block", 10); + this.put("entity.minecraft.zombie", 4); + this.put("entity.minecraft.zombie_villager", 8); + this.put("entity.minecraft.zombified_piglin", 5); + this.put("entity.minecraft.zoglin", 8); + this.put("entity.minecraft.ravager", 80); + this.put("entity.minecraft.pillager", 20); + this.put("entity.minecraft.evoker", 20); + this.put("entity.minecraft.vindicator", 20); + this.put("entity.minecraft.vex", 20); + this.put("entity.minecraft.piglin", 2); + this.put("entity.minecraft.drowned", 2); + this.put("entity.minecraft.guardian", 6); + this.put("entity.minecraft.spider", 2); + this.put("entity.minecraft.skeleton", 2); + this.put("entity.minecraft.creeper", 3); + this.put("entity.minecraft.endermite", 5); + this.put("entity.minecraft.enderman", 4); + this.put("entity.minecraft.wither", 55); + this.put("entity.minecraft.villager", 25); + this.put("entity.minecraft.sheep", 5); + this.put("entity.minecraft.cow", 3); + this.put("entity.minecraft.mooshroom", 3); + this.put("entity.minecraft.chicken", 3); + this.put("entity.minecraft.panda", 5); + this.put("entity.minecraft.wolf", 8); + this.put("entity.minecraft.cat", 8); + this.put("entity.minecraft.bee", 15); + this.put("entity.minecraft.boat", 5); + this.put("entity.minecraft.chest_boat", 5); + this.put("entity.minecraft.item_frame", 3); + this.put("entity.minecraft.glow_item_frame", 3); + this.put("entity.minecraft.armor_stand", 10); + this.put("entity.minecraft.item", 10); + this.put("entity.minecraft.experience_orb", 3); + this.put("entity.minecraft.tnt", 70); + this.put("entity.minecraft.hopper_minecart", 20); + } + }; + } + + public class Chat { + public boolean enable = false; + public String format = "<#B1B2FF>[%playtime%\uD83D\uDD25 %mined%⛏ %placed%\uD83D\uDD33 %killed%\uD83D\uDDE1 %moved%\uD83C\uDF0D] <Click to Message\">%player%> %message%"; + public MentionPlayer mention_player = new MentionPlayer(); + + public History history = new History(); + public Display display = new Display(); + + public class History { + public int cache_size = 50; + } + + public class MentionPlayer { + public String sound = "entity.experience_orb.pickup"; + public float volume = 100f; + public float pitch = 1f; + public int repeat_count = 3; + public int interval_ms = 1000; + } + + public class Display { + + public int expiration_duration_s = 3600; + } + } + + public class Skin { + public boolean enable = false; + + public Property default_skin = new Property("textures", "eyJ0aW1lc3RhbXAiOjE1ODYzMjc4ODA1NjYsInByb2ZpbGVJZCI6ImI3MzY3YzA2MjYxYzRlYjBiN2Y3OGY3YzUxNzBiNzQ4IiwicHJvZmlsZU5hbWUiOiJFbXB0eUlyb255Iiwic2lnbmF0dXJlUmVxdWlyZWQiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS84NWZmZjI1ZDY2NzIwNmYyZTQ2ZDQ0MmNmMzU4YjNmMWVjMzYxMzgzOTE3NTFiYTZlZGY5NjVmZmM4M2I4NjAzIiwibWV0YWRhdGEiOnsibW9kZWwiOiJzbGltIn19fX0=", + "PoUf4TsNx6SVHTWZJ6Iwk3acWyiDk84VeKBVcOeqimaSBAGYKfeuXRTFV8c9IBE9cjsRAVaTGC/mwRfHlcD/rmxeDDOkhsFVidr8UL+91afIO8d+EnyoBghmnbZonqpcjCv+nkxQ5SP93qTDelD3jd8xF1FAU97BBvrx0yK+QNn5rPg2RUGGoUZUg75KlEJds1dNftpHc8IyAHz/FQIywlkohu26ghOqFStjok4WPHD3ok0z7Kwcjk7u58PYf67TkEGnGbmxTUDlNbLmxUqjxCr4NshS+e3y3jRfJN0nP82dbYh/NP2Fx8m7pSMsQtm/Ta2MN7JC0Pm2yvZB/APNoNHVSZZ2SOITbPF/yAkIdHrk+ieCKqDbeuc8TFs2n+6FktYdwPXcqrK266CzlSTPycVZQeyrgrOI+fqU1HwCz+MgdlcsAdAoyuFlFPaVqDesI46YPsSJzA3C3CNhjvuebOn357U9Po82eSFAPYbtBPVNjiNgiqn5l+1x8ZVHImwpGv/toa5/fUyfMmlxijwG/C9gQ4mE+buutMn9nfE1y/AisU/2DWeFBESw3eRAICcmVVi875N8kT+Wja8WsbpDCw+pV2wZC3x3nEdOceAdXtDEb0oy3bQPW3TSZ+Wnp68qwSxjI/aDosqVuyyqqlm+w/irUmNHGL+t7g/kD932g0Q="); + + public ArrayList random_skins = new ArrayList<>() { + { + this.add(new Property("textures", "eyJ0aW1lc3RhbXAiOjE1ODYzMjc4ODA1NjYsInByb2ZpbGVJZCI6ImI3MzY3YzA2MjYxYzRlYjBiN2Y3OGY3YzUxNzBiNzQ4IiwicHJvZmlsZU5hbWUiOiJFbXB0eUlyb255Iiwic2lnbmF0dXJlUmVxdWlyZWQiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS84NWZmZjI1ZDY2NzIwNmYyZTQ2ZDQ0MmNmMzU4YjNmMWVjMzYxMzgzOTE3NTFiYTZlZGY5NjVmZmM4M2I4NjAzIiwibWV0YWRhdGEiOnsibW9kZWwiOiJzbGltIn19fX0=", "PoUf4TsNx6SVHTWZJ6Iwk3acWyiDk84VeKBVcOeqimaSBAGYKfeuXRTFV8c9IBE9cjsRAVaTGC/mwRfHlcD/rmxeDDOkhsFVidr8UL+91afIO8d+EnyoBghmnbZonqpcjCv+nkxQ5SP93qTDelD3jd8xF1FAU97BBvrx0yK+QNn5rPg2RUGGoUZUg75KlEJds1dNftpHc8IyAHz/FQIywlkohu26ghOqFStjok4WPHD3ok0z7Kwcjk7u58PYf67TkEGnGbmxTUDlNbLmxUqjxCr4NshS+e3y3jRfJN0nP82dbYh/NP2Fx8m7pSMsQtm/Ta2MN7JC0Pm2yvZB/APNoNHVSZZ2SOITbPF/yAkIdHrk+ieCKqDbeuc8TFs2n+6FktYdwPXcqrK266CzlSTPycVZQeyrgrOI+fqU1HwCz+MgdlcsAdAoyuFlFPaVqDesI46YPsSJzA3C3CNhjvuebOn357U9Po82eSFAPYbtBPVNjiNgiqn5l+1x8ZVHImwpGv/toa5/fUyfMmlxijwG/C9gQ4mE+buutMn9nfE1y/AisU/2DWeFBESw3eRAICcmVVi875N8kT+Wja8WsbpDCw+pV2wZC3x3nEdOceAdXtDEb0oy3bQPW3TSZ+Wnp68qwSxjI/aDosqVuyyqqlm+w/irUmNHGL+t7g/kD932g0Q=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYyMTcyMTI4NjI0OCwKICAicHJvZmlsZUlkIiA6ICJiYzRlZGZiNWYzNmM0OGE3YWM5ZjFhMzlkYzIzZjRmOCIsCiAgInByb2ZpbGVOYW1lIiA6ICI4YWNhNjgwYjIyNDYxMzQwIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzQwNGRjZDdhODFiNzllMGRkODUyMzE4ZDUxMDRmMThhNDA2MDdlODA1NjM5OWZkYzUxNTU5ZjhmN2M4ZWRiOWEiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==", "AiHSx+/7R5CjzeKxfYRnBMu0XW4uWGbSW30WqEC9q2GEV8aacMTJRPg5HNWXneCHJ1CNdiXBgs9meaw+uffKxm05KvrN5DwgQbU3yhf+g3megmN/qk1fmIgsMBEncD+NEcyMF7WQQ4GZCrnyfHbvCFetmgwkJr4On8vlnkGxiUuRuq+6FmntsVII5pS7Vyv8hrv0YDOs5amaggtlh5L8RGbRp10JSMSsno3fF2oPFUmwnyQKEIQnzhRbO9Fi+KtKF3nQyQG0N/I+BsBtVYcuoX/UQ+rjGDWxNiwgELuUrx3bFTiUBDGEgRMFP1JrWe9LPtkhcGfF6hBHL3vJyim3wvGU5L3S3x+aXr8Tv/sR0BVppeu4pSEZV+FgXeDKMWWHzNd2NvhnxpbVUttTycKSuAQHV4cc9AL32Oq15Oo3lBtvtspIa/y+VF9nesO5d+K4Ys6fW3pvNfFK473JmFRl3LFwdCbhBKsLDerBKm/UL51d/aT0xpqSSYKumjDgWaS6wQVhTqICf1UZjQp2OM9Oo/bo74e+exUGsJsQiaSqGF9YuR5yAqosj1wsFfaV/kPQO2rDH7Yj3aEkQEmLubukLGiBbXADSDG23ZzQkDDreznlGmHeYhSf7XGh/LNE45Dtd2iG5FHr8DAmqm+ipQ6xkw7/SNmqDJr+6JgCFNSzVIg=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY1Mjk3MTQ5MjU2MywKICAicHJvZmlsZUlkIiA6ICJhOGJhMGY1YTFmNjQ0MTgzODZkZGI3OWExZmY5ZWRlYyIsCiAgInByb2ZpbGVOYW1lIiA6ICJDcmVlcGVyOTA3NSIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9jYjMxMDk0OWU4NTU0MmJmNzA5Y2VhNzk2M2EzMDFkZDMxNGFmMjk0Yjc2YTUyYThkNGMzOTVkM2FjNjk0Y2IzIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=", "keO3tnrsR27Dx7vWNWSpMOd4YP5jH1PEilbkOfOhvpIrlYh8ciYJl002kXDvlG8gFrujUpVuxf5VvT/7svtZokoGkgGY3Z2yKpdGiHtf4FVwQC4Bro+GRPdkxRf3nRNLE4fbkWy3tAhm6beqCDpbPcJ5NORYdQDcWx/AniRqskiz1/xb5S8FV6293IOvlLvVV4bnESe1bPfk/g5kAera2yjHmmg+sIBcQxbIzUdFsfsLRh/se8mOW5jS368K1MP1iMMFkMlW/pXG17ITA73cJOK+N48UAyjM7kVu+Yx+zWG4mMAarUdLM1apT+lsFCVe4mKq2JjFw6vliUYG3Y6LCBm2JhR/N5P4cEfs63SmsuIm9zhDe1yJsNU6Io4iQTjr2NMpcq/lbK0rZngSlUVzADptcOOY+ERbPLbC3nWU7QbPnupgTOsIxI1RQGKyG1MV24PDgSKlQVZ1P7GyIu785FBvxd64inXWGk/GQmZ+WytzRbhSCYsP2RCbnyBqEL/qp0KbvCjd0P3WgxSfjwBUFiIbKBf9SNKkOp80KO4azp0XD/cNezhdeTXRGgB5EMgFi22cEV1dn0d3I5Zut5vNgQrptUU0fwHPizzwykGfzCNUc/tkSW91j1Gh3z9AA8JMlmxcqVG2kfUZqKBbmsrV5jewO+ctppC+1Qi4mgUPixY=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYyMDIyMDc4MTQyNCwKICAicHJvZmlsZUlkIiA6ICJiYjdjY2E3MTA0MzQ0NDEyOGQzMDg5ZTEzYmRmYWI1OSIsCiAgInByb2ZpbGVOYW1lIiA6ICJsYXVyZW5jaW8zMDMiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjVmMTcyMGM3ODBhNzk1OGI0MWYxNTNlNTA2OWRiNjg2MWJkMjgxYmU0MzJlN2JjNzk0MTE0YTdmNGVjNTJmZCIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "olnQih89nZxKFe0UzWiXU8+wlndGBClUqXxAabwEm6j15SZH9ue7Xd2OM2kRKBiHoqbT+2TSg4xG7cBeAeapVN4vpRP5NPujERl/JI41jYNhMb+DmskreS59fh0QfZPAxOpj/rmmAJVfNN1QblxRM3wlMGaEgS5TH9HfeehgLrBaaDM8/JAgnas4Yh6L0uRoNebjXHrhqgguVBMF3xsWpvpAPCzQCYX2vjCCF3WtOEy7EEUF4u5Lo4teQhr9yfnYGBc/ktE4I0MByqTaKrLqvF45n4jOShPP0RcmLh9JpOXyrScRuaUDhQ8bd8xhkWEb94HMzwznvDLNh1/nbNybCMb5GydYf51hJVfqjU5TMWID71F8FTTBJrCZDBRESFIP+QZ3czYP+urgzmfLgDmcoPIukMaHWLU6qFpTF0QazAgF4u5Fe4J6QEZSyZz0B2kqQG3vN1dXxLgHItjQbEeceChNYNjuZFOTleXzpYkg5/4Zqy6Oek3bMscTYY7IPBV56WiO8eGw5JYMfyDeM3iyh4ZxLEC3HDRtOTBHo7WxWPR/AUOU9HP9CdmKQbGThGAUuqlqRJzbg5XNRvKIcnngI329VZV5RmAnt+G5Vfy6uqBagpMQZ3720PXPG6H5q4SBuXmHt1ccKgJvQv9lTh20EymuIALTnCodr8qDbnRfdrI=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTU5MjQ5Nzg0MjM2NCwKICAicHJvZmlsZUlkIiA6ICJlM2I0NDVjODQ3ZjU0OGZiOGM4ZmEzZjFmN2VmYmE4ZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJNaW5pRGlnZ2VyVGVzdCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9iYTZhMzY1MDIyYjE2ODVlMWVjNzgxNTU3Zjk2NDE1NDA3ZjQyZDY0Nzc0OTZhZTliZjYyY2UzMjkzOWQ0ZjRiIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=", "il6M+JLeg3Le2Z07Oo9DHqw85krb7PFJCSTvWE23pq3RzRa/YnN/IHoSkeE+Sv1XxaHfoPZa0+B/7n/SzU1AMoXDQTPigvRVXRT+i/dfCEuQb3gfbi3OW4LjhpkAnP/s6Vakrfurpm09JkjDyISjLpBEU8j80nTn1td+yuCS5MIGNGDlaq7nL1S524osMKrZzY5nIfGxGL9af8JMW2GcM65VYWAJPUA1YtU1OFA1dVmu6t2yFna6NEm58DcE38GeEYLE23v/HLgYNdb1euNtFFLXMdRiqtTtP6066RjgvRGhFf6CRRf3t0Z8xfGvetS913HuqA2Z/5fu2noHP2YRNkll28RLq0D7wDuePKpSD3/Gk7vFYbYCJ5FSEnLoc7K5oTpcEowZAMsKf64oI3VbE74D8quSXNT8JKek6BlxKVgkas/Wzx01k5OzWLwfXPtWexPM2HmfvCzO7GVAotSSIG4yf6b7qZuHtXSHcHT/g3wsTi+r9Is5GW7t9d09lh4bYhqVijEOyhKTkWjoPIJxeMX1CV2HKhjcNXIhI4HkdSlRLRcuO9jl0AGBkH25py220TCWKLDcXKp+ZMdezYsOPr3F9LhZ0xYuJPdjG+vsMlfy4QLBwcsxvqV5XIYpX5csTXiwl2Dv87YK4MpCkNLoWImi/o5pbismxQWdAhhVgQk=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYyNDI0NDczNDU3MCwKICAicHJvZmlsZUlkIiA6ICI4MmM2MDZjNWM2NTI0Yjc5OGI5MWExMmQzYTYxNjk3NyIsCiAgInByb2ZpbGVOYW1lIiA6ICJOb3ROb3RvcmlvdXNOZW1vIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2IyMTU1MjA5NzQxZjRlOWUzMGExYjZhNjEzMmM3OWM0ZDMyZWRhOWUyYTIxMzc4Yjg5Yzc3YWYyOWQ4MmZiODUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==", "eAMJqWmhSM5ZTjaUep3duZbS2HW+2/uLebRfTXbNe7U+ZSZSz1GxZ9+92H60PyFcZb0vlGB7fZAviud4s39n8Zae9IirwPG1Ad96D7LK5B/E+jsbDrtwcywHH29nl3FgWjWy28eqIi1z/gmG8tQ0RzjBHUQzhaSnqmwQ0ea0cYLLNQqDMoXPKNOfG79VG4SDSR7m7fywD23w58bDiXEJbD/xwx4wQm98MzIyOg8UNjXaAsRyHKj5DlIGh7+1NbemChcoaLb4gkNCWfkKraMHT4n/J4zpjUMJRcF2adhhXXtnldPRupZXkeA45mOW3Jhamg8UGESANzhZsnXjQ819CFOjl9WusRHFEzQyOPFOtg+340log054xdTzzbRwsqfF2b5YS+LItRxPCut8ZcCqu37wwLjx1e4Jj9yLvYMk9TIcD2V+UZ3oTFuNNzy55z1rlKiKxjQcSkMw20eRdySBCRqafqNDKEPJUWpMcoej6ALS0UjRyrKlz2FGw8gdQrS+dd9czUFoXHpAEvvqvEsQcQJ/QK+uazwiD+2QsW+XRmtj2fJmE6NJCmyBvGRuRiH+fvTmdQ0saBiJV/wc20brk4V/3eLp5S8dQL3PMo0EuesTJ5j7kjr/FC6NfxPEity0H6bNPI4PmbtHI4ujYytFSH3m6IgoqWvhZdp1pWLMQIA=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYwODY5MTA0OTcxNCwKICAicHJvZmlsZUlkIiA6ICJhNzdkNmQ2YmFjOWE0NzY3YTFhNzU1NjYxOTllYmY5MiIsCiAgInByb2ZpbGVOYW1lIiA6ICIwOEJFRDUiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTc3NWYyZTE2NDFiNWM2YmVkNDc2NGIzYjNiNGFkMzkyZTRmMTVhNzgwMTU1ZGMyMDFjNzY2ZmRlZTRjZTM1ZiIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "rLEtXBQQihP9d8yww6zvbfPCU//nb80/rM0SDz1qTcPQrDmGKXLJtoIeD0q2J/IqQi33ucRAY2bEiWSMDUHpC3ry1EQb73ut1sC6ErcCa9/F6PT0o8bcbAdd9JkIjtEmumy516xNe5mejXULZC7LDkZ7nXyJgAJ6D3jaAm0o89kcwt9ofr3IVEw+d662ZpmIjJeEekX3Y6ExqKTiKrFV1soZWlnGyvAgTJBNuvFYu0uI304p1o9K4ePa9nSa54fBAc04NYktowLrRgG3so8SgYYCzCtYxaL7YFNIPnmUEylcXVomiPshE5hEhtVOM98rQLr7hoKLiai2IUPILqGJoO7C9TJYbevrM+so+ukd3SPfFC8LTt3/VrPFqWKbt+8D8lM5qgPpzBQVCpVXV6K7mpnCA/CewMvkKRYMbkB1lUDgcj2nkH+mzPZaa5bljrf4FMvwL+OpNdV394IWjCeZ8SObv/EqgsWD6X4k7IOWtvqraUg29sMIkU3879IhRBdSpJUChss4sVk5fxh8l6mmNE68hYE+BvzV4sn+i9bbnjBCUeg6nu2zex7Usalv6Z40od3LbZGJRTjjekYfYUzcsmTnfRYFo6+Ae9X2ZxXRNozA4Nyu/4LbiWCAxM0bRwgzaOeSVCpWFbFBu+HYqBA7K6OUJtgtevlMoscoQfJPe4k=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYyNDgzODYzNzc5MCwKICAicHJvZmlsZUlkIiA6ICIxNzhmMTJkYWMzNTQ0ZjRhYjExNzkyZDc1MDkzY2JmYyIsCiAgInByb2ZpbGVOYW1lIiA6ICJzaWxlbnRkZXRydWN0aW9uIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzE1NDgwMjk0NGRkNzk4NGQ0YjFmMTJiMjUxYTU2ZmFkZjRjZWM2MjM3NzI0NzEwYzMyMzAwYzQ1N2E0ZjYzMDAiCiAgICB9CiAgfQp9", "XQvO7IqapXVFto9HTjaEUWX59IhlWXGGa/fo0NCuN5PJRzxaLGDLd/IaU7hYmDfk7gqMc8fHojzOFD3722UUHCGxeq88q0vDp3yUjVEeVEL9KofTyH2XcuGrpJxG9UYPrmdGVVpOqxHRJOloJW+zezPLY9Qh+VADFy+ElCeAhNahvUL6JrWNcxUODj0PG9g+GJmIKL5MhsWYsvZHzMYkrI+clDJTVGGh3iSjSNb325emmuNhkIGzAyf7XW76A/FEZy1UOYZol4lc0CWa83QSqhwSoZ6q4PbQAfEpC8xnRXsuLUBRcE5IqIkRr6pdNyRLpZWjlPUY+Qcv71cg0yg/kR2BNtgS7Zk0NvGHiqsndZsyoH/12bersSjoNrRdEEmWJ/mRW47C/qt/Syq5KJWetJf0eemKuo6opiKbuySbCly4uM7EjPqUDFnXUjVbAOCzDLlimnGM4g6OYrBSamyE8P1wocurCrRxW5sZSuauKvUe2xRI39m4XzpBHX+8qaX16oDNkrmZVlR4NKJ5uKMaUgcOXIY1TnFJlv96urZquEKmEfVotS7dT8srdgxPwvffcKgdQmFUZFiD2fkgVrKIkGUkzgr1EufvYlb7pkDeMzBj8q6NA0m3+5cTHB7CCa7FfieN1/JHFXiGBZErlENQ7PS0kSOgCBhHwFTkkdTvbNU=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYxODM4NjExODU0NywKICAicHJvZmlsZUlkIiA6ICJhYTZhNDA5NjU4YTk0MDIwYmU3OGQwN2JkMzVlNTg5MyIsCiAgInByb2ZpbGVOYW1lIiA6ICJiejE0IiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzQ0YTI2M2E5NTQ5NjJmZDhiZGQ1NjhlNjZmNDFkMGNlYmJhNGIwYzA0NWNmNzBhODA3NGY5ZjI5ZDcxNzA1MGYiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==", "t2oMBS1i6yONHoiFacR5qjzySyYBhrcy59PAGba6cV4sQBBKn90++r1+5zxZDrLZ1ZE9hKvVPpxzicSemzbdpz44EQ6lJLA4APrA5y4JN1aamapZS4PUtIvlQAoFEY9alu+YEhiHNQUzY3Bvb/32jxkUv0jXYmOhrQBg9cfEexE+umXnEd8lakjLUOgNw/th4cr3GDRUKY0SP1fBxx1BPmXjNwYhrZI+XzeLVDRKf+LxIOGxcGv1ilyQBw6M9bDLtPTnQZ9OYH4Kh5W72SddicMR96+vARgsLVoOxXWRi528ka869fWYTu/uvO3494eNeT3eCyzv7bzE0tnGKjvmYsIBL3RWhkbCTuhi46BBwLqBA09GdIsODocVbORMyo/ePkyLNwlM0QZr5V4eR3JSVCOZNkWGBhV5txVe2cdOIUJcNo+dE86VG2UraZg+wtUewylXQiD/qc5EmvZ77b9IcPuTaeB8aJB20xXd72fzqkp5A8fqRxjVPkTGntNBqi6gdETP80OIHcOcPSYc+nUKNv/o2URrGo9IbDORP9FjZDZA4k3/CFe4ALvc7lzdkDZhJEQmS7bLJlW9fmgnokgK2ZlPbCq/dU6zEv2HjwhwFpjhFmw0vjfw9+6/sc95obHjdeB8TOIRxaG9ES1SOkyOSCx1b836C9PEcrjwfApmlTw=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTU5MzUyODQ5NTE0MCwKICAicHJvZmlsZUlkIiA6ICI0ZDcwNDg2ZjUwOTI0ZDMzODZiYmZjOWMxMmJhYjRhZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJzaXJGYWJpb3pzY2hlIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZjBmZjI5N2I3YmYyZWY1OGYxODk3MDUyOTU2ZDYzMzg0MGYzOWVhMjdlMTIxMjY2MzMyMWExMzJhMjJmNjUiCiAgICB9CiAgfQp9", "L0uf/pwI5ayEiFlR19U3YJQI1fKQfLt6ip+xxZbdcxeTPBe3BZYzeOgbogT6/iCDz+WVRN2nW/JDHhT+dNX23MYXS9N02bR0+dwigtvWbCMfeRfwHHR8UGIQbLHErt+TBAYdgJW4I+KlK7Z8DFG37dlvLm66E67YHV9uWudKdMkdpa9ycD0vD4AniT/7LrNVpxBXk0HjG4WT4IYV4IAps/HqsNxWYmWITn1T4uJmDLtRLdxSmRSPhJBqIx8BstQV0O+YlI5ZbSjYViyvOslQY1GDNNoU8VNFIrEUVjn6qBFPMfBt92p7znfvFRvbE3yQx9XQJRM2hGzGf/WAGubGqayL4nIUVouiRqSH77q8AcUH+WEUa5dML0FOEaMw1UVkn/sBwPMyTHlw+P3dClo92NEi6+BtBXu4ocMaaJ7SH0ncGhI09gixq4zpoWXsiT53HIWJiD0dtlmFp0dzLrA4gduySLWohJLcaMf5LOoncqg7JiUIXLfmE+HX0GtCjpZKezxKeJIBidntZ5r0e7y6Jy8GoJ9dpOnY9bnwbYO9XGXFTfV4BpFLcraXv9adZGM+TXuNo0Nt/peq1bd7extfR3mQVRlgMg9+ni1734LoFxeeEWALRWDGzB0fkVPXLQU4YL/ekJfEM6dxwyE30sf0JmyDoffEk4mRZPldCWBjU34=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYzODY4MjMwMjQ3MSwKICAicHJvZmlsZUlkIiA6ICI0ZjU2ZTg2ODk2OGU0ZWEwYmNjM2M2NzRlNzQ3ODdjOCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDE1IiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzE0YjRlODI2OTJiOWRjYmY2YzEyOTJkNjJiNzMxN2MxMjRjNGMzOTQ2Y2FiMjFlY2E4NDJiNDJlNzBmMGMwMjIiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==", "renIrkVFQUor3+5AeDYOcGtUnUpdk9/i5ANWFGCBTSVZjeKQ/t5xDEe9kqwsWawR/55N2+1Db2c7lIpHpJ4cGqtG7BzTm+TZNUgSOu0rG27DwxheiuGbYSMm/lQSiNi7FvRlhLXuxsYZ0nHhXKoeG4xW5PXaE/zjXeXR1hffnfR/ROanmK/m2nIbkfPo59wjc+ZTF3nxhX+tGay+7dy/Y6xqhyZ4ZnM1a9+z8hC8ERgXzUUczfhRaDPQcv9dEdpyQhlfJyEV6r6NBSpBVVNaZ2bGs+VyxrRVtr/nXigps1KtFXH3j+gBiNYJWu7LpDS+1DTezlP9qkbDUPSKuO1O913GDRdJxdcVn7HGYD3W6yGB0r6sDBvb7RYESMzafRIFbBjhJrJFi3/aQjxTuFSc66bUkDqNBGYQcXyUXP1wEuB22mwQABv2OZiFdXMMRDniSZvPsxoriDdAS+umHcrAgTApu13xLyJJa8tFBD9rpGxDDoUbnNJdzZSpjrgfu38Kgpa3pW45HY21eSOubQNdz7qBTBmQwVViuVoAqH9mM/HqeIrGzwdRJaOH3GsZRofr4zh9HVc+5o02W72d39BskA56ae8zjGza9sF1jhkgiaW1NH/zuu7LnfujjvvMcczrddv8P1r7yqsIwUrP0ObB+ylsCsrb6mAV5uqXuklS7e4=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYwMDgxMTU4MzU2NSwKICAicHJvZmlsZUlkIiA6ICI2OTBkMDM2OGM2NTE0OGM5ODZjMzEwN2FjMmRjNjFlYyIsCiAgInByb2ZpbGVOYW1lIiA6ICJ5emZyXzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDU2NTViODE3MmVmOTVjYWI4NDkwODk2NTFjZjU3NmFjZWFhMTBiMjExMTQ0ZGRjNDdlNzAwMGM3ZGE2OGM4ZCIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "bK4ylZquICTUR1ZzPJYdqZmD5oHXRJ4GMhHrsqOY3zY2ZKMoFtCTVI5LaYohazvV+jmttnV8VuAIo1v+TvOnHwu6spAdXtRkPTSOtyJTXb8+01FonUXxyEx+AMyWGRPPQpjGOjHfJQP+GauOn5L9M0hgadWdMokcPUUv9fw0CkcJdBMWmU/i5MxnMjVHIZ+5bRLSQTVHTaM2LrtD2RmSv7ZvJgr9Y0syw2Qmr2KVC5ERFCgWdLdZwwrR9qC0PLnik3DuP89P+qXhqUzJ5N5NZS9nsEhVHKEMrkfiHbundeqw62aHqP144I+mYj+Q3tgLO4i35MKKFTMhnuwTbjK2AR++rREEtTPjZ1zBGVNfFrkrFfHPKYlr1Ew+wriLCUtSXRFWoebH3xfQsFYTjctFFy8Q/Wh7jyFEdImnCCobsU2qfQsq+tEa29oIXbYWgKbKoW9f9peVLo3PfcN9A3zC8BLSJQtxRA6WcTMLGUaswfL/6GoL6KEZY3tNyylieic0Aqi+f5HbjDiow6upPAIPKSy46bYXI7s4UMM1+gOjF3pr0Rm5iIIY5jfvuOGkC864Ox8K1z8W4Hds+2+r9iGzn/pwNvNAX0zw2Lwb9adcrrusuIRTggHpWzWIn/9UpuY19eqrEHQNGVqOiNkOY3Y5f7k5lfap1lLu6ls8od+PMyQ=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTU5ODE5NzM4NDM4NywKICAicHJvZmlsZUlkIiA6ICJiMTQyNDBiMWM3ZDQ0MzkyYjMxY2E3Y2IyM2NmZWExMCIsCiAgInByb2ZpbGVOYW1lIiA6ICJOZXBpcnVfIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2IzMzhmNWFjZjU3MTVhY2E4NjJmOWVmYzY1NzRmM2E4MGUyNjZjMmQ0MDhkNTcyODExYzdlNDg4MzA4ZDUzMDQiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==", "Jfnt3pOSwq3nFYZUXqlP1NRBHfQBZJW1OEklB4ZKp2XdBPmLtVuk1jPN10RD792apdLReiz4Qt2HIWZj/S41nc0QeP6FdTQc/50CngAtPMTk3RCtDLYrGZN8QAjZ3xYn5kPsolthvKYS/csQzNT1NJM5pQYYtQX1aBCYrvWlxJsX1F/MYpG94JNUiMyeU4zaZZgNeofa3YqSJ3Ys5A/W3gPcryDyZbj4x/rrREBAUByzZu3SnF422XefNIz81OowmmVdvtNW2/vosV94OhXoa5AQMgtudOhXV+T8PZS1KlFwWM6rnFBuowlULZtodxRTzBdxRyT/oFVsTsH1RNjJ9l4sYW4gGj8qiLxf92mqeHvIdw7U3391LE174xP+BhCV75EMb1h+2ARASmIFtvAt6D2SoWPGqnO1PyR0KBBqg+RVgaTd9pBLvjboanhd7HYyVSlqY3fSsdT/Yy/+pscrREN2HTXW/4LrntsoYz5OjfhB1t3hXhtU4cGL03LFaN95qN5VE2oKIVvDqGdS9PUcAaSRqIa63qot1oKQzrHR+FjmILs+erBITj0jplgszysOZ1VU71WOhqPpOb58qhudVcOxHDEySW8vr98gdJ55knyKdih2oeuJknds20n3DApdnXmW8W/LowLUTqibxusx/lmERNgIWoafPMSPWHV0Fk0=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY0Nzg5MjUwMzUyMywKICAicHJvZmlsZUlkIiA6ICIxN2Q0ODA1ZDRmMTA0YTA5OWRiYzJmNzYzMDNjYmRkZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJnaWZ0bWV0b25uZXMiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNmQ4MDU0OWEyNTJjYzI4ODJhNDUxMDc4NTUxMTNhMTg0NzJlNmI1YTVjNjE2YjI4NTc2Yjk4MjVjNDUzZGMzZSIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "LtRvG/OteephxnjkSQfrdfbDUWUhoG1p4L2pIit1e7KF0UV/5BjgzfQUt+MXRrmJTGQVoJCZzHt+VFHT9UL1ptguabvM6sbBWaVmg7RPo1KKBYkmai+dP5ceCXPA2StVogKN+JjRwNF6Paw850IVha6h90It2VRk43/IbMzdSyTnMIH92WLb93BvENYX00yRCJY/m48tYECibH6FtX6vgqK6UgOgNVqW0g6Otuwx2Z4Pi0xUHn6i9gCayPrWSg6Y7cVQ5pM49t1A9tfN+Kt4J+sB63Ez0LSws3y/5MP/8sFUBQXKaZYpATK5dmBohZao9wroX9Ni9sADzPcGF3XAcHXZjRr2Qk6Y0AsukuAlXMPlDWFGxnVK+jElwcGEsx8g8cK5iufAuDAQLQSUJ6vgBUE8DndEfL+l8LpebRlgMT4kZIn75ZbhY4BU2zvs/fUp3mPhQjjkrTMqwofDFj6YxnZksUi+qwEIcRc3ysoFl6phMN5n2mRV6616l6DTSh6y7Vd6tT4s6UsfWlFUwC0gdaPrU0CSX96Afk+BSVceh6qxs0IJVBn1bBe0uDTwK1a3yUOMXvjosG0L8jzpNY7sGE6ybwHUxPMaNIilqvnhO2NdSgsNHVLlYHzh0jUMs5ap4U+9fAEYYr0OpkqgZ7l6aQeV4ME96RrGqZmEkxJPjas=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY4MjUyMjEwMzIwOSwKICAicHJvZmlsZUlkIiA6ICJkYmEyNmVkNTk2ZmE0NjBjOTZjOThhYWYwOWM2MDZhNyIsCiAgInByb2ZpbGVOYW1lIiA6ICJPcmlnaW5hbFJlemEiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTQ4MmU5Y2JkMmJiMjg3NmNkYjczMjQ5YTNkMDhlMWU1MjljNTk1ZDExNGUzNzA2Zjg4Mjk5MzNlOGE4Y2M2ZCIKICAgIH0KICB9Cn0=", "j01rUhHFGAskv3OEkzdRDE5iTvnWWCjWQFfi0cFfe1IGWzLYK/bbtZK4rZ7Gt5o/tJdBHZH8MqU/ce9LN+mofLAqycWHJT0tUJZi84JjXsz0KufaX0ZPL9QO9ngohOUtbCk1TYwHIySe4aLiT6DU2hRocS0/HnjsFUG93vhznpxMNVMiwLRTxhRhATqPXnmnBt+LcLwrGHnKOUPDB4iwBDZTtSR8U9TYwYZXoMZ4EL9ND4xxw3U/MDsYde72pHpZVcJxFW01E/iZiVpe/TLFA8gIPG6K+ilDJm8wR4oDmjSLk3bRjwvChHLJ418s1wThSp465YknZpp5R5nwRTne7lkqHMB5k+Uv9IDewSEb3HVqYGTMzeZ112iyS3jk+ukWss0Fkvn/dZQ7T0tfVxlmXVezpwtWGvMgINXNv7gARcsoDmz7ZWISmfO20QXbV/SxO0ymgKzocZ5Sa4WjF8TbmqVVA2exbsnI0dfFoivTshJPgd4wxzlB9Qfey4KSMuxS4sQu7cztaEtvO3W3HaP3IApluYU8lyLNbCvEZ6nCkl7XUH2KVUvgzeFW4WrVHQBkiZyEiA+2RsOwKZSf1r6X4YfgGpNdwAAa5Iq11Ss1R14rh4t2oeYqBDqegAVIZPZR4hVZMjD8RsAbuiZngBPQXGEQAHI6qdgvlgiUbvnwWDM=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY5MzQxMDU0MjA5OSwKICAicHJvZmlsZUlkIiA6ICI3N2RiZmY0ZDIyYWU0YjExYTExMWZiMzE1ODJiZWI2YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJIYWppbWVLdW4yMjAiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTk0Yjg0ODkzY2Q5NzE2OWMyODExYWE4M2JmN2MzM2MzZWMyNTE1ZjlhNjcwM2UzMGI0MTNkMzRhN2Y2ZGE3OSIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "KxPrgCDUJOIcqqsWig1r8osPWar+Hh4EJ6Opz7MpBQ91wJxavmPLmxwZ8e3PuKtcSjJD1Gg8Czo/EcNBSqYfREr2z8Z3gOKw1+upunapcm76Z3wS1Vl+xtgUANU2CLuyfdaA2fvdW7RL085rBUsZRqL0DZlYath0LlWLChfbHaIEfhvE9nUBF6jfPYhiNLJr7EmBT91qm41fl1G0Yhs/OOI67E0VMXG9MfeHW8RBqHKhTD6Ph/CPmgLQlEjVxJnZjcEC9qkY16ANrx838HTuvRKMHWIlMYVAED03mYCvLo8wObApamkI13Iz+HWNaC0tdv7voXiZ2gjwJx2GO5RrZ6OUECkzPCC0HBGjAZDDFiClqpI1m2vITSqzuZcPe8fntpVGuNpY/euH4GCow5cBqowu0jSOj7PZmAKaaicvV1dZUGstWxCEoNCw0IuA/LMzxCRHlRMft3k3qk2FJ4z9HVVRZIohIHt3u4q6dD/y+5eI409bhzHJbP4sONLw1KUayNQpfAJrGab3MdtDXwRgjGTpl1L3Q+p6eQXpGvx9ABcxpXlzzHulx5+lvQEMJgFZVLyWV+OG/04esgEJeOtBn0/jFPA425t+ijWcZ2ruB4aqrZjegOYKIxQUdXKqP9JTT0+EMlwU57zwZZch6aSiPBObK5X9HHZVWTqqM1xV2oo=")); + this.add(new Property("textures", "eyJ0aW1lc3RhbXAiOjE1ODc4MDcxMTkyNzEsInByb2ZpbGVJZCI6IjIzZjFhNTlmNDY5YjQzZGRiZGI1MzdiZmVjMTA0NzFmIiwicHJvZmlsZU5hbWUiOiIyODA3Iiwic2lnbmF0dXJlUmVxdWlyZWQiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9iMjI4MTk0OWZlZDc2M2Q3OWYwODZiNGU4MjE0ZGVjNTdiZDM4NzgzODhkOTJmYWQ4NmRjMzQxNzE2MWNkYjJkIiwibWV0YWRhdGEiOnsibW9kZWwiOiJzbGltIn19fX0=", "N/b/lSK6Y3Wqm4lTj47YHbec9yVAj7XmDjfWhVAa033UNA30U8o+2pTY0aVDAzFut624iC3xjqMzBlXt7SczsT0w8EV+MnW51V6aPlanj5SQ6zVwB20TdhhAzBNvIQbvo4x4BL99ZpyBJMBRcCVEehjaD3rgshBxH6t2z7WzzYM1cij/5egedjhm8ek8DMdYYakN6DWIOWDv05VQSiWRMhitSI2sqJMTYKaJcLph7/56Ke5zRNtA2mwEcdB+GnDPkeEINzx3A0WG/vOS3iYL8L4T5Dv1GzBlq9s10R1K4Ks5TQLhVJ4Rp2S4COLvvWsgREHQVf6NEIOG2ww4wqTi/xmHni2d6TM9K+vtLSBE7umEvLeOzp8oqbQvtD1ipa0iatR8lEXU1bcGITtwZi+i+zLeOIfx2592XevcOGwTuvhBBM53rN5suLnpcGFIT5TuOQrFinT1+vXoE2D/UkDll8nvtGzJyqFgSSFDrvf0e6ZkbFlIQRoJGkfhnDLON2aEycOe9EcD+NiLDXQc9++j+3Kl5QFyze3xd21+ConIZRGDXKqvoEhfp1ovR7ND76IVOAoGMcDT4N+n+NWdXIilipux3gQ5UZkALw1ocFzhEZY9pCYw9e7XGQRh27N/RYns+sSI1qXbtBbl0FCl7X5efvsJLWId0JuEag5f5RAYYYo=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY0MjkzNTM3ODAxNiwKICAicHJvZmlsZUlkIiA6ICIxNzU1N2FjNTEzMWE0YTUzODAwODg3Y2E4ZTQ4YWQyNSIsCiAgInByb2ZpbGVOYW1lIiA6ICJQZW50YXRpbCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS85YTkxZTYzYzhlZDEzNWU5MzcxOTIzZWZkYTM0ZGEzZjQ3MmFlYWU2OGU0OGYwNTk4YmQzMjFlYmU2ZGRhODQ0IiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=", "axlLVBXTguxfcZe/96c4Srg+PB06MborzBRG3QUNlZfSGwKHN2QLXd1dmGhzvMOfVax88Hnatqbn1fKknM5pu/A/KJ2UoxKXisJAld9lN5CZ01XkifWFSyEev0GXa7b1fq7zmh/nNQnJ1K0jpVycL22YHjFE4dSywC2JTG7yl8hq1qtf6sJX9CLxfRKejm+b9cxSlajligJeITNdQeSBmEH6jOTxlsBVwLWp2gqNuanpHulNtMkemKGrDp07xoO6ZymIUNH9/bhlDc1Q7dmaOlQCbIPZNvkDVV7C+nJmf89BOSNac28MRL226vxJKwBEeWOclYSnYWEADc+6ptlaHHHAwmnaSDfsnHQfWjK3vUNHiRlvLtpjE4G/gQtJSlSBfcX2ge/No+Rjwidpm/pLvOpF0l1Y2EzeRzjiHuLd3bFNN+qARCKzKzbP8bNyra1jWVsqn/MOrsv/7ZJNeFV1LmpkPLzdTr01C3cvk1DBEqxBzmuQVtphK9xMVz+9C5BM1nd//f8KbsuFZtF5KIZd8qqtfNe0GYikTEq8VHIgOxpp4q9M9vTjKh//E96/khZmY0aiv1hfAu17qJpDM5yMErGn0IcNjfBTcZRnAesrmNaILxAV3vAN1n3kLNbOVEK1jaSK1pwQG2Cu+XL1WJ99AgIw+1LVLNqKyDecAghByA0=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTYyMDUzODc2OTY1OSwKICAicHJvZmlsZUlkIiA6ICI0ZjU2ZTg2ODk2OGU0ZWEwYmNjM2M2NzRlNzQ3ODdjOCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDE1IiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2IwOGVlMGI1MWM4ZjFjMzc0M2YxNTAyNWJhZDkzNjczZTg3Yjc4NDVhODU5YTFlNGY2ODUxMDM3Y2QyNDYwNjIiCiAgICB9CiAgfQp9", "BpYxbKDjyWnHTjQM36pSK6doKULuQ+PFWEtNOefoOqf3JZLJ4mrpT9plB195KJ0vzU4Yaqg8GefVlqiENpbNVTnn4wihOSJ8lAcEFdBPDhADLBD7BbUW0NGHJwgMEichaR4JCjFjjTfei013sQyYQZ+KFPaqLb/vuU+8Pfw1MV39luYXUAzNosrZv1pAMN4RDyIP9bzsmwBdbWgVR+tuN43sly/LWlW85SB2aOd0z7N7/j71vN2BOmpmOYLj0nqDZyBw7hXgnBy04/msSGVAOgILW0WOYKAaYjGss967wGlysy/cBpDG1/KlLhueUht9C485/qWG5N4j0/iS7hsSI+wVCs/tSKmYHHB/Z1f9eiRkJulm1xQL4f4cmGpwbzP6VNzSSXhInBM67tsT5kUVhvvHyZ2DkZLL+30I1tfcbfIEZaLCmKYCEpbxMgawHpOtq6W8wX8oNLNji+BVndWTmNGt92eX1JmrUVP/0MVyGIiwZo5AHtZZPnKuUc8SJDckvtljeimAzczTM+u+wXKagi1oYGPZJaCkPK9wlMe5XGj6br/UHBs3WbvzH3FND7NNAE4cahd0vayuNIS5VLQtyPhX8eYGJPUps+5kIjfA0RqP0UtvSDVZ9bMXUYokzNtkvjZwuQR7QQNCoh5+tXg0KzKaKQyRbyU9VcEG6YfDTLU=")); + this.add(new Property("textures", "eyJ0aW1lc3RhbXAiOjE1ODc4ODc0MjE4NzMsInByb2ZpbGVJZCI6ImIwZDczMmZlMDBmNzQwN2U5ZTdmNzQ2MzAxY2Q5OGNhIiwicHJvZmlsZU5hbWUiOiJPUHBscyIsInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjUxMDFkZmFiY2EwMjVmNmZmOTdlOTExYWMyYThlMjNkYjhlNDBlOTgzNTY4YjdmMDAxOTJiZmI0NWE0ZTUxYiIsIm1ldGFkYXRhIjp7Im1vZGVsIjoic2xpbSJ9fX19", "Xg96/raiX+7NX6zCENGeT5iy6pnghgK8kBdoCVFuMPjHRKTpwBOuhPhat2LzGeHNH5pPRANsjrp3+ug7F0c49d5/AubOq4xNr1DnqNM0Rj76ZuZ2V1i3uc9sLYyfuBPw98ggaiYJq4QHjPZagLDvPhY/WnyZa0Vml6X1nWrD7tNPg2Nj8VIiwrFB3eMsoWzC5WFyze4oOfUtTTVyDmU524I3Wy26wt0x6Ch+EqE5VJZha4JwQgb48RUiOXZDimw8paq6k3C4GrQnT2j+OQL4ndALiT8XxY4NsZS6XCAZukfkJh9S80x41SwTLHbQSWDx+O+prMeoHeM3zfl27E/w5yizugtcMjN3qLCY8J+1MqoIvr2trJKY/3VBDv29o3XmwFeB0M2iGaO9HK7qenRHCCv0w64xIdsO+VtsS0mtUFAh2Hh5nJqif9spOzaWA4rYPRtiYilXYpMLni9dSsRRhD8whWp4Rvnhw0x1Usyh5YA9HW2mEOdplbjYIzn0lZMmcrAZNY6zXqrjFPQ9Gx/5y9s12YTfgcjlE+gGn6M7J8B1N2FIXMLhib7PBh1oJBFNdNFqu2eeGLvh70eC1e219XwZVhajxVKKCVmwtXpaiCoFsMgTNlmr1j1J4Hr+lobeGxeq57mCJFET8LPjPzIWZF0F5dpBZ4S8hD2rnxPN4Sc=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY2MjY0NTA2MzU5MiwKICAicHJvZmlsZUlkIiA6ICI5YzM1ZGU3MjdmMzU0ZTVlYjFiOWRhOGViYTZhYzM1YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZXphV1MiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmNjYzdmZWJhOTc5OGY0NzE2OTZiNjk1MTE5OWQ5NDY4ZmJmZjVhZmIxNTJhODBjOTU4MjMzY2M1OWMxNTQ3YSIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", "d2wdSQ2ZuayUbU3PJdY43OMZujsdUALKYb7zY4FSkY27PJZYiUH/sQfYEM5g52NIUGjL+qw2o12q3VNYdk++ytKqzHbLzHe4iUAS7G8oUdns3tzyOchBMbDfrur60ItkZMtRGkP5PjH6pHjyLj17tI8Cr5LorZeak5/uFmbuoyKC4SIBQzJvkZYTd1V8DYZkQfLzn6P54EtcUS1LZg48NjCdlcoU26w6qU4jzXLCEjey6qIaERar0kqDjeiuyd320DtaNWw2r3/eZ1ca7hUCPX5ALdPvJAAaGCzc7bK4nkePTwVZVG9FvsUQ/z1l1mPBUgbgf5DahDJW+wm6lsqeH/FYDhTlDQ98PPFdq+S2Tg/P1Hcs/K34mxbg7MA/8lKxEPOIhUbWrJACx1OQJxt4F9PHs11bjA5//tgUa/Y5dDIP/Eg2tZSZpx7FBhuKxjiYlcE8qQEQy6o3X8msBP1nIuZBKH67JfXcx6fKSPOuMwevdeqetfR2D7/77KVsEgSI9pUdb5noy5bHwan0gX+wtXfjcXCPOzuPaFdony8xkc1hJzmPXb9QBuMxzHurropJ/ayobwVpRedObJ0qm5/ksBZKi9CMLBKoqvYWdQY6L5Ci94FoNpo7G50ILhb1CW5cfEBtBDPGtYVdIv1R2Pg/3TE7Qo8i9gEuaYcU/g3OWtU=")); + this.add(new Property("textures", "ewogICJ0aW1lc3RhbXAiIDogMTY3MjcyNzk5MDgyNywKICAicHJvZmlsZUlkIiA6ICI3YmRhNDBlM2E1YjU0YzE0YWJmZGYzNGMyODY2NjQ0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJfRWdvcl9wbGF5XyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9hYjk0YjBlMjY2YjM5YTM3ODQ4OTI3NzhiZTg5YTFkNmIzYjIxZGFjZDc1NDMxZjRkMDVlYTI0ZjE0OTcwMmQ4IgogICAgfQogIH0KfQ==", "USwVBGfqtLBXEeAqAMaqB/l+ND4Qo3DlcglcTxDU1u+c8Ps1fp8gGWiXMtr5Tvep6nsqJin0JOeBuRj6RJDlP8txORrbW+C4c2BVNHZdCO8L0sQ/QISG5f8qBM4YU+8G5gIHMPDva5rVPmgj2hsgPDUyZimETXw2Hs6oyphit0r/fWnbURjfBOm4rpKzlKSpyLncxWFI2SmKl2+xLs8w/0oCv0X9vbVdWVjIzmi48/w3DOpIGkmqCzRvr1YHa5kpf80CNGWq/8KMngWlzA0LfILSGzkfWxxTBMSs2L/SoWnOuvwqRLkVZtZ15yNnkuDhI93BnT/k9+fwXaLs/6aqvmWmGh+s7D+JjI07SPSfcasO7c1jb8atA1cqsujuNzWj7JywZDJNRfWnCqbapwLFbllLvvkZL7QV13k7POcLoy71SRI0DRD9mDR9GWYVcxuBCTseIz3Cb1Bo0W+TXXF31RIJSHlF+Bz0Wy4IKfEt9y+LbKiUIqhq7LzppxZ386hmaM1uCoj7L1/JMlJZlro85jo1+ryN78b29qcpm9cTjZd8N48Etz+OM4bLv5ihmSQHgxnhFodY3oVC/VnCYFoQtdciZDOBiM1kAKIy7jokEJWbjvWz0hTSYJFJjrkwWmeeP3IQjr9Y4oc3pUzRFYdR93rwMFQERmskst5DP38cy0k=")); + } + }; + } + + public class Back { + public boolean enable = false; + public double ignore_distance = 32d; + } + + public class Tpa { + public boolean enable = false; + public int timeout = 300; + + } + + public class Works { + public boolean enable = false; + + public int sample_time_ms = FabricLoader.getInstance().isDevelopmentEnvironment() ? 60 * 1000 : 60 * 1000 * 60; + public int sample_distance_limit = 512; + public int sample_counter_top_n = 20; + } + + public class WorldDownloader { + public boolean enable = false; + public String url_format = "http://example.com:%port%%path%"; + public int port = 22222; + public int bytes_per_second_limit = 128 * 1000; + public int context_cache_size = 5; + } + + public class BypassChatSpeed { + public boolean enable = false; + } + + public class BypassMoveSpeed { + public boolean enable = false; + } + + public class BypassMaxPlayerLimit { + public boolean enable = false; + } + + public class DeathLog { + public boolean enable = false; + } + + public class MainStats { + public boolean enable = false; + } + + public class MultiObsidianPlatform { + public boolean enable = false; + public double factor = 4; + } + + public class OpProtect { + public boolean enable = false; + } + + public class Pvp { + public boolean enable = false; + } + + public class StrongerPlayerList { + public boolean enable = false; + } + + public class WhitelistFix { + + public boolean enable = false; + } + + public class ZeroCommandPermission { + public boolean enable = false; + } + + public class Head { + + public boolean enable = false; + } + + public class Profiler { + + public boolean enable = false; + } + + public class CommandSpy { + public boolean enable = false; + } + + public class Scheduler { + public boolean enable = false; + } + + public class BiomeLookupCache { + public boolean enable = false; + } + + public class TickChunkCache { + public boolean enable = false; + } + + public class Config { + public boolean enable = false; + } + + public class Test { + // disable TestModule by default + public boolean enable = false; + } + + public class Hat { + public boolean enable = false; + } + + public class Fly { + public boolean enable = false; + } + + public class God { + public boolean enable = false; + } + + public class Language { + public boolean enable = false; + } + + public class Reply { + public boolean enable = false; + } + + public class Afk { + + public boolean enable = false; + public String format = "[AFK] %player_display_name%"; + + public AfkChecker afk_checker = new AfkChecker(); + + public class AfkChecker { + public String cron = "0 0/5 * ? * *"; + public boolean kick_player = false; + } + } + + public class Suicide { + + public boolean enable = false; + } + + public class CommandInteractive { + + public boolean enable = false; + public boolean log_use = true; + + } + + public class Heal { + public boolean enable = false; + } + + public class Feed { + public boolean enable = false; + } + + public class Repair { + public boolean enable = false; + + } + + public class Seen { + public boolean enable = false; + } + + public class More { + public boolean enable = false; + } + + public class Extinguish { + public boolean enable = false; + } + + public class Home { + public boolean enable = false; + public int max_homes = 3; + } + + public class Ping { + public boolean enable = false; + } + + public class SystemMessage { + public boolean enable = false; + + public Map key2value = new HashMap<>() { + { + this.put("multiplayer.player.joined", "+ %s"); + this.put("commands.seed.success", " Seeeeeeeeeeed: %s"); + this.put("multiplayer.disconnect.not_whitelisted", "Please apply a whitelist first!"); + this.put("death.attack.explosion.player", "%1$s booooooom because of %2$s"); + } + }; + } + + public class EnderChest { + public boolean enable = false; + } + + public class Workbench { + public boolean enable = false; + } + + public class Enchantment { + + public boolean enable = false; + + } + + public class Anvil { + + public boolean enable = false; + } + + public class GrindStone { + + public boolean enable = false; + } + + public class StoneCutter { + + public boolean enable = false; + } + + public Bed bed = new Bed(); + public class Bed { + + public boolean enable = false; + } + } +} diff --git a/src/main/java/io/github/sakurawald/config/model/HeadModel.java b/src/main/java/io/github/sakurawald/config/model/HeadModel.java new file mode 100644 index 000000000..7f02a4e87 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/HeadModel.java @@ -0,0 +1,9 @@ +package io.github.sakurawald.config.model; + +import io.github.sakurawald.module.initializer.head.HeadModule; + +public class HeadModel { + public HeadModule.EconomyType economyType = HeadModule.EconomyType.ITEM; + public String costType = "minecraft:emerald_block"; + public int costAmount = 1; +} diff --git a/src/main/java/io/github/sakurawald/config/model/HomeModel.java b/src/main/java/io/github/sakurawald/config/model/HomeModel.java new file mode 100644 index 000000000..f08f1544b --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/HomeModel.java @@ -0,0 +1,11 @@ +package io.github.sakurawald.config.model; + +import io.github.sakurawald.module.initializer.teleport_warmup.Position; + +import java.util.HashMap; +import java.util.Map; + +public class HomeModel { + + public Map> homes = new HashMap<>(); +} diff --git a/src/main/java/io/github/sakurawald/config/model/PvPModel.java b/src/main/java/io/github/sakurawald/config/model/PvPModel.java new file mode 100644 index 000000000..508407f08 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/PvPModel.java @@ -0,0 +1,9 @@ +package io.github.sakurawald.config.model; + +import java.util.HashSet; + +public class PvPModel { + + public HashSet whitelist = new HashSet<>(); + +} diff --git a/src/main/java/io/github/sakurawald/config/model/SchedulerModel.java b/src/main/java/io/github/sakurawald/config/model/SchedulerModel.java new file mode 100644 index 000000000..b6dcf1aac --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/SchedulerModel.java @@ -0,0 +1,20 @@ +package io.github.sakurawald.config.model; + +import io.github.sakurawald.module.initializer.scheduler.ScheduleJob; + +import java.util.ArrayList; +import java.util.List; + +public class SchedulerModel { + + public List scheduleJobs = new ArrayList<>() { + { + this.add(new ScheduleJob("example_job", false, 3, List.of("0 0 * ? * *"), + List.of( + List.of("tellraw @a [{\"text\":\"Nobody gets the gift!\",\"color\":\"aqua\",\"bold\":false,\"italic\":false,\"underlined\":false,\"strikethrough\":false,\"obfuscated\":false}]"), + List.of("title @a title \"All players get the gift!\"", "give !all_player! minecraft:diamond 1"), + List.of("title @a title \"player !random_player! get the gift!\"", "give !random_player! minecraft:diamond 1") + ))); + } + }; +} diff --git a/src/main/java/io/github/sakurawald/config/model/SeenModel.java b/src/main/java/io/github/sakurawald/config/model/SeenModel.java new file mode 100644 index 000000000..4c8bc2d61 --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/SeenModel.java @@ -0,0 +1,7 @@ +package io.github.sakurawald.config.model; + +import java.util.HashMap; + +public class SeenModel { + public HashMap player2seen = new HashMap<>(); +} diff --git a/src/main/java/io/github/sakurawald/config/model/WorksModel.java b/src/main/java/io/github/sakurawald/config/model/WorksModel.java new file mode 100644 index 000000000..df95fb52a --- /dev/null +++ b/src/main/java/io/github/sakurawald/config/model/WorksModel.java @@ -0,0 +1,11 @@ +package io.github.sakurawald.config.model; + +import io.github.sakurawald.module.initializer.works.work_type.Work; + +import java.util.concurrent.CopyOnWriteArrayList; + +public class WorksModel { + + public CopyOnWriteArrayList works = new CopyOnWriteArrayList<>(); + +} diff --git a/src/main/java/io/github/sakurawald/module/ModuleManager.java b/src/main/java/io/github/sakurawald/module/ModuleManager.java new file mode 100644 index 000000000..d83764735 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/ModuleManager.java @@ -0,0 +1,99 @@ +package io.github.sakurawald.module; + +import com.google.gson.JsonElement; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import org.jetbrains.annotations.ApiStatus; +import org.reflections.Reflections; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static io.github.sakurawald.Fuji.LOGGER; + +public class ModuleManager { + private static final Map, ModuleInitializer> initializers = new HashMap<>(); + private static final Map module2enable = new HashMap<>(); + + @SuppressWarnings("SameParameterValue") + private static Set> scanModules(String packageName) { + Reflections reflections = new Reflections(packageName); + return reflections.getSubTypesOf(ModuleInitializer.class); + } + + public static void initializeModules() { + scanModules(ModuleManager.class.getPackageName()).forEach(ModuleManager::getInitializer); + } + + public static void reloadModules() { + initializers.values().forEach(initializer -> { + try { + initializer.onReload(); + } catch (Exception e) { + LOGGER.error("Failed to reload module -> {}", e.getMessage()); + } + } + ); + } + + public static void reportModules() { + ArrayList enabled = new ArrayList<>(); + module2enable.forEach((module, enable) -> { + if (enable) enabled.add(module); + }); + + LOGGER.info("Enabled {}/{} modules -> {}", enabled.size(), module2enable.size(), enabled); + } + + @ApiStatus.AvailableSince("1.1.5") + public static boolean isModuleEnabled(String moduleName) { + return module2enable.get(moduleName); + } + + /** + * @return if a module is disabled, then this method will return null. + * (If a module is enabled, but the module doesn't extend AbstractModule, then this me* + * hod will also return null, but the module doesn't extend AbstractModule, then this method will also return null.) + */ + @ApiStatus.AvailableSince("1.1.5") + public static T getInitializer(Class clazz) { + JsonElement config = Configs.configHandler.toJsonElement(); + if (!initializers.containsKey(clazz)) { + String basePackageName = calculateBasePackageName(ModuleInitializer.class, clazz.getName()); + if (enableModule(config, basePackageName)) { + try { + ModuleInitializer moduleInitializer = clazz.getDeclaredConstructor().newInstance(); + moduleInitializer.initialize(); + initializers.put(clazz, moduleInitializer); + } catch (Exception e) { + LOGGER.error(e.toString()); + } + } + } + return clazz.cast(initializers.get(clazz)); + } + + public static boolean enableModule(JsonElement config, String basePackageName) { + boolean enable; + try { + enable = config.getAsJsonObject().get("modules").getAsJsonObject().get(basePackageName).getAsJsonObject().get("enable").getAsBoolean(); + } catch (Exception e) { + LOGGER.error("The enable-supplier key '{}' is missing -> force enable this module", "modules.%s.enable".formatted(basePackageName)); + enable = true; + } + + module2enable.put(basePackageName, enable); + return enable; + } + + public static String calculateBasePackageName(Class packageRootClass, String className) { + String basePackageName; + int left = packageRootClass.getPackageName().length() + 1; + basePackageName = className.substring(left); + int right = basePackageName.indexOf("."); + basePackageName = basePackageName.substring(0, right); + return basePackageName; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/ModuleInitializer.java b/src/main/java/io/github/sakurawald/module/initializer/ModuleInitializer.java new file mode 100644 index 000000000..6ca053eba --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/ModuleInitializer.java @@ -0,0 +1,29 @@ +package io.github.sakurawald.module.initializer; + + +import com.mojang.brigadier.CommandDispatcher; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +public abstract class ModuleInitializer { + + public void initialize() { + CommandRegistrationCallback.EVENT.register(this::registerCommand); + this.onInitialize(); + } + + public void onInitialize() { + // no-op + } + + public void onReload() { + // no-op + } + + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + // no-op + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/afk/AfkModule.java b/src/main/java/io/github/sakurawald/module/initializer/afk/AfkModule.java new file mode 100644 index 000000000..deb4db5d3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/afk/AfkModule.java @@ -0,0 +1,84 @@ +package io.github.sakurawald.module.initializer.afk; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + + +public class AfkModule extends ModuleInitializer { + + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(server -> updateJobs()); + } + + @Override + public void onReload() { + updateJobs(); + } + + public void updateJobs() { + ScheduleUtil.removeJobs(AfkCheckerJob.class.getName()); + ScheduleUtil.addJob(AfkCheckerJob.class, null, null, Configs.configHandler.model().modules.afk.afk_checker.cron, new JobDataMap()); + } + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("afk").executes(this::$afk)); + } + + @SuppressWarnings("SameReturnValue") + private int $afk(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, (player -> { + // note: issue command will update lastLastActionTime, so it's impossible to use /afk to disable afk + ((ServerPlayerAccessor_afk) player).fuji$setAfk(true); + MessageUtil.sendMessage(player, "afk.on"); + return Command.SINGLE_SUCCESS; + })); + } + + public static class AfkCheckerJob implements Job { + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + for (ServerPlayerEntity player : Fuji.SERVER.getPlayerManager().getPlayerList()) { + ServerPlayerAccessor_afk afk_player = (ServerPlayerAccessor_afk) player; + + // get last action time + long lastActionTime = player.getLastActionTime(); + long lastLastActionTime = afk_player.fuji$getLastLastActionTime(); + afk_player.fuji$setLastLastActionTime(lastActionTime); + + // diff last action time + /* note: + when a player joins the server, + we'll set lastLastActionTime's initial value to Player#getLastActionTime(), + but there are a little difference even if you call Player#getLastActionTime() again + */ + if (lastActionTime - lastLastActionTime <= 3000) { + if (afk_player.fuji$isAfk()) continue; + + afk_player.fuji$setAfk(true); + if (Configs.configHandler.model().modules.afk.afk_checker.kick_player) { + player.networkHandler.disconnect(MessageUtil.ofVomponent(player, "afk.kick")); + } + } + } + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/afk/ServerPlayerAccessor_afk.java b/src/main/java/io/github/sakurawald/module/initializer/afk/ServerPlayerAccessor_afk.java new file mode 100644 index 000000000..278707109 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/afk/ServerPlayerAccessor_afk.java @@ -0,0 +1,12 @@ +package io.github.sakurawald.module.initializer.afk; + +public interface ServerPlayerAccessor_afk { + + void fuji$setAfk(boolean flag); + + boolean fuji$isAfk(); + + void fuji$setLastLastActionTime(long lastActionTime); + + long fuji$getLastLastActionTime(); +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/anvil/AnvilModule.java b/src/main/java/io/github/sakurawald/module/initializer/anvil/AnvilModule.java new file mode 100644 index 000000000..474d08eb1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/anvil/AnvilModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.anvil; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class AnvilModule extends ModuleInitializer { + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("anvil").executes(this::$anvil)); + } + + private int $anvil(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> new AnvilScreenHandler(i, inventory, ScreenHandlerContext.create(p.getWorld(), p.getBlockPos())) { + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + }, Text.translatable("container.repair"))); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/back/BackModule.java b/src/main/java/io/github/sakurawald/module/initializer/back/BackModule.java new file mode 100644 index 000000000..78cd3a2a5 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/back/BackModule.java @@ -0,0 +1,54 @@ +package io.github.sakurawald.module.initializer.back; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.teleport_warmup.Position; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import java.util.HashMap; + +@SuppressWarnings("LombokGetterMayBeUsed") +public class BackModule extends ModuleInitializer { + + @Getter + private final HashMap player2lastPos = new HashMap<>(); + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("back").executes(this::$back)); + } + + private int $back(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, (player -> { + Position lastPos = player2lastPos.get(player.getName().getString()); + if (lastPos == null) { + MessageUtil.sendActionBar(player, "back.no_previous_position"); + return Command.SINGLE_SUCCESS; + } + + lastPos.teleport(player); + return Command.SINGLE_SUCCESS; + })); + } + + public void updatePlayer(ServerPlayerEntity player) { + Position lastPos = player2lastPos.get(player.getGameProfile().getName()); + double ignoreDistance = Configs.configHandler.model().modules.back.ignore_distance; + if (lastPos == null + || (!lastPos.sameLevel(player.getWorld())) + || (lastPos.sameLevel(player.getWorld()) && player.getPos().squaredDistanceTo(lastPos.getX(), lastPos.getY(), lastPos.getZ()) > ignoreDistance * ignoreDistance) + ) { + player2lastPos.put(player.getGameProfile().getName(), + Position.of(player)); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/bed/BedModule.java b/src/main/java/io/github/sakurawald/module/initializer/bed/BedModule.java new file mode 100644 index 000000000..9fe384f23 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/bed/BedModule.java @@ -0,0 +1,41 @@ +package io.github.sakurawald.module.initializer.bed; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + + +public class BedModule extends ModuleInitializer { + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("bed").executes(this::$bed)); + } + + private int $bed(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, (player) -> { + BlockPos respawnPosition = player.getSpawnPointPosition(); + RegistryKey respawnDimension = player.getSpawnPointDimension(); + ServerWorld serverLevel = Fuji.SERVER.getWorld(respawnDimension); + if (respawnPosition == null || serverLevel == null) { + MessageUtil.sendMessage(player, "bed.not_found"); + return Command.SINGLE_SUCCESS; + } + + player.teleport(serverLevel, respawnPosition.getX(), respawnPosition.getY(), respawnPosition.getZ(), player.getYaw(), player.getPitch()); + MessageUtil.sendMessage(player, "bed.success"); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/better_fake_player/BetterFakePlayerModule.java b/src/main/java/io/github/sakurawald/module/initializer/better_fake_player/BetterFakePlayerModule.java new file mode 100644 index 000000000..fc85890c5 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/better_fake_player/BetterFakePlayerModule.java @@ -0,0 +1,218 @@ +package io.github.sakurawald.module.initializer.better_fake_player; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.DateUtil; +import io.github.sakurawald.util.MessageUtil; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.kyori.adventure.text.Component; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Uuids; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +public class BetterFakePlayerModule extends ModuleInitializer { + private final ArrayList CONSTANT_EMPTY_LIST = new ArrayList<>(); + private final HashMap> player2fakePlayers = new HashMap<>(); + private final HashMap player2expiration = new HashMap<>(); + + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(this::registerScheduleTask); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("player").then( + CommandManager.literal("who").executes(this::$who) + ).then( + CommandManager.literal("renew").executes(this::$renew) + ) + ); + } + + @SuppressWarnings("SameReturnValue") + private int $renew(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + renewFakePlayers(player); + return Command.SINGLE_SUCCESS; + }); + } + + + private int $who(CommandContext context) { + /* validate */ + validateFakePlayers(); + + /* output */ + StringBuilder builder = new StringBuilder(); + for (String player : player2fakePlayers.keySet()) { + ArrayList fakePlayers = player2fakePlayers.get(player); + if (fakePlayers.isEmpty()) continue; + builder.append(player).append(": "); + for (String fakePlayer : fakePlayers) { + builder.append(fakePlayer).append(" "); + } + builder.append("\n"); + } + ServerCommandSource source = context.getSource(); + source.sendMessage(MessageUtil.ofComponent(source, "better_fake_player.who.header").append(Component.text(builder.toString()))); + return Command.SINGLE_SUCCESS; + } + + public boolean hasFakePlayers(ServerPlayerEntity player) { + validateFakePlayers(); + return player2fakePlayers.containsKey(player.getGameProfile().getName()); + } + + public void renewFakePlayers(ServerPlayerEntity player) { + String name = player.getGameProfile().getName(); + int duration = Configs.configHandler.model().modules.better_fake_player.renew_duration_ms; + long newTime = System.currentTimeMillis() + duration; + player2expiration.put(name, newTime); + MessageUtil.sendMessage(player, "better_fake_player.renew.success", DateUtil.toStandardDateFormat(newTime)); + } + + private void validateFakePlayers() { + /* remove invalid fake-player */ + Iterator>> it = player2fakePlayers.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + + ArrayList myFakePlayers = entry.getValue(); + // fix: NPE + if (myFakePlayers == null || myFakePlayers.isEmpty()) { + it.remove(); + continue; + } + myFakePlayers.removeIf(name -> { + ServerPlayerEntity fakePlayer = Fuji.SERVER.getPlayerManager().getPlayer(name); + return fakePlayer == null || fakePlayer.isRemoved(); + }); + } + } + + public boolean canSpawnFakePlayer(ServerPlayerEntity player) { + /* validate */ + validateFakePlayers(); + + /* check */ + int limit = this.getCurrentAmountLimit(); + int current = this.player2fakePlayers.getOrDefault(player.getGameProfile().getName(), CONSTANT_EMPTY_LIST).size(); + return current < limit; + } + + public void addFakePlayer(ServerPlayerEntity player, String fakePlayer) { + this.player2fakePlayers.computeIfAbsent(player.getGameProfile().getName(), k -> new ArrayList<>()).add(fakePlayer); + } + + public boolean canManipulateFakePlayer(CommandContext ctx, String fakePlayer) { + // IMPORTANT: disable /player ... shadow command for online-player + if (ctx.getNodes().get(2).getNode().getName().equals("shadow")) return false; + + // bypass: console + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return true; + + // bypass: op + if (Fuji.SERVER.getPlayerManager().isOperator(player.getGameProfile())) return true; + + ArrayList myFakePlayers = this.player2fakePlayers.getOrDefault(player.getGameProfile().getName(), CONSTANT_EMPTY_LIST); + return myFakePlayers.contains(fakePlayer); + } + + private int getCurrentAmountLimit() { + ArrayList> rules = Configs.configHandler.model().modules.better_fake_player.caps_limit_rule; + LocalDate currentDate = LocalDate.now(); + LocalTime currentTime = LocalTime.now(); + int currentDays = currentDate.getDayOfWeek().getValue(); + int currentMinutes = currentTime.getHour() * 60 + currentTime.getMinute(); + for (List rule : rules) { + if (currentDays >= rule.get(0) && currentMinutes >= rule.get(1)) return rule.get(2); + } + return -1; + } + + @SuppressWarnings("unused") + public void registerScheduleTask(MinecraftServer server) { + ScheduleUtil.addJob(ManageFakePlayersJob.class, null, null, ScheduleUtil.CRON_EVERY_MINUTE, new JobDataMap() { + { + this.put(BetterFakePlayerModule.class.getName(), BetterFakePlayerModule.this); + } + }); + } + + public boolean isMyFakePlayer(ServerPlayerEntity player, ServerPlayerEntity fakePlayer) { + return player2fakePlayers.getOrDefault(player.getGameProfile().getName(), CONSTANT_EMPTY_LIST).contains(fakePlayer.getGameProfile().getName()); + } + + public GameProfile createOfflineGameProfile(String fakePlayerName) { + UUID offlinePlayerUUID = Uuids.getOfflinePlayerUuid(fakePlayerName); + return new GameProfile(offlinePlayerUUID, fakePlayerName); + } + + public static class ManageFakePlayersJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + /* validate */ + BetterFakePlayerModule module = (BetterFakePlayerModule) context.getJobDetail().getJobDataMap().get(BetterFakePlayerModule.class.getName()); + module.validateFakePlayers(); + + int limit = module.getCurrentAmountLimit(); + long currentTimeMS = System.currentTimeMillis(); + for (String playerName : module.player2fakePlayers.keySet()) { + /* check for renew limits */ + long expiration = module.player2expiration.getOrDefault(playerName, 0L); + ArrayList fakePlayers = module.player2fakePlayers.getOrDefault(playerName, module.CONSTANT_EMPTY_LIST); + if (expiration <= currentTimeMS) { + /* auto-renew for online-playerName */ + ServerPlayerEntity playerByName = Fuji.SERVER.getPlayerManager().getPlayer(playerName); + if (playerByName != null) { + module.renewFakePlayers(playerByName); + continue; + } + + for (String fakePlayerName : fakePlayers) { + ServerPlayerEntity fakePlayer = Fuji.SERVER.getPlayerManager().getPlayer(fakePlayerName); + if (fakePlayer == null) return; + fakePlayer.kill(); + MessageUtil.sendBroadcast("better_fake_player.kick_for_expiration", fakePlayer.getGameProfile().getName(), playerName); + } + // remove entry + module.player2expiration.remove(playerName); + + // we'll kick all fake players, so we don't need to check for amount limits + continue; + } + + /* check for amount limits */ + for (int i = fakePlayers.size() - 1; i >= limit; i--) { + ServerPlayerEntity fakePlayer = Fuji.SERVER.getPlayerManager().getPlayer(fakePlayers.get(i)); + if (fakePlayer == null) continue; + fakePlayer.kill(); + + MessageUtil.sendBroadcast("better_fake_player.kick_for_amount", fakePlayer.getGameProfile().getName(), playerName); + } + } + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/better_info/BetterInfoModule.java b/src/main/java/io/github/sakurawald/module/initializer/better_info/BetterInfoModule.java new file mode 100644 index 000000000..65ca00452 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/better_info/BetterInfoModule.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.initializer.better_info; + +import com.mojang.brigadier.CommandDispatcher; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import java.util.List; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class BetterInfoModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("info").then( + dispatcher.findNode(List.of("data", "get", "entity")) + ) + ); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/biome_lookup_cache/ChunkManager.java b/src/main/java/io/github/sakurawald/module/initializer/biome_lookup_cache/ChunkManager.java new file mode 100644 index 000000000..c357e0358 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/biome_lookup_cache/ChunkManager.java @@ -0,0 +1,70 @@ +package io.github.sakurawald.module.initializer.biome_lookup_cache; + +import com.mojang.datafixers.util.Either; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.WorldView; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.WorldChunk; + +/** + * Utility methods for getting chunks. + * + * @author Wesley1808 + */ +public class ChunkManager { + + @NotNull + public static RegistryEntry getRoughBiome(World level, BlockPos pos) { + Chunk chunk = getChunkNow(level, pos); + int x = pos.getX() >> 2; + int y = pos.getY() >> 2; + int z = pos.getZ() >> 2; + + return chunk != null ? chunk.getBiomeForNoiseGen(x, y, z) : level.getGeneratorStoredBiome(x, y, z); + } + + @Nullable + public static Chunk getChunkNow(WorldView levelReader, BlockPos pos) { + return getChunkNow(levelReader, pos.getX() >> 4, pos.getZ() >> 4); + } + + @Nullable + public static Chunk getChunkNow(WorldView levelReader, int chunkX, int chunkZ) { + if (levelReader instanceof ServerWorld level) { + return getChunkFromHolder(getChunkHolder(level, chunkX, chunkZ)); + } else { + return levelReader.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); + } + } + + @Nullable + public static WorldChunk getChunkFromFuture(CompletableFuture> chunkFuture) { + Either either; + if (chunkFuture == ChunkHolder.UNLOADED_WORLD_CHUNK_FUTURE || (either = chunkFuture.getNow(null)) == null) { + return null; + } + + return either.left().orElse(null); + } + + @Nullable + public static WorldChunk getChunkFromHolder(ChunkHolder holder) { + return holder != null ? getChunkFromFuture(holder.getAccessibleFuture()) : null; + } + + @Nullable + private static ChunkHolder getChunkHolder(ServerWorld level, int chunkX, int chunkZ) { + return level.getChunkManager().getChunkHolder(ChunkPos.toLong(chunkX, chunkZ)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/ChatModule.java b/src/main/java/io/github/sakurawald/module/initializer/chat/ChatModule.java new file mode 100644 index 000000000..c22440670 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/ChatModule.java @@ -0,0 +1,187 @@ +package io.github.sakurawald.module.initializer.chat; + +import com.google.common.collect.EvictingQueue; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.chat.display.DisplayHelper; +import io.github.sakurawald.module.initializer.chat.mention.MentionPlayersJob; +import io.github.sakurawald.module.initializer.main_stats.MainStats; +import io.github.sakurawald.module.initializer.main_stats.MainStatsModule; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Queue; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class ChatModule extends ModuleInitializer { + + private final MiniMessage miniMessage = MiniMessage.builder().build(); + private final MainStatsModule mainStatsModule = ModuleManager.getInitializer(MainStatsModule.class); + @Getter + private Queue chatHistory; + + @Override + public void onInitialize() { + Configs.chatHandler.loadFromDisk(); + + chatHistory = EvictingQueue.create(Configs.configHandler.model().modules.chat.history.cache_size); + } + + + @Override + public void onReload() { + Configs.chatHandler.loadFromDisk(); + + EvictingQueue newQueue = EvictingQueue.create(Configs.configHandler.model().modules.chat.history.cache_size); + newQueue.addAll(chatHistory); + chatHistory.clear(); + chatHistory = newQueue; + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("chat") + .then(literal("format") + .then(argument("format", StringArgumentType.greedyString()) + .executes(this::$format) + ))); + } + + private int $format(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + String name = player.getGameProfile().getName(); + String format = StringArgumentType.getString(ctx, "format"); + Configs.chatHandler.model().format.player2format.put(name, format); + Configs.chatHandler.saveToDisk(); + return Command.SINGLE_SUCCESS; + }); + } + + + private Component resolvePositionTag(ServerPlayerEntity player, Component component) { + Component replacement = Component.text("%s (%d %d %d) %s".formatted(player.getServerWorld().getRegistryKey().getValue(), + player.getBlockX(), player.getBlockY(), player.getBlockZ(), player.getChunkPos().toString())).color(NamedTextColor.GOLD); + return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)pos(?=\\s|$)").replacement(replacement).build()); + } + + private Component resolveItemTag(ServerPlayerEntity player, Component component) { + String displayUUID = DisplayHelper.createItemDisplay(player); + Component replacement = + player.getMainHandStack().toHoverableText().asComponent() + .hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt")) + .clickEvent(displayCallback(displayUUID)); + return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)item(?=\\s|$)").replacement(replacement).build()); + } + + private Component resolveInvTag(ServerPlayerEntity player, Component component) { + String displayUUID = DisplayHelper.createInventoryDisplay(player); + Component replacement = + MessageUtil.ofComponent(player, "display.inventory.text") + .hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt")) + .clickEvent(displayCallback(displayUUID)); + return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)inv(?=\\s|$)").replacement(replacement).build()); + } + + private Component resolveEnderTag(ServerPlayerEntity player, Component component) { + String displayUUID = DisplayHelper.createEnderChestDisplay(player); + Component replacement = + MessageUtil.ofComponent(player, "display.ender_chest.text") + .hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt")) + .clickEvent(displayCallback(displayUUID)); + return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)ender(?=\\s|$)").replacement(replacement).build()); + } + + @NotNull + private ClickEvent displayCallback(String displayUUID) { + return ClickEvent.callback(audience -> { + if (audience instanceof ServerCommandSource css && css.getPlayer() != null) { + DisplayHelper.viewDisplay(css.getPlayer(), displayUUID); + } + }, ClickCallback.Options.builder().lifetime(Duration.of(Configs.configHandler.model().modules.chat.display.expiration_duration_s, ChronoUnit.SECONDS)) + .uses(Integer.MAX_VALUE).build()); + } + + @SuppressWarnings("unused") + private String resolveMentionTag(ServerPlayerEntity player, String str) { + /* resolve player tag */ + ArrayList mentionedPlayers = new ArrayList<>(); + + String[] playerNames = Fuji.SERVER.getPlayerNames(); + // fix: mention the longest name first + Arrays.sort(playerNames, Comparator.comparingInt(String::length).reversed()); + + for (String playerName : playerNames) { + // here we must continue so that mentionPlayers will not be added + if (!str.contains(playerName)) continue; + str = str.replace(playerName, "%s".formatted(playerName)); + mentionedPlayers.add(Fuji.SERVER.getPlayerManager().getPlayer(playerName)); + } + + /* run mention player task */ + if (!mentionedPlayers.isEmpty()) { + MentionPlayersJob.scheduleJob(mentionedPlayers); + } + return str; + } + + public void broadcastChatMessage(ServerPlayerEntity player, String message) { + /* resolve format */ + message = Configs.chatHandler.model().format.player2format.getOrDefault(player.getGameProfile().getName(), message) + .replace("%message%", message); + message = resolveMentionTag(player, message); + String format = Configs.configHandler.model().modules.chat.format; + format = format.replace("%message%", message); + format = format.replace("%player%", player.getGameProfile().getName()); + + /* resolve stats */ + if (mainStatsModule != null) { + MainStats stats = MainStats.uuid2stats.getOrDefault(player.getUuid().toString(), new MainStats()); + format = stats.update(player).resolve(Fuji.SERVER, format); + } + + /* resolve tags */ + Component component = miniMessage.deserialize(format, Formatter.date("date", LocalDateTime.now(ZoneId.systemDefault()))).asComponent(); + component = resolveItemTag(player, component); + component = resolveInvTag(player, component); + component = resolveEnderTag(player, component); + component = resolvePositionTag(player, component); + chatHistory.add(component); + // info so that it can be seen in the console + Fuji.LOGGER.info(PlainTextComponentSerializer.plainText().serialize(component)); + for (ServerPlayerEntity receiver : Fuji.SERVER.getPlayerManager().getPlayerList()) { + receiver.sendMessage(component); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/DisplayHelper.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/DisplayHelper.java new file mode 100644 index 000000000..6ba899d10 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/DisplayHelper.java @@ -0,0 +1,59 @@ +package io.github.sakurawald.module.initializer.chat.display; + +import io.github.sakurawald.module.initializer.chat.display.gui.*; +import io.github.sakurawald.util.MessageUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +@SuppressWarnings({"SameReturnValue"}) +public class DisplayHelper { + + private static final SoftReferenceMap uuid2gui = new SoftReferenceMap<>(); + + public static String createInventoryDisplay(@NotNull ServerPlayerEntity player) { + Text title = MessageUtil.ofVomponent(player, "display.gui.title", player.getGameProfile().getName()); + String uuid = UUID.randomUUID().toString(); + uuid2gui.put(uuid, new InventoryDisplayGui(title, player)); + return uuid; + } + + public static String createEnderChestDisplay(@NotNull ServerPlayerEntity player) { + Text title = MessageUtil.ofVomponent(player, "display.gui.title", player.getGameProfile().getName()); + String uuid = UUID.randomUUID().toString(); + uuid2gui.put(uuid, new EnderChestDisplayGui(title, player)); + return uuid; + } + + public static String createItemDisplay(@NotNull ServerPlayerEntity player) { + /* new object */ + DisplayGuiBuilder displayGuiBuilder; + Text title = MessageUtil.ofVomponent(player, "display.gui.title", player.getGameProfile().getName()); + ItemStack itemStack = player.getMainHandStack().copy(); + if (DisplayGuiBuilder.isShulkerBox(itemStack)) { + // shulker-box item + displayGuiBuilder = new ShulkerBoxDisplayGui(title, itemStack, null); + } else { + // non-shulker-box item + displayGuiBuilder = new ItemDisplayGui(title, itemStack); + } + + /* put object */ + String uuid = UUID.randomUUID().toString(); + uuid2gui.put(uuid, displayGuiBuilder); + return uuid; + } + + public static void viewDisplay(@NotNull ServerPlayerEntity player, String displayUUID) { + DisplayGuiBuilder displayGuiBuilder = uuid2gui.get(displayUUID); + if (displayGuiBuilder == null) { + MessageUtil.sendMessage(player, "display.invalid"); + return; + } + displayGuiBuilder.build(player).open(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/SoftReferenceMap.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/SoftReferenceMap.java new file mode 100644 index 000000000..3724ee627 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/SoftReferenceMap.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.chat.display; + +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public class SoftReferenceMap { + private final Map> map = new HashMap<>(); + + public void put(K key, V value) { + SoftReference softRef = new SoftReference<>(value); + map.put(key, softRef); + } + + public V get(K key) { + SoftReference softRef = map.get(key); + if (softRef != null) { + return softRef.get(); + } + return null; + } + + public boolean containsKey(K key) { + return map.containsKey(key); + } + + public void remove(K key) { + map.remove(key); + } + + public void clear() { + map.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/DisplayGuiBuilder.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/DisplayGuiBuilder.java new file mode 100644 index 000000000..8df08c31c --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/DisplayGuiBuilder.java @@ -0,0 +1,43 @@ +package io.github.sakurawald.module.initializer.chat.display.gui; + +import eu.pb4.sgui.api.ClickType; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import eu.pb4.sgui.api.gui.SimpleGui; +import eu.pb4.sgui.api.gui.SlotGuiInterface; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.block.ShulkerBoxBlock; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; + +public abstract class DisplayGuiBuilder { + + protected static final int LINE_SIZE = 9; + + protected static void $setSlot(SimpleGui gui, int i, ItemStack itemStack, SlotClickForDeeperDisplayCallback slotClickForDeeperDisplayCallback) { + GuiElementBuilder guiElementBuilder = GuiElementBuilder.from(itemStack).setCallback(slotClickForDeeperDisplayCallback); + if (isShulkerBox(itemStack)) { + guiElementBuilder.addLoreLine(MessageUtil.ofVomponent(gui.getPlayer(), "display.click.prompt")); + } + gui.setSlot(i, guiElementBuilder.build()); + } + + public static boolean isShulkerBox(ItemStack itemStack) { + return itemStack.getItem() instanceof BlockItem bi && bi.getBlock() instanceof ShulkerBoxBlock; + } + + public abstract SimpleGui build(ServerPlayerEntity player); + + protected record SlotClickForDeeperDisplayCallback(SimpleGui parentGui, + ServerPlayerEntity player) implements GuiElementInterface.ClickCallback { + @Override + public void click(int i, ClickType clickType, net.minecraft.screen.slot.SlotActionType clickType1, SlotGuiInterface slotGuiInterface) { + ItemStack itemStack = slotGuiInterface.getSlot(i).getItemStack(); + if (isShulkerBox(itemStack)) { + ShulkerBoxDisplayGui shulkerBoxDisplayGui = new ShulkerBoxDisplayGui(MessageUtil.ofVomponent(player, "display.gui.title", player.getGameProfile().getName()), itemStack, parentGui); + shulkerBoxDisplayGui.build(player).open(); + } + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/EnderChestDisplayGui.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/EnderChestDisplayGui.java new file mode 100644 index 000000000..e5760c24e --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/EnderChestDisplayGui.java @@ -0,0 +1,42 @@ +package io.github.sakurawald.module.initializer.chat.display.gui; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; + + +public class EnderChestDisplayGui extends DisplayGuiBuilder { + + private final Text title; + private final DefaultedList items = DefaultedList.of(); + + public EnderChestDisplayGui(Text title, ServerPlayerEntity serverPlayer) { + this.title = title; + serverPlayer.getEnderChestInventory().heldStacks.forEach(itemStack -> this.items.add(itemStack.copy())); + } + + @Override + public SimpleGui build(ServerPlayerEntity player) { + SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X4, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(this.title); + + /* construct base */ + for (int i = 0; i < 9; i++) { + gui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + gui.setSlot(4, Items.ENDER_CHEST.getDefaultStack()); + + /* construct items */ + SlotClickForDeeperDisplayCallback slotClickForDeeperDisplayCallback = new SlotClickForDeeperDisplayCallback(gui, player); + for (int i = 0; i < this.items.size(); i++) { + $setSlot(gui, LINE_SIZE + i, this.items.get(i), slotClickForDeeperDisplayCallback); + } + return gui; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/InventoryDisplayGui.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/InventoryDisplayGui.java new file mode 100644 index 000000000..bd91ad0ca --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/InventoryDisplayGui.java @@ -0,0 +1,61 @@ +package io.github.sakurawald.module.initializer.chat.display.gui; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; + + +public class InventoryDisplayGui extends DisplayGuiBuilder { + + private final Text title; + private final DefaultedList armor = DefaultedList.of(); + private final DefaultedList offhand = DefaultedList.of(); + private final DefaultedList items = DefaultedList.of(); + + public InventoryDisplayGui(Text title, ServerPlayerEntity player) { + this.title = title; + PlayerInventory inventory = player.getInventory(); + inventory.armor.forEach(itemStack -> armor.add(itemStack.copy())); + inventory.offHand.forEach(itemStack -> offhand.add(itemStack.copy())); + inventory.main.forEach(itemStack -> items.add(itemStack.copy())); + } + + @Override + public SimpleGui build(ServerPlayerEntity player) { + /* construct base */ + SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X6, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(this.title); + + for (int i = 0; i < LINE_SIZE * 2; i++) { + gui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + + /* construct armor */ + for (int i = 1; i < 5; i++) { + gui.setSlot(i, armor.get((5 - 1) - i)); + } + + /* construct offhand */ + SlotClickForDeeperDisplayCallback slotClickForDeeperDisplayCallback = new SlotClickForDeeperDisplayCallback(gui, player); + gui.setSlot(7, offhand.get(0), slotClickForDeeperDisplayCallback); + + /* construct items */ + for (int i = LINE_SIZE * 5; i < LINE_SIZE * 6; i++) { + ItemStack itemStack = items.get(i - LINE_SIZE * 5); + $setSlot(gui, i, itemStack, slotClickForDeeperDisplayCallback); + } + for (int i = LINE_SIZE * 2; i < LINE_SIZE * 5; i++) { + ItemStack itemStack = items.get(i - LINE_SIZE); + $setSlot(gui, i, itemStack, slotClickForDeeperDisplayCallback); + } + return gui; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ItemDisplayGui.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ItemDisplayGui.java new file mode 100644 index 000000000..cbcd660db --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ItemDisplayGui.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.chat.display.gui; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +public class ItemDisplayGui extends DisplayGuiBuilder { + + private final Text title; + private final ItemStack itemStack; + + public ItemDisplayGui(Text title, ItemStack itemStack) { + this.title = title; + this.itemStack = itemStack; + } + + @Override + public SimpleGui build(ServerPlayerEntity player) { + SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_3X3, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(this.title); + + /* construct base */ + for (int i = 0; i < 9; i++) { + gui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + /* construct item */ + gui.setSlot(4, itemStack); + return gui; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ShulkerBoxDisplayGui.java b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ShulkerBoxDisplayGui.java new file mode 100644 index 000000000..5a63861ff --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/display/gui/ShulkerBoxDisplayGui.java @@ -0,0 +1,62 @@ +package io.github.sakurawald.module.initializer.chat.display.gui; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.util.GuiUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + + +public class ShulkerBoxDisplayGui extends DisplayGuiBuilder { + + private final Text title; + private final ItemStack itemStack; + private final SimpleGui parentGui; + + public ShulkerBoxDisplayGui(Text title, ItemStack itemStack, SimpleGui parentGui) { + this.title = title; + this.itemStack = itemStack; + this.parentGui = parentGui; + } + + @Override + public SimpleGui build(ServerPlayerEntity player) { + SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X4, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(this.title); + + /* construct base */ + for (int i = 0; i < 9; i++) { + gui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + gui.setSlot(4, itemStack); + if (this.parentGui != null) { + gui.setSlot(LINE_SIZE - 1, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "back")) + .setSkullOwner(GuiUtil.PREVIOUS_PAGE_ICON) + .setCallback(parentGui::open)); + } + + /* construct items */ + NbtCompound blockEntityData = BlockItem.getBlockEntityNbt(itemStack); + if (blockEntityData != null) { + NbtList items = (NbtList) blockEntityData.get("Items"); + if (items == null) return gui; + items.forEach(tag -> { + NbtCompound compoundTag = (NbtCompound) tag; + int slot = compoundTag.getInt("Slot"); + ItemStack itemStack = ItemStack.fromNbt(compoundTag); + gui.setSlot(LINE_SIZE + slot, itemStack); + }); + } + return gui; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/chat/mention/MentionPlayersJob.java b/src/main/java/io/github/sakurawald/module/initializer/chat/mention/MentionPlayersJob.java new file mode 100644 index 000000000..7251e8336 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/chat/mention/MentionPlayersJob.java @@ -0,0 +1,46 @@ +package io.github.sakurawald.module.initializer.chat.mention; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.config.model.ConfigModel; +import io.github.sakurawald.util.ScheduleUtil; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.minecraft.server.network.ServerPlayerEntity; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import java.util.ArrayList; +import java.util.Collections; + +@SuppressWarnings("PatternValidation") +public class MentionPlayersJob implements Job { + + public static void scheduleJob(ArrayList players) { + ConfigModel.Modules.Chat.MentionPlayer mentionPlayer = Configs.configHandler.model().modules.chat.mention_player; + int intervalMs = mentionPlayer.interval_ms; + int repeatCount = mentionPlayer.repeat_count; + Sound sound = Sound.sound(Key.key(mentionPlayer.sound), Sound.Source.MUSIC, mentionPlayer.volume, mentionPlayer.pitch); + ScheduleUtil.addJob(MentionPlayersJob.class, null, null, intervalMs, repeatCount, new JobDataMap() { + { + this.put(ArrayList.class.getName(), players); + this.put(Sound.class.getName(), sound); + } + }); + } + + public static void scheduleJob(ServerPlayerEntity serverPlayer) { + scheduleJob(new ArrayList<>(Collections.singletonList(serverPlayer))); + } + + @SuppressWarnings("unchecked") + @Override + public void execute(JobExecutionContext context) { + ArrayList players = (ArrayList) context.getJobDetail().getJobDataMap().get(ArrayList.class.getName()); + Sound sound = (Sound) context.getJobDetail().getJobDataMap().get(Sound.class.getName()); + for (ServerPlayerEntity player : players) { + if (player == null) continue; + player.playSound(sound); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/command_cooldown/CommandCooldownModule.java b/src/main/java/io/github/sakurawald/module/initializer/command_cooldown/CommandCooldownModule.java new file mode 100644 index 000000000..3c4516076 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/command_cooldown/CommandCooldownModule.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.command_cooldown; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import java.util.HashMap; +import java.util.Map; +import net.minecraft.server.network.ServerPlayerEntity; + +public class CommandCooldownModule extends ModuleInitializer { + + private final HashMap> map = new HashMap<>(); + + + public long calculateCommandCooldown(ServerPlayerEntity player, String commandLine) { + + // find the matched cooldown-entry + HashMap commandRegex2LastExecutedTimeMS = map.computeIfAbsent(player, k -> new HashMap<>()); + long leftTime = 0; + for (Map.Entry entry : Configs.configHandler.model().modules.command_cooldown.command_regex_2_cooldown_ms.entrySet()) { + if (!commandLine.matches(entry.getKey())) continue; + + long commandLineLastExecutedTimeMS = commandRegex2LastExecutedTimeMS.computeIfAbsent(entry.getKey(), k -> 0L); + long currentTimeMS = System.currentTimeMillis(); + long cooldownMS = entry.getValue(); + + leftTime = Math.max(0, cooldownMS - (currentTimeMS - commandLineLastExecutedTimeMS)); + if (leftTime == 0) { + commandRegex2LastExecutedTimeMS.put(entry.getKey(), currentTimeMS); + } + } + + return leftTime; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/config/ConfigModule.java b/src/main/java/io/github/sakurawald/module/initializer/config/ConfigModule.java new file mode 100644 index 000000000..30c2ce110 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/config/ConfigModule.java @@ -0,0 +1,41 @@ +package io.github.sakurawald.module.initializer.config; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class ConfigModule extends ModuleInitializer { + + + @Override + public void onReload() { + Configs.configHandler.loadFromDisk(); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("fuji").requires(source -> source.hasPermissionLevel(4)).then( + CommandManager.literal("reload").executes(this::$reload) + ) + ); + } + + private int $reload(CommandContext ctx) { + // reload modules + ModuleManager.reloadModules(); + + MessageUtil.sendMessage(ctx.getSource(), "reload"); + return Command.SINGLE_SUCCESS; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/deathlog/DeathLogModule.java b/src/main/java/io/github/sakurawald/module/initializer/deathlog/DeathLogModule.java new file mode 100644 index 000000000..f05f52372 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/deathlog/DeathLogModule.java @@ -0,0 +1,254 @@ +package io.github.sakurawald.module.initializer.deathlog; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import lombok.SneakyThrows; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtList; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Uuids; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.Vec3d; +import java.io.File; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static net.minecraft.server.command.CommandManager.argument; + +@SuppressWarnings("ResultOfMethodCallIgnored") + +public class DeathLogModule extends ModuleInitializer { + private final Path STORAGE_PATH = Fuji.CONFIG_PATH.resolve("deathlog"); + private final String DEATHS = "Deaths"; + private final String TIME = "time"; + private final String REASON = "reason"; + private final String DIMENSION = "dimension"; + private final String X = "x"; + private final String Y = "y"; + private final String Z = "z"; + private final String REMARK = "remark"; + private final String ARMOR = "armor"; + private final String OFFHAND = "offhand"; + private final String ITEM = "item"; + private final String SCORE = "score"; + private final String XP_LEVEL = "xp_level"; + private final String XP_PROGRESS = "xp_progress"; + private final String INVENTORY = "inventory"; + + + @Override + public void onInitialize() { + STORAGE_PATH.toFile().mkdirs(); + } + + @SuppressWarnings({"UnusedReturnValue", "unused"}) + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("deathlog").requires(s -> s.hasPermissionLevel(4)) + .then(CommandManager.literal("view").then(CommandUtil.offlinePlayerArgument("from").executes(this::$view))) + .then(CommandManager.literal("restore") + .then(CommandUtil.offlinePlayerArgument("from") + .then(argument("index", IntegerArgumentType.integer()) + .then(argument("to", EntityArgumentType.player()).executes(this::$restore)))) + )); + } + + @SuppressWarnings("DataFlowIssue") + @SneakyThrows + private int $restore(CommandContext ctx) { + /* read from file */ + ServerCommandSource source = ctx.getSource(); + String from = StringArgumentType.getString(ctx, "from"); + int index = IntegerArgumentType.getInteger(ctx, "index"); + ServerPlayerEntity to = EntityArgumentType.getPlayer(ctx, "to"); + + File file = STORAGE_PATH.resolve(getStorageFileName(from)).toFile(); + NbtCompound rootTag; + rootTag = NbtIo.read(file.toPath()); + if (rootTag == null) { + source.sendMessage(Component.text("No deathlog found.")); + return 0; + } + + NbtList deathsTag = (NbtList) rootTag.get(DEATHS); + if (index >= deathsTag.size()) { + source.sendMessage(Component.text("Index out of bound.")); + return 0; + } + NbtCompound deathTag = deathsTag.getCompound(index); + NbtCompound inventoryTag = deathTag.getCompound(INVENTORY); + List item = readSlotsTag((NbtList) inventoryTag.get(ITEM)); + List armor = readSlotsTag((NbtList) inventoryTag.get(ARMOR)); + List offhand = readSlotsTag((NbtList) inventoryTag.get(OFFHAND)); + + // check the player's inventory for safety + if (!to.getInventory().isEmpty() && !Fuji.SERVER.getPlayerManager().isOperator(to.getGameProfile())) { + source.sendMessage(Component.text("To player's inventory is not empty!")); + return Command.SINGLE_SUCCESS; + } + + /* restore inventory */ + for (int i = 0; i < item.size(); i++) { + to.getInventory().main.set(i, item.get(i)); + } + for (int i = 0; i < armor.size(); i++) { + to.getInventory().armor.set(i, armor.get(i)); + } + for (int i = 0; i < offhand.size(); i++) { + to.getInventory().offHand.set(i, offhand.get(i)); + } + to.setScore(inventoryTag.getInt(SCORE)); + to.experienceLevel = inventoryTag.getInt(XP_LEVEL); + to.experienceProgress = inventoryTag.getFloat(XP_PROGRESS); + source.sendMessage(Component.text("Restore %s's death log %d for %s".formatted(from, index, to.getGameProfile().getName()))); + return Command.SINGLE_SUCCESS; + } + + private String getStorageFileName(String playerName) { + return Uuids.getOfflinePlayerUuid(playerName) + ".dat"; + } + + @SuppressWarnings("DataFlowIssue") + @SneakyThrows + private int $view(CommandContext ctx) { + String from = StringArgumentType.getString(ctx, "from"); + + File file = STORAGE_PATH.resolve(getStorageFileName(from)).toFile(); + NbtCompound rootTag; + rootTag = NbtIo.read(file.toPath()); + + if (rootTag == null) { + ctx.getSource().sendMessage(Component.text("No deathlog found.")); + return 0; + } + + NbtList deaths = (NbtList) rootTag.get(DEATHS); + TextComponent.Builder builder = Component.text(); + String to = Objects.requireNonNull(ctx.getSource().getPlayer()).getGameProfile().getName(); + for (int i = 0; i < deaths.size(); i++) { + builder.append(asViewComponent(deaths.getCompound(i), from, i, to)); + } + + ctx.getSource().sendMessage(builder.asComponent()); + return Command.SINGLE_SUCCESS; + } + + private Component asViewComponent(NbtCompound deathTag, String from, int index, String to) { + NbtCompound remarkTag = deathTag.getCompound(REMARK); + Component hover = Component.empty().color(NamedTextColor.DARK_GREEN) + .append(Component.text("Time: " + remarkTag.getString(TIME))) + .appendNewline() + .append(Component.text("Reason: " + remarkTag.getString(REASON))) + .appendNewline() + .append(Component.text("Dimension: " + remarkTag.getString(DIMENSION))) + .appendNewline() + .append(Component.text("Coordinate: %f %f %f".formatted( + remarkTag.getDouble(X), + remarkTag.getDouble(Y), + remarkTag.getDouble(Z) + ))); + return Component.empty().color(NamedTextColor.RED) + .append(Component.text(index)).appendSpace() + .clickEvent(ClickEvent.runCommand("/deathlog restore %s %d %s".formatted(from, index, to))) + .hoverEvent(HoverEvent.showText(hover)); + } + + + @SuppressWarnings("DataFlowIssue") + @SneakyThrows + public void store(ServerPlayerEntity player) { + File file = new File(STORAGE_PATH.toString(), getStorageFileName(player.getGameProfile().getName())); + + NbtCompound rootTag; + if (!file.exists()) { + NbtIo.write(new NbtCompound(), file.toPath()); + } + rootTag = NbtIo.read(file.toPath()); + if (rootTag == null) return; + + NbtList deathsTag; + if (!rootTag.contains(DEATHS)) { + rootTag.put(DEATHS, new NbtList()); + } + deathsTag = (NbtList) rootTag.get(DEATHS); + + NbtCompound deathTag = new NbtCompound(); + writeInventoryTag(deathTag, player); + writeRemarkTag(deathTag, player); + deathsTag.add(deathTag); + + NbtIo.write(rootTag, file.toPath()); + } + + private void writeRemarkTag(NbtCompound deathTag, ServerPlayerEntity player) { + String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String reason = player.getDamageTracker().getDeathMessage().getString(); + String dimension = player.getWorld().getRegistryKey().getValue().toString(); + Vec3d position = player.getPos(); + + NbtCompound remarkTag = new NbtCompound(); + remarkTag.putString(TIME, time); + remarkTag.putString(REASON, reason); + remarkTag.putString(DIMENSION, dimension); + remarkTag.putDouble(X, position.x); + remarkTag.putDouble(Y, position.y); + remarkTag.putDouble(Z, position.z); + deathTag.put(REMARK, remarkTag); + } + + private void writeInventoryTag(NbtCompound deathTag, ServerPlayerEntity player) { + PlayerInventory inventory = player.getInventory(); + DefaultedList armor = inventory.armor; + DefaultedList offhand = inventory.offHand; + DefaultedList items = inventory.main; + + NbtCompound inventoryTag = new NbtCompound(); + inventoryTag.put(ARMOR, writeSlotsTag(new NbtList(), armor)); + inventoryTag.put(OFFHAND, writeSlotsTag(new NbtList(), offhand)); + inventoryTag.put(ITEM, writeSlotsTag(new NbtList(), items)); + inventoryTag.putInt(SCORE, player.getScore()); + inventoryTag.putInt(XP_LEVEL, player.experienceLevel); + inventoryTag.putFloat(XP_PROGRESS, player.experienceProgress); + + deathTag.put(INVENTORY, inventoryTag); + } + + private NbtList writeSlotsTag(NbtList slotsTag, DefaultedList itemStackList) { + for (ItemStack item : itemStackList) { + slotsTag.add(item.writeNbt(new NbtCompound())); + } + return slotsTag; + } + + private List readSlotsTag(NbtList slotsTag) { + ArrayList ret = new ArrayList<>(); + for (int i = 0; i < slotsTag.size(); i++) { + ret.add(ItemStack.fromNbt(slotsTag.getCompound(i))); + } + return ret; + } + +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/initializer/enchantment/EnchantmentModule.java b/src/main/java/io/github/sakurawald/module/initializer/enchantment/EnchantmentModule.java new file mode 100644 index 000000000..4cb6a6cbb --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/enchantment/EnchantmentModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.enchantment; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.EnchantmentScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class EnchantmentModule extends ModuleInitializer { + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("enchantment").executes(this::$enchantment)); + } + + private int $enchantment(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> new EnchantmentScreenHandler(i, inventory, ScreenHandlerContext.create(p.getWorld(), p.getBlockPos())) { + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + }, Text.translatable("container.enchant"))); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/enderchest/EnderChestModule.java b/src/main/java/io/github/sakurawald/module/initializer/enderchest/EnderChestModule.java new file mode 100644 index 000000000..0d10bf6d1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/enderchest/EnderChestModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.enderchest; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.inventory.EnderChestInventory; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.stat.Stats; +import net.minecraft.text.Text; + + +public class EnderChestModule extends ModuleInitializer { + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("enderchest").executes(this::$enderchest)); + } + + @SuppressWarnings("SameReturnValue") + private int $enderchest(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + EnderChestInventory enderChestInventory = player.getEnderChestInventory(); + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> GenericContainerScreenHandler.createGeneric9x3(i, inventory, enderChestInventory), Text.translatable("container.enderchest"))); + player.incrementStat(Stats.OPEN_ENDERCHEST); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/extinguish/ExtinguishModule.java b/src/main/java/io/github/sakurawald/module/initializer/extinguish/ExtinguishModule.java new file mode 100644 index 000000000..8625fd9f0 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/extinguish/ExtinguishModule.java @@ -0,0 +1,29 @@ +package io.github.sakurawald.module.initializer.extinguish; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class ExtinguishModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("extinguish").executes(this::$extinguish)); + } + + @SuppressWarnings("SameReturnValue") + private int $extinguish(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.setFireTicks(0); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/feed/FeedModule.java b/src/main/java/io/github/sakurawald/module/initializer/feed/FeedModule.java new file mode 100644 index 000000000..134ef3e31 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/feed/FeedModule.java @@ -0,0 +1,36 @@ +package io.github.sakurawald.module.initializer.feed; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.HungerManager; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class FeedModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("feed").executes(this::$feed)); + } + + @SuppressWarnings("SameReturnValue") + private int $feed(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + HungerManager foodData = player.getHungerManager(); + foodData.setFoodLevel(20); + foodData.setSaturationLevel(5); + foodData.setExhaustion(0); + + MessageUtil.sendMessage(player, "feed"); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/fly/FlyModule.java b/src/main/java/io/github/sakurawald/module/initializer/fly/FlyModule.java new file mode 100644 index 000000000..079c87b64 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/fly/FlyModule.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.fly; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class FlyModule extends ModuleInitializer { + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("fly").executes(this::$fly)); + } + + private int $fly(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, (player) -> { + boolean flag = !player.getAbilities().allowFlying; + player.getAbilities().allowFlying = flag; + + if (!flag) { + player.getAbilities().flying = false; + } + + player.sendAbilitiesUpdate(); + MessageUtil.sendMessage(player, flag ? "fly.on" : "fly.off"); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/god/GodModule.java b/src/main/java/io/github/sakurawald/module/initializer/god/GodModule.java new file mode 100644 index 000000000..456e013a7 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/god/GodModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.god; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class GodModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("god").executes(this::$god)); + } + + @SuppressWarnings("SameReturnValue") + private int $god(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + boolean flag = !player.getAbilities().invulnerable; + player.getAbilities().invulnerable = flag; + player.sendAbilitiesUpdate(); + + MessageUtil.sendMessage(player, flag ? "god.on" : "god.off"); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/grindstone/GrindStoneModule.java b/src/main/java/io/github/sakurawald/module/initializer/grindstone/GrindStoneModule.java new file mode 100644 index 000000000..1987a1f82 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/grindstone/GrindStoneModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.grindstone; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.GrindstoneScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class GrindStoneModule extends ModuleInitializer { + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("grindstone").executes(this::$grindstone)); + } + + private int $grindstone(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> new GrindstoneScreenHandler(i, inventory, ScreenHandlerContext.create(p.getWorld(), p.getBlockPos())) { + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + }, Text.translatable("container.grindstone_title"))); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/hat/HatModule.java b/src/main/java/io/github/sakurawald/module/initializer/hat/HatModule.java new file mode 100644 index 000000000..51d6bf7ed --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/hat/HatModule.java @@ -0,0 +1,38 @@ +package io.github.sakurawald.module.initializer.hat; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.Hand; + + +public class HatModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("hat").executes(this::$hat)); + } + + @SuppressWarnings("SameReturnValue") + private int $hat(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + ItemStack mainHandItem = player.getMainHandStack(); + ItemStack headSlotItem = player.getEquippedStack(EquipmentSlot.HEAD); + + player.equipStack(EquipmentSlot.HEAD, mainHandItem); + player.setStackInHand(Hand.MAIN_HAND, headSlotItem); + MessageUtil.sendMessage(player, "hat.success"); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/HeadModule.java b/src/main/java/io/github/sakurawald/module/initializer/head/HeadModule.java new file mode 100644 index 000000000..fda19f91a --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/HeadModule.java @@ -0,0 +1,93 @@ +package io.github.sakurawald.module.initializer.head; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.head.api.Category; +import io.github.sakurawald.module.initializer.head.api.Head; +import io.github.sakurawald.module.initializer.head.api.HeadDatabaseAPI; +import io.github.sakurawald.module.initializer.head.gui.HeadGui; +import io.github.sakurawald.util.CommandUtil; +import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant; +import net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage; +import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.item.Item; +import net.minecraft.registry.Registries; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import java.util.concurrent.CompletableFuture; + +// Thanks to: https://modrinth.com/mod/headindex +public class HeadModule extends ModuleInitializer { + + public final HeadDatabaseAPI HEAD_DATABASE = new HeadDatabaseAPI(); + public Multimap heads = HashMultimap.create(); + + @SuppressWarnings("UnstableApiUsage") + public void tryPurchase(ServerPlayerEntity player, int amount, Runnable onPurchase) { + int trueAmount = amount * Configs.headHandler.model().costAmount; + switch (Configs.headHandler.model().economyType) { + case FREE -> onPurchase.run(); + case ITEM -> { + try (Transaction transaction = Transaction.openOuter()) { + long extracted = PlayerInventoryStorage.of(player).extract(ItemVariant.of(getCostItem()), trueAmount, transaction); + if (extracted == trueAmount) { + transaction.commit(); + onPurchase.run(); + } + } + } + } + } + + public Text getCost() { + return switch (Configs.headHandler.model().economyType) { + case ITEM -> + Text.empty().append(getCostItem().getName()).append(Text.of(" × " + Configs.headHandler.model().costAmount)); + case FREE -> Text.empty(); + }; + } + + public Item getCostItem() { + return Registries.ITEM.get(Identifier.tryParse(Configs.headHandler.model().costType)); + } + + @Override + public void onInitialize() { + CompletableFuture.runAsync(() -> heads = HEAD_DATABASE.getHeads()); + Configs.headHandler.loadFromDisk(); + } + + @Override + public void onReload() { + Configs.headHandler.loadFromDisk(); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("head").executes(this::$head)); + } + + public int $head(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + new HeadGui(player).open(); + return Command.SINGLE_SUCCESS; + }); + } + + public enum EconomyType { + ITEM, + FREE + } + + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/api/Category.java b/src/main/java/io/github/sakurawald/module/initializer/head/api/Category.java new file mode 100644 index 000000000..50a049298 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/api/Category.java @@ -0,0 +1,83 @@ +package io.github.sakurawald.module.initializer.head.api; + +import io.github.sakurawald.util.MessageUtil; +import java.util.UUID; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +public enum Category { + ALPHABET("alphabet", + new Head( + UUID.fromString("1f961930-4e97-47b7-a5a1-2cc5150f3764"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmMzNWU3MjAyMmUyMjQ5YzlhMTNlNWVkOGE0NTgzNzE3YTYyNjAyNjc3M2Y1NDE2NDQwZDU3M2E5MzhjOTMifX19").of() + ), + ANIMALS("animals", + new Head( + UUID.fromString("6554e785-2a74-481a-9aac-06fc18620a57"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWQxN2U0OGM5MjUzZTY3NDczM2NlYjdiYzNkYTdmNTIxNTFlNTI4OWQwMjEyYzhmMmRkNzFlNDE2ZTRlZTY1In19fQ==" + ).of() + ), + BLOCKS("blocks", + new Head( + UUID.fromString("795e1ad8-de6d-4edc-a1b5-4e6aad038403"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODQ0OWI5MzE4ZTMzMTU4ZTY0YTQ2YWIwZGUxMjFjM2Q0MDAwMGUzMzMyYzE1NzQ5MzJiM2M4NDlkOGZhMGRjMiJ9fX0=" + ).of() + ), + DECORATION("decoration", + new Head( + UUID.fromString("f3244903-0c01-4f8d-bbc2-4b13338c6a10"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmQyYWNiZTVkMmM2MmU0NTViMGQ4ZTY5YzdmNmIwMWJiNjg5NzVmYmZjZmQ5NWMyNzViM2Y5MTYzMTU4NTE5YyJ9fX0=" + ).of() + ), + FOOD_DRINKS("food-drinks", + new Head( + UUID.fromString("187ab05d-1d27-450b-bea8-a723fd1d3b4a"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZiNDhlMmI5NjljNGMxYjg2YzVmODJhMmUyMzc5OWY0YTZmMzFjZTAwOWE1ZjkyYjM5ZjViMjUwNTdiMmRkMCJ9fX0=" + ).of() + ), + HUMANS("humans", + new Head( + UUID.fromString("68cd5f2e-01d3-4ac8-882e-2f7ce487b33b"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGNiN2ExNWVkYTFjYmU0N2E4ZDVkN2Y3ODBlODliYmMzNWUwYzE3N2ZjYjljNjQ4MGExMWIwMmNjODE2NWMxYyJ9fX0=" + ).of() + ), + HUMANOID("humanoid", + new Head( + UUID.fromString("0d8391c2-1748-4869-8631-935ff2d55e07"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGNhOGVmMjQ1OGEyYjEwMjYwYjg3NTY1NThmNzY3OWJjYjdlZjY5MWQ0MWY1MzRlZmVhMmJhNzUxMDczMTVjYyJ9fX0=" + ).of() + ), + MISC("miscellaneous", + new Head( + UUID.fromString("13affe21-698a-4a5e-aff1-ad5183d5f810"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTFlNDJiOGY1MGZlZDgyOGQ0Yjk4MWMyN2NhMTNkMDcxY2U4NjNmNjE1NDBiMjc2MzgyNjZmNzcyZDQxZCJ9fX0=" + ).of() + ), + MONSTERS("monsters", + new Head( + UUID.fromString("a1d05a1e-5937-48ad-973f-70b922d025be"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjI2MDJkNWIzNjJhYTE2MzZkMzVhZjIwZmM3MGQyZTc5NDEzMmVhNjRkNjJkMjNmNTVkYjg1MTVhMGM2MTljNyJ9fX0=" + ).of() + ), + PLANTS("plants", new Head( + UUID.fromString("6b063c51-34b4-4fcb-be0d-a6aff0783328"), + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTAxOWVjZTEyNWVjNzlmOTUxNzhlMWFmNGRhMmE4Yjk4MmRlNzFlZDQyYzMxY2FjNGIxZDJmNjY1MzU1ZGY1YSJ9fX0=" + ).of()); + + public final String name; + public final ItemStack icon; + + Category(String name, ItemStack icon) { + this.name = name; + this.icon = icon; + } + + public ItemStack of(ServerPlayerEntity player) { + return icon.setCustomName(getDisplayName(player)); + } + + public Text getDisplayName(ServerPlayerEntity player) { + return MessageUtil.ofVomponent(player, "head.category." + name); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/api/Head.java b/src/main/java/io/github/sakurawald/module/initializer/head/api/Head.java new file mode 100644 index 000000000..b07f340c8 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/api/Head.java @@ -0,0 +1,66 @@ +package io.github.sakurawald.module.initializer.head.api; + +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.nbt.NbtString; +import net.minecraft.text.Text; + +public class Head { + public final String name; + public final UUID uuid; + public final String value; + @Nullable + public final String tags; + + public Head(@Nullable String name, UUID uuid, String value, @Nullable String tags) { + this.name = name; + this.uuid = uuid; + this.value = value; + this.tags = tags; + } + + public Head(UUID uuid, String value) { + this.name = ""; + this.uuid = uuid; + this.value = value; + this.tags = null; + } + + public String getTagsOrEmpty() { + return tags == null ? "" : tags; + } + + public ItemStack of() { + ItemStack ret = new ItemStack(Items.PLAYER_HEAD); + if (name != null) { + ret.setCustomName(Text.literal(name).styled(style -> style.withItalic(false))); + } + + if (tags != null) { + NbtCompound displayTag = ret.getOrCreateSubNbt("display"); + NbtList loreTag = new NbtList(); + loreTag.add(NbtString.of(Text.Serialization.toJsonString(Text.literal(tags)))); + displayTag.put("Lore", loreTag); + } + + NbtCompound ownerTag = ret.getOrCreateSubNbt("SkullOwner"); + ownerTag.putUuid("Id", uuid); + + NbtCompound propertiesTag = new NbtCompound(); + NbtList texturesTag = new NbtList(); + + NbtCompound textureValue = new NbtCompound(); + textureValue.putString("Value", value); + texturesTag.add(textureValue); + + propertiesTag.put("textures", texturesTag); + ownerTag.put("Properties", propertiesTag); + + return ret; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/api/HeadDatabaseAPI.java b/src/main/java/io/github/sakurawald/module/initializer/head/api/HeadDatabaseAPI.java new file mode 100644 index 000000000..458f919ac --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/api/HeadDatabaseAPI.java @@ -0,0 +1,83 @@ +package io.github.sakurawald.module.initializer.head.api; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import io.github.sakurawald.Fuji; +import net.fabricmc.loader.api.FabricLoader; +import org.apache.commons.io.FileUtils; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; + +@SuppressWarnings("FieldCanBeLocal") + +public class HeadDatabaseAPI { + private final String API = "https://minecraft-heads.com/scripts/api.php?cat=%s&tags=true"; + private final Path STORAGE_PATH = Fuji.CONFIG_PATH.resolve("head").toAbsolutePath(); + + public Multimap getHeads() { + refreshCacheFromAPI(); + return loadCache(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + private void refreshCacheFromAPI() { + for (Category category : Category.values()) { + try { + Fuji.LOGGER.info("Saving {} heads to cache", category.name); + URLConnection connection = URI.create(String.format(API, category.name)).toURL().openConnection(); + var stream = new BufferedInputStream(connection.getInputStream()); + FileUtils.copyInputStreamToFile(stream, STORAGE_PATH.resolve(category.name + ".json").toFile()); + } catch (IOException e) { + Fuji.LOGGER.warn("Failed to save new heads to cache"); + } + + if (!Files.exists(STORAGE_PATH.resolve(category.name + ".json"))) { + Fuji.LOGGER.info("Loading fallback {} heads", category.name); + try { + Files.createDirectories(STORAGE_PATH); + Files.copy( + FabricLoader.getInstance().getModContainer(Fuji.MOD_ID).flatMap(modContainer -> modContainer.findPath("assets/fuji/cache/" + category.name + ".json")).get(), + STORAGE_PATH.resolve(category.name + ".json") + ); + } catch (IOException e) { + Fuji.LOGGER.warn("Failed to load fallback heads", e); + } + } + } + } + + private Multimap loadCache() { + Multimap heads = HashMultimap.create(); + Gson gson = new Gson(); + for (Category category : Category.values()) { + try { + Fuji.LOGGER.info("Loading {} heads from cache", category.name); + var stream = Files.newInputStream(STORAGE_PATH.resolve(category.name + ".json")); + JsonArray headsJson = JsonParser.parseReader(new InputStreamReader(stream)).getAsJsonArray(); + for (JsonElement headJson : headsJson) { + try { + Head head = gson.fromJson(headJson, Head.class); + heads.put(category, head); + } catch (Exception e) { + Fuji.LOGGER.warn("Invalid head: " + headJson); + } + } + } catch (IOException e) { + Fuji.LOGGER.warn("Failed to load heads from cache", e); + } + } + Fuji.LOGGER.info("Finished loading {} heads", heads.size()); + return heads; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/gui/HeadGui.java b/src/main/java/io/github/sakurawald/module/initializer/head/gui/HeadGui.java new file mode 100644 index 000000000..37d2310f7 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/gui/HeadGui.java @@ -0,0 +1,48 @@ +package io.github.sakurawald.module.initializer.head.gui; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.head.HeadModule; +import io.github.sakurawald.module.initializer.head.api.Category; +import io.github.sakurawald.util.MessageUtil; +import java.util.ArrayList; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; + +public class HeadGui extends SimpleGui { + protected final ServerPlayerEntity player; + final HeadModule module = ModuleManager.getInitializer(HeadModule.class); + + public HeadGui(ServerPlayerEntity player) { + super(ScreenHandlerType.GENERIC_9X2, player, false); + + this.player = player; + + int index = 0; + for (Category category : Category.values()) { + addCategoryButton(index, category); + ++index; + } + this.setTitle(MessageUtil.ofVomponent(player, "head.title")); + this.setSlot(this.getSize() - 1, new GuiElementBuilder() + .setItem(Items.COMPASS) + .setName(MessageUtil.ofVomponent(player, "search")) + .setCallback((index1, type1, action) -> new SearchInputGui(this).open())); + this.setSlot(this.getSize() - 2, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "head.category.player")) + .setCallback((index1, type1, action) -> new PlayerInputGui(this).open())); + } + + private void addCategoryButton(int index, Category category) { + this.setSlot(index, category.of(player), (i, type, action, gui) -> { + var headsGui = new PagedHeadsGui(this, new ArrayList<>(module.heads.get(category))); + headsGui.setTitle(category.getDisplayName(player)); + headsGui.open(); + }); + } + + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/gui/PagedHeadsGui.java b/src/main/java/io/github/sakurawald/module/initializer/head/gui/PagedHeadsGui.java new file mode 100644 index 000000000..3d2f0e1fe --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/gui/PagedHeadsGui.java @@ -0,0 +1,146 @@ +package io.github.sakurawald.module.initializer.head.gui; + +import eu.pb4.sgui.api.ClickType; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.GuiInterface; +import eu.pb4.sgui.api.gui.layered.Layer; +import eu.pb4.sgui.api.gui.layered.LayeredGui; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.head.HeadModule; +import io.github.sakurawald.module.initializer.head.api.Head; +import io.github.sakurawald.util.GuiUtil; +import io.github.sakurawald.util.MessageUtil; +import java.util.List; +import java.util.UUID; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +public class PagedHeadsGui extends LayeredGui { + public final List heads; + final GuiInterface parent; + final Layer contentLayer; + final Layer navigationLayer; + final HeadModule module = ModuleManager.getInitializer(HeadModule.class); + public int page = 0; + + public PagedHeadsGui(GuiInterface parent, List heads) { + super(ScreenHandlerType.GENERIC_9X6, parent.getPlayer(), false); + this.heads = heads; + this.parent = parent; + this.contentLayer = new Layer(5, 9); + updateContent(); + this.addLayer(contentLayer, 0, 0); + this.navigationLayer = new Layer(1, 9); + this.updateNavigation(); + this.addLayer(navigationLayer, 0, 5); + } + + private int getMaxPage() { + return Math.max(1, (int) Math.ceil((double) this.heads.size() / 45)); + } + + private void updatePage() { + this.updateNavigation(); + this.updateContent(); + } + + private void updateNavigation() { + for (int i = 0; i < 9; i++) { + navigationLayer.setSlot(i, Items.PINK_STAINED_GLASS_PANE.getDefaultStack()); + } + navigationLayer.setSlot( + 3, new Head( + UUID.fromString("8aa062dc-9852-42b1-ae37-b2f8a3121c0e"), + GuiUtil.PREVIOUS_PAGE_ICON).of().setCustomName(MessageUtil.ofVomponent(parent.getPlayer(), "previous_page")), + ((index, type, action) -> { + this.page -= 1; + if (this.page < 0) { + this.page = 0; + } + + this.updatePage(); + }) + ); + navigationLayer.setSlot( + 5, new Head( + UUID.fromString("8aa062dc-9852-42b1-ae37-b2f8a3121c0e"), + GuiUtil.NEXT_PAGE_ICON).of().setCustomName(MessageUtil.ofVomponent(parent.getPlayer(), "next_page")), + ((index, type, action) -> { + this.page += 1; + if (this.page >= getMaxPage()) { + this.page = getMaxPage() - 1; + } + this.updatePage(); + }) + ); + navigationLayer.setSlot(4, new GuiElementBuilder(Items.PLAYER_HEAD) + .setSkullOwner(GuiUtil.QUESTION_MARK_ICON) + .setName(MessageUtil.ofVomponent(parent.getPlayer(), "head.page", this.page + 1, this.getMaxPage())) + ); + } + + private void updateContent() { + for (int i = 0; i < 45; i++) { + if (heads.size() > i + (this.page * 45)) { + Head head = heads.get(i + (this.page * 45)); + var builder = GuiElementBuilder.from(head.of()); + if (Configs.headHandler.model().economyType != HeadModule.EconomyType.FREE) { + builder.addLoreLine(Text.empty()); + builder.addLoreLine(MessageUtil.ofVomponent(parent.getPlayer(), "head.price").copy().append(module.getCost())); + } + + contentLayer.setSlot(i, builder.asStack(), (index, type, action) -> processHeadClick(head, type)); + } else { + contentLayer.setSlot(i, Items.AIR.getDefaultStack()); + } + } + } + + private void processHeadClick(Head head, ClickType type) { + var player = getPlayer(); + + ItemStack cursorStack = getPlayer().currentScreenHandler.getCursorStack(); + ItemStack headStack = head.of(); + + if (cursorStack.isEmpty()) { + if (type.shift) { + module.tryPurchase(player, 1, () -> player.getInventory().insertStack(headStack)); + } else if (type.isMiddle) { + module.tryPurchase(player, headStack.getMaxCount(), () -> { + headStack.setCount(headStack.getMaxCount()); + player.currentScreenHandler.setCursorStack(headStack); + }); + } else { + module.tryPurchase(player, 1, () -> player.currentScreenHandler.setCursorStack(headStack)); + } + } else if (cursorStack.getMaxCount() <= cursorStack.getCount()) { + //noinspection UnnecessaryReturnStatement + return; + } else if (ItemStack.canCombine(headStack, cursorStack)) { + if (type.isLeft) { + module.tryPurchase(player, 1, () -> cursorStack.increment(1)); + } else if (type.isRight) { + if (Configs.headHandler.model().economyType == HeadModule.EconomyType.FREE) + cursorStack.decrement(1); + } else if (type.isMiddle) { + var amount = headStack.getMaxCount() - cursorStack.getCount(); + module.tryPurchase(player, amount, () -> { + headStack.setCount(headStack.getMaxCount()); + player.currentScreenHandler.setCursorStack(headStack); + }); + } + } else { + if (Configs.headHandler.model().economyType == HeadModule.EconomyType.FREE) + player.currentScreenHandler.setCursorStack(ItemStack.EMPTY); + } + } + + @Override + public void onClose() { + parent.open(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/gui/PlayerInputGui.java b/src/main/java/io/github/sakurawald/module/initializer/head/gui/PlayerInputGui.java new file mode 100644 index 000000000..ebe5e4434 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/gui/PlayerInputGui.java @@ -0,0 +1,123 @@ +package io.github.sakurawald.module.initializer.head.gui; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTextures; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.yggdrasil.ProfileResult; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.AnvilInputGui; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.head.HeadModule; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.text.Text; +import net.minecraft.util.UserCache; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +class PlayerInputGui extends AnvilInputGui { + final HeadModule module = ModuleManager.getInitializer(HeadModule.class); + private final HeadGui parentGui; + private final ItemStack outputStack = Items.PLAYER_HEAD.getDefaultStack(); + private long apiDebounce = 0; + + public PlayerInputGui(HeadGui parentGui) { + super(parentGui.player, false); + this.parentGui = parentGui; + this.setDefaultInputValue(""); + this.setSlot(1, Items.PLAYER_HEAD.getDefaultStack()); + this.setSlot(2, outputStack); + this.setTitle(MessageUtil.ofVomponent(player, "head.category.player")); + } + + @Override + public void onTick() { + if (apiDebounce != 0 && apiDebounce <= System.currentTimeMillis()) { + apiDebounce = 0; + + CompletableFuture.runAsync(() -> { + MinecraftServer server = player.server; + UserCache profileCache = server.getUserCache(); + if (profileCache == null) { + outputStack.removeSubNbt("SkullOwner"); + return; + } + + Optional possibleProfile = profileCache.findByName(this.getInput()); + MinecraftSessionService sessionService = server.getSessionService(); + if (possibleProfile.isEmpty()) { + outputStack.removeSubNbt("SkullOwner"); + return; + } + + ProfileResult profileResult = sessionService.fetchProfile(possibleProfile.get().getId(), false); + if (profileResult == null) { + outputStack.removeSubNbt("SkullOwner"); + return; + } + + GameProfile profile = profileResult.profile(); + + MinecraftProfileTextures textures = sessionService.getTextures(profile); + if (textures == MinecraftProfileTextures.EMPTY) { + outputStack.removeSubNbt("SkullOwner"); + return; + } + + MinecraftProfileTexture texture = textures.skin(); + NbtCompound ownerTag = outputStack.getOrCreateSubNbt("SkullOwner"); + ownerTag.putUuid("Id", profile.getId()); + ownerTag.putString("Name", profile.getName()); + + NbtCompound propertiesTag = new NbtCompound(); + NbtList texturesTag = new NbtList(); + NbtCompound textureValue = new NbtCompound(); + + textureValue.putString("Value", new String(Base64.getEncoder().encode(String.format("{\"textures\":{\"SKIN\":{\"url\":\"%s\"}}}", texture.getUrl()).getBytes()), StandardCharsets.UTF_8)); + + texturesTag.add(textureValue); + propertiesTag.put("textures", texturesTag); + ownerTag.put("Properties", propertiesTag); + + var builder = GuiElementBuilder.from(outputStack); + if (Configs.headHandler.model().economyType != HeadModule.EconomyType.FREE) { + builder.addLoreLine(Text.empty()); + builder.addLoreLine(MessageUtil.ofVomponent(player, "head.price").copy().append(module.getCost())); + } + + this.setSlot(2, builder.asStack(), (index, type, action, gui) -> + module.tryPurchase(player, 1, () -> { + var cursorStack = getPlayer().currentScreenHandler.getCursorStack(); + if (player.currentScreenHandler.getCursorStack().isEmpty()) { + player.currentScreenHandler.setCursorStack(outputStack.copy()); + } else if (ItemStack.canCombine(outputStack, cursorStack) && cursorStack.getCount() < cursorStack.getMaxCount()) { + cursorStack.increment(1); + } else { + player.dropItem(outputStack.copy(), false); + } + }) + ); + }); + } + } + + @Override + public void onInput(String input) { + super.onInput(input); + apiDebounce = System.currentTimeMillis() + 500; + } + + @Override + public void onClose() { + parentGui.open(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/head/gui/SearchInputGui.java b/src/main/java/io/github/sakurawald/module/initializer/head/gui/SearchInputGui.java new file mode 100644 index 000000000..0e5b92b48 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/head/gui/SearchInputGui.java @@ -0,0 +1,36 @@ +package io.github.sakurawald.module.initializer.head.gui; + +import eu.pb4.sgui.api.gui.AnvilInputGui; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.head.HeadModule; +import io.github.sakurawald.util.MessageUtil; +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.item.Items; + +class SearchInputGui extends AnvilInputGui { + final HeadModule module = ModuleManager.getInitializer(HeadModule.class); + private final HeadGui parentGui; + + public SearchInputGui(HeadGui parentGui) { + super(parentGui.player, false); + this.parentGui = parentGui; + + this.setDefaultInputValue(""); + this.setSlot(1, Items.BARRIER.getDefaultStack()); + this.setSlot(2, Items.SLIME_BALL.getDefaultStack().setCustomName(MessageUtil.ofVomponent(player, "confirm")), (index, type, action, gui) -> { + String search = this.getInput(); + var heads = module.heads.values().stream() + .filter(head -> head.name.toLowerCase().contains(search.toLowerCase()) || head.getTagsOrEmpty().toLowerCase().contains(search.toLowerCase())) + .collect(Collectors.toList()); + var $gui = new PagedHeadsGui(this, heads); + $gui.setTitle(MessageUtil.ofVomponent(player, "head.search.output", search)); + $gui.open(); + }); + } + + @Override + public void onClose() { + parentGui.open(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/heal/HealModule.java b/src/main/java/io/github/sakurawald/module/initializer/heal/HealModule.java new file mode 100644 index 000000000..034be61f3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/heal/HealModule.java @@ -0,0 +1,31 @@ +package io.github.sakurawald.module.initializer.heal; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class HealModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("heal").executes(this::$heal)); + } + + @SuppressWarnings("SameReturnValue") + private int $heal(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.setHealth(player.getMaxHealth()); + MessageUtil.sendMessage(player, "heal"); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/home/HomeModule.java b/src/main/java/io/github/sakurawald/module/initializer/home/HomeModule.java new file mode 100644 index 000000000..eec76a775 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/home/HomeModule.java @@ -0,0 +1,135 @@ +package io.github.sakurawald.module.initializer.home; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.config.handler.ConfigHandler; +import io.github.sakurawald.config.handler.ObjectConfigHandler; +import io.github.sakurawald.config.model.HomeModel; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.teleport_warmup.Position; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import io.github.sakurawald.util.ScheduleUtil; +import lombok.Getter; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import java.util.HashMap; +import java.util.Map; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +@SuppressWarnings("LombokGetterMayBeUsed") + +public class HomeModule extends ModuleInitializer { + + + @Getter + private final ConfigHandler data = new ObjectConfigHandler<>("home.json", HomeModel.class); + + public void onInitialize() { + data.loadFromDisk(); + data.autoSave(ScheduleUtil.CRON_EVERY_MINUTE); + } + + @Override + public void onReload() { + data.loadFromDisk(); + data.autoSave(ScheduleUtil.CRON_EVERY_MINUTE); + } + + @SuppressWarnings({"UnusedReturnValue", "unused"}) + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("home") + .then(CommandManager.literal("set").then(myHomesArgument().executes(ctx -> $set(ctx, false)).then(literal("override").executes(ctx -> $set(ctx, true))))) + .then(CommandManager.literal("tp").then(myHomesArgument().executes(this::$tp))) + .then(CommandManager.literal("unset").then(myHomesArgument().executes(this::$unset))) + .then(CommandManager.literal("list").executes(this::$list)) + ); + } + + private Map getHomes(ServerPlayerEntity player) { + String playerName = player.getGameProfile().getName(); + Map> homes = data.model().homes; + homes.computeIfAbsent(playerName, k -> new HashMap<>()); + return homes.get(playerName); + } + + private int $tp(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + Map name2position = getHomes(player); + String homeName = StringArgumentType.getString(ctx, "name"); + if (!name2position.containsKey(homeName)) { + MessageUtil.sendMessage(player, "home.no_found", homeName); + return 0; + } + + Position position = name2position.get(homeName); + position.teleport(player); + return Command.SINGLE_SUCCESS; + }); + } + + private int $unset(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + Map name2position = getHomes(player); + String homeName = StringArgumentType.getString(ctx, "name"); + if (!name2position.containsKey(homeName)) { + MessageUtil.sendMessage(player, "home.no_found", homeName); + return 0; + } + + name2position.remove(homeName); + MessageUtil.sendMessage(player, "home.unset.success", homeName); + return Command.SINGLE_SUCCESS; + }); + } + + private int $set(CommandContext ctx, boolean override) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + Map name2position = getHomes(player); + String homeName = StringArgumentType.getString(ctx, "name"); + if (name2position.containsKey(homeName)) { + if (!override) { + MessageUtil.sendMessage(player, "home.set.fail.need_override", homeName); + return Command.SINGLE_SUCCESS; + } + } else if (name2position.size() >= Configs.configHandler.model().modules.home.max_homes) { + MessageUtil.sendMessage(player, "home.set.fail.limit"); + return Command.SINGLE_SUCCESS; + } + + name2position.put(homeName, Position.of(player)); + MessageUtil.sendMessage(player, "home.set.success", homeName); + return Command.SINGLE_SUCCESS; + }); + } + + private int $list(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + MessageUtil.sendMessage(player, "home.list", getHomes(player).keySet()); + return Command.SINGLE_SUCCESS; + }); + } + + public RequiredArgumentBuilder myHomesArgument() { + return argument("name", StringArgumentType.string()) + .suggests((context, builder) -> { + ServerPlayerEntity player = context.getSource().getPlayer(); + if (player == null) return builder.buildFuture(); + + Map name2position = getHomes(player); + name2position.keySet().forEach(builder::suggest); + return builder.buildFuture(); + } + ); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/language/LanguageModule.java b/src/main/java/io/github/sakurawald/module/initializer/language/LanguageModule.java new file mode 100644 index 000000000..6ab983df4 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/language/LanguageModule.java @@ -0,0 +1,14 @@ +package io.github.sakurawald.module.initializer.language; + +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; + + +public class LanguageModule extends ModuleInitializer { + + @Override + public void onReload() { + MessageUtil.getLang2json().clear(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStats.java b/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStats.java new file mode 100644 index 000000000..da5efb5c5 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStats.java @@ -0,0 +1,127 @@ +package io.github.sakurawald.module.initializer.main_stats; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.ToString; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.stat.ServerStatHandler; +import net.minecraft.stat.Stats; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.regex.Pattern; + +@ToString +public class MainStats { + public static final HashMap uuid2stats = new HashMap<>(); + private static final int CM_TO_KM_DIVISOR = 100 * 1000; + private static final int GT_TO_H_DIVISOR = 20 * 3600; + public int playtime; + public int mined; + public int placed; + public int killed; + public int moved; + + public static MainStats calculatePlayerMainStats(String uuid) { + MainStats playerMainStats = new MainStats(); + try { + // get player statistics + File file = new File("world/stats/" + uuid + ".json"); + if (!file.exists()) return playerMainStats; + JsonObject json = JsonParser.parseReader(new FileReader(file)).getAsJsonObject(); + JsonObject stats = json.getAsJsonObject("stats"); + if (stats == null) return playerMainStats; + int mined_all = sumUpStats(stats.getAsJsonObject("minecraft:mined"), ".*"); + int used_all = sumUpStats(stats.getAsJsonObject("minecraft:used"), ".*"); + JsonObject custom = stats.getAsJsonObject("minecraft:custom"); + if (custom == null) return playerMainStats; + int moved_all = sumUpStats(custom, ".+_cm") / CM_TO_KM_DIVISOR; + JsonElement mobKills = custom.get("minecraft:mob_kills"); + int mob_kills = (mobKills == null ? 0 : mobKills.getAsInt()); + JsonElement playTime = custom.get("minecraft:play_time"); + int play_time = (playTime == null ? 0 : playTime.getAsInt()) / GT_TO_H_DIVISOR; + + // set main-stats + playerMainStats.playtime += play_time; + playerMainStats.mined += mined_all; + playerMainStats.placed += used_all; + playerMainStats.killed += mob_kills; + playerMainStats.moved += moved_all; + + } catch (IOException e) { + throw new RuntimeException(e); + } + + return playerMainStats; + } + + public static MainStats calculateServerMainStats() { + MainStats serverMainStats = new MainStats(); + File file = new File("world/stats/"); + File[] files = file.listFiles(); + if (files == null) return serverMainStats; + + for (File playerStatFile : files) { + String uuid = playerStatFile.getName().replace(".json", ""); + MainStats playerMainStats = MainStats.calculatePlayerMainStats(uuid); + serverMainStats.add(playerMainStats); + } + + return serverMainStats; + } + + private static int sumUpStats(JsonObject jsonObject, String regex) { + if (jsonObject == null) return 0; + int count = 0; + Pattern pattern = Pattern.compile(regex); + for (String key : jsonObject.keySet()) { + if (pattern.matcher(key).matches()) { + count += jsonObject.get(key).getAsInt(); + } + } + return count; + } + + public MainStats update(ServerPlayerEntity player) { + this.playtime = player.getStatHandler().getStat((Stats.CUSTOM.getOrCreateStat(Stats.PLAY_TIME))) / GT_TO_H_DIVISOR; + this.killed = player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(Stats.MOB_KILLS)); + ServerStatHandler statHandler = player.getStatHandler(); + this.moved = (statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.WALK_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.CROUCH_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.SPRINT_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.WALK_ON_WATER_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.FALL_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.CLIMB_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.FLY_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.WALK_UNDER_WATER_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.MINECART_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.BOAT_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.PIG_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.HORSE_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.AVIATE_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.SWIM_ONE_CM)) + + statHandler.getStat(Stats.CUSTOM.getOrCreateStat(Stats.STRIDER_ONE_CM))) / CM_TO_KM_DIVISOR; + return this; + } + + public String resolve(MinecraftServer server, String str) { + return str.replace("%playtime%", String.valueOf(playtime)) + .replace("%mined%", String.valueOf(mined)) + .replace("%placed%", String.valueOf(placed)) + .replace("%killed%", String.valueOf(killed)) + .replace("%moved%", String.valueOf(moved)) + .replace("%uptime%", String.valueOf(server.getTicks() / GT_TO_H_DIVISOR)) + .replace("%version%", server.getVersion()); + } + + public void add(MainStats other) { + this.playtime += other.playtime; + this.mined += other.mined; + this.placed += other.placed; + this.killed += other.killed; + this.moved += other.moved; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStatsModule.java b/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStatsModule.java new file mode 100644 index 000000000..86dba7caf --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/main_stats/MainStatsModule.java @@ -0,0 +1,103 @@ +package io.github.sakurawald.module.initializer.main_stats; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.config.model.ConfigModel; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.motd.MotdModule; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.server.MinecraftServer; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +public class MainStatsModule extends ModuleInitializer { + + private final List colors = Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); + private final MotdModule motd_module = ModuleManager.getInitializer(MotdModule.class); + + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + this.updateMainStats(server); + this.registerScheduleTask(server); + }); + } + + public void updateMainStats(MinecraftServer server) { + // calc main stats + MainStats serverMainStats = MainStats.calculateServerMainStats(); + + // update motd if motd module is enabled + if (motd_module != null) { + ConfigModel.Modules.MOTD motd = Configs.configHandler.model().modules.motd; + ArrayList descriptions = new ArrayList<>(); + motd.descriptions.forEach(description -> descriptions.add(serverMainStats.resolve(server, description))); + motd_module.updateDescriptions(descriptions); + } + } + + @SuppressWarnings({"SameParameterValue", "unused"}) + private String centerText(String text, int lineLength, int lengthDelta) { + /* calc length */ + char[] chars = text.toCharArray(); + double length = 0; + boolean bold = false; + char code; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == '§') { + // skip § + if (i + 1 != chars.length) { + // skip code + code = chars[i + 1]; + if (code == 'l') bold = true; + else if (colors.contains(code) && code == 'r') bold = false; + } + } else { + length += (chars[i] == ' ' ? 1 : (bold ? 1.1555555555555556 : 1)); + } + } + + /* add length delta */ + length += lengthDelta; + + /* build spaces */ + double spaces = (lineLength - length) / 2; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < spaces; i++) { + builder.append(' '); + } + builder.append(text); + return builder.toString(); + } + + public void registerScheduleTask(MinecraftServer server) { + // async task + ScheduleUtil.addJob(UpdateMainStatsJob.class, null, null, ScheduleUtil.CRON_EVERY_MINUTE, new JobDataMap() { + { + this.put(MinecraftServer.class.getName(), server); + this.put(MainStatsModule.class.getName(), MainStatsModule.this); + } + }); + } + + public static class UpdateMainStatsJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + // save all online-player's stats + MinecraftServer server = (MinecraftServer) context.getJobDetail().getJobDataMap().get(MinecraftServer.class.getName()); + server.getPlayerManager().getPlayerList().forEach((p) -> p.getStatHandler().save()); + + // update main stats + MainStatsModule module = (MainStatsModule) context.getJobDetail().getJobDataMap().get(MainStatsModule.class.getName()); + module.updateMainStats(server); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/more/MoreModule.java b/src/main/java/io/github/sakurawald/module/initializer/more/MoreModule.java new file mode 100644 index 000000000..12299ddff --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/more/MoreModule.java @@ -0,0 +1,31 @@ +package io.github.sakurawald.module.initializer.more; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.item.ItemStack; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class MoreModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("more").executes(this::$more)); + } + + @SuppressWarnings("SameReturnValue") + private int $more(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, (player -> { + ItemStack mainHandItem = player.getMainHandStack(); + mainHandItem.setCount(mainHandItem.getMaxCount()); + return Command.SINGLE_SUCCESS; + })); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/motd/MotdModule.java b/src/main/java/io/github/sakurawald/module/initializer/motd/MotdModule.java new file mode 100644 index 000000000..589c4a2c1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/motd/MotdModule.java @@ -0,0 +1,68 @@ +package io.github.sakurawald.module.initializer.motd; + +import com.google.common.base.Preconditions; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; +import javax.imageio.ImageIO; +import net.minecraft.server.ServerMetadata; +import net.minecraft.text.Text; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; + + +public class MotdModule extends ModuleInitializer { + private final File ICON_FOLDER = Fuji.CONFIG_PATH.resolve("icon").toFile(); + + private List descriptions = new ArrayList<>(); + + public void updateDescriptions(List descriptions) { + this.descriptions = descriptions; + } + + @Override + public void onInitialize() { + updateDescriptions(Configs.configHandler.model().modules.motd.descriptions); + } + + @Override + public void onReload() { + updateDescriptions(Configs.configHandler.model().modules.motd.descriptions); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public Optional getRandomIcon() { + ICON_FOLDER.mkdirs(); + File[] icons = ICON_FOLDER.listFiles(); + if (icons == null || icons.length == 0) { + Fuji.LOGGER.warn("No icons found in {}", ICON_FOLDER.getAbsolutePath()); + return Optional.empty(); + } + File randomIcon = icons[new Random().nextInt(icons.length)]; + ByteArrayOutputStream byteArrayOutputStream; + try { + BufferedImage bufferedImage = ImageIO.read(randomIcon); + Preconditions.checkState(bufferedImage.getWidth() == 64, "Must be 64 pixels wide"); + Preconditions.checkState(bufferedImage.getHeight() == 64, "Must be 64 pixels high"); + byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, "PNG", byteArrayOutputStream); + } catch (IOException e) { + Fuji.LOGGER.warn("Failed to encode favicon", e); + return Optional.empty(); + } + return Optional.of(new ServerMetadata.Favicon(byteArrayOutputStream.toByteArray())); + } + + public Text getRandomDescription() { + return MessageUtil.ofVomponent(descriptions.get(new Random().nextInt(descriptions.size()))); + } + + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/multi_obsidian_platform/MultiObsidianPlatformModule.java b/src/main/java/io/github/sakurawald/module/initializer/multi_obsidian_platform/MultiObsidianPlatformModule.java new file mode 100644 index 000000000..b05ea4193 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/multi_obsidian_platform/MultiObsidianPlatformModule.java @@ -0,0 +1,95 @@ +package io.github.sakurawald.module.initializer.multi_obsidian_platform; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import java.util.HashMap; +import net.minecraft.block.Blocks; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; + + +public class MultiObsidianPlatformModule extends ModuleInitializer { + + private final HashMap TRANSFORM_CACHE = new HashMap<>(); + + /* this method is used to fix Entity#position() async */ + private BlockPos findNearbyEndPortalBlock(BlockPos bp) { + ServerWorld overworld = Fuji.SERVER.getOverworld(); + + // should we find nearby END_PORTAL block ? + if (overworld.getBlockState(bp) == Blocks.END_PORTAL.getDefaultState()) return bp; + + // let's find nearby END_PORTAL block + int radius = 3; + for (int y = -radius; y < radius; y++) { + for (int x = -radius; x < radius; x++) { + for (int z = -radius; z < radius; z++) { + BlockPos test = bp.add(x, y, z); + if (overworld.getBlockState(test) == Blocks.END_PORTAL.getDefaultState()) return test; + } + } + } + + Fuji.LOGGER.warn("BlockPos {} is not END_PORTAL and we can't find a nearby END_PORTAL block !", bp); + return bp; + } + + private BlockPos findCenterEndPortalBlock(BlockPos bp) { + ServerWorld overworld = Fuji.SERVER.getOverworld(); + if (overworld.getBlockState(bp.north()) != Blocks.END_PORTAL.getDefaultState()) { + if (overworld.getBlockState(bp.west()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.south().east(); + } else if (overworld.getBlockState(bp.east()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.south().west(); + } + return bp.south(); + } + if (overworld.getBlockState(bp.south()) != Blocks.END_PORTAL.getDefaultState()) { + if (overworld.getBlockState(bp.west()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.north().east(); + } else if (overworld.getBlockState(bp.east()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.north().west(); + } + return bp.north(); + } + if (overworld.getBlockState(bp.west()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.east(); + } + if (overworld.getBlockState(bp.east()) != Blocks.END_PORTAL.getDefaultState()) { + return bp.west(); + } + // This is the center block. + return bp; + } + + public BlockPos transform(BlockPos bp) { + if (TRANSFORM_CACHE.containsKey(bp)) { + return TRANSFORM_CACHE.get(bp); + } + // fix: for sand-dupe, the blockpos (x, ?, z) of sand may differ +1 or -1 + bp = findNearbyEndPortalBlock(bp); + bp = findCenterEndPortalBlock(bp); + double factor = Configs.configHandler.model().modules.multi_obsidian_platform.factor; + int x = (int) (bp.getX() / factor); + int y = 50; + int z = (int) (bp.getZ() / factor); + int x_offset = x % 16; + int z_offset = z % 16; + x -= x_offset; + z -= z_offset; + x += 100; + TRANSFORM_CACHE.put(bp, new BlockPos(x, y, z)); + return TRANSFORM_CACHE.get(bp); + } + + public void makeObsidianPlatform(ServerWorld serverLevel, BlockPos centerBlockPos) { + int i = centerBlockPos.getX(); + int j = centerBlockPos.getY() - 2; + int k = centerBlockPos.getZ(); + BlockPos.iterate(i - 2, j + 1, k - 2, i + 2, j + 3, k + 2).forEach(blockPos -> serverLevel.setBlockState(blockPos, Blocks.AIR.getDefaultState())); + BlockPos.iterate(i - 2, j, k - 2, i + 2, j, k + 2).forEach(blockPos -> serverLevel.setBlockState(blockPos, Blocks.OBSIDIAN.getDefaultState())); + } + + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/NewbieWelcomeModule.java b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/NewbieWelcomeModule.java new file mode 100644 index 000000000..be29786d3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/NewbieWelcomeModule.java @@ -0,0 +1,19 @@ +package io.github.sakurawald.module.initializer.newbie_welcome; + +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.newbie_welcome.random_teleport.RandomTeleport; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.server.network.ServerPlayerEntity; + + +public class NewbieWelcomeModule extends ModuleInitializer { + + public void welcomeNewbiePlayer(ServerPlayerEntity player) { + /* welcome message */ + MessageUtil.sendBroadcast("newbie_welcome.welcome_message", player.getGameProfile().getName()); + + /* random teleport */ + RandomTeleport.randomTeleport(player, player.getServerWorld(), true); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFinder.java b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFinder.java new file mode 100644 index 000000000..089d08ff2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFinder.java @@ -0,0 +1,15 @@ +package io.github.sakurawald.module.initializer.newbie_welcome.random_teleport; + +import java.util.OptionalInt; +import net.minecraft.world.chunk.Chunk; + +@FunctionalInterface +public interface HeightFinder { + /** + * Attempts to find a safe surface Y value for the specified X & Z values. + * + * @return A Y value corresponding to the player's feet pos + */ + OptionalInt getY(Chunk chunk, int x, int z); +} + diff --git a/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFindingStrategy.java b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFindingStrategy.java new file mode 100644 index 000000000..dedf9665a --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/HeightFindingStrategy.java @@ -0,0 +1,110 @@ +package io.github.sakurawald.module.initializer.newbie_welcome.random_teleport; + +import java.util.OptionalInt; +import net.minecraft.block.BlockState; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.dimension.DimensionTypes; + +public enum HeightFindingStrategy implements HeightFinder { + SKY_TO_SURFACE__FIRST_SOLID(HeightFindingStrategy::findYTopBottom), + BOTTOM_TO_SKY__FIRST_SAFE_AIR(HeightFindingStrategy::findYBottomUp), + ; + + private final HeightFinder heightFinder; + + HeightFindingStrategy(HeightFinder heightFinder) { + this.heightFinder = heightFinder; + } + + private static int calculateMaxY(Chunk chunk) { + final int maxY = chunk.getTopY(); + ChunkSection[] sections = chunk.getSectionArray(); + int maxSectionIndex = Math.min(sections.length - 1, maxY >> 4); + + for (int index = maxSectionIndex; index >= 0; --index) { + if (!sections[index].isEmpty()) { + return Math.min(index << 4 + 15, maxY); + } + } + + return Integer.MAX_VALUE; + } + + public static HeightFindingStrategy forWorld(ServerWorld world) { + if (world.getDimensionKey() == DimensionTypes.OVERWORLD || world.getDimensionKey() == DimensionTypes.THE_END) { + return HeightFindingStrategy.SKY_TO_SURFACE__FIRST_SOLID; + } + if (world.getDimensionKey() == DimensionTypes.THE_NETHER) { + return HeightFindingStrategy.BOTTOM_TO_SKY__FIRST_SAFE_AIR; + } + + // fallback + return HeightFindingStrategy.SKY_TO_SURFACE__FIRST_SOLID; + } + + public static OptionalInt findYTopBottom(Chunk chunk, int x, int z) { + final int maxY = calculateMaxY(chunk); + final int bottomY = chunk.getBottomY(); + if (maxY <= bottomY) { + return OptionalInt.empty(); + } + + final BlockPos.Mutable mutablePos = new BlockPos.Mutable(x, maxY, z); + boolean isAir1 = chunk.getBlockState(mutablePos).isAir(); // Block at head level + boolean isAir2 = chunk.getBlockState(mutablePos.move(Direction.DOWN)).isAir(); // Block at feet level + boolean isAir3; // Block below feet + + while (mutablePos.getY() > bottomY) { + isAir3 = chunk.getBlockState(mutablePos.move(Direction.DOWN)).isAir(); + if (!isAir3 && isAir2 && isAir1) { // If there is a floor block and space for player body+head + return OptionalInt.of(mutablePos.getY() + 1); + } + + isAir1 = isAir2; + isAir2 = isAir3; + } + + return OptionalInt.empty(); + } + + @SuppressWarnings("deprecation") + private static OptionalInt findYBottomUp(Chunk chunk, int x, int z) { + final int topY = getChunkHighestNonEmptySectionYOffsetOrTopY(chunk); + final int bottomY = chunk.getBottomY(); + if (topY <= bottomY) { + return OptionalInt.empty(); + } + + final BlockPos.Mutable mutablePos = new BlockPos.Mutable(x, bottomY, z); + BlockState bsFeet1 = chunk.getBlockState(mutablePos); // Block below feet + BlockState bsBody2 = chunk.getBlockState(mutablePos.move(Direction.UP)); // Block at feet level + BlockState bsHead3; // Block at head level + + while (mutablePos.getY() < topY) { + bsHead3 = chunk.getBlockState(mutablePos.move(Direction.UP)); + if (bsFeet1.isSolid() && bsBody2.isAir() && bsHead3.isAir()) { // If there is a floor block and space for player body+head + return OptionalInt.of(mutablePos.getY() - 1); + } + + bsFeet1 = bsBody2; + bsBody2 = bsHead3; + } + + return OptionalInt.empty(); + } + + public static int getChunkHighestNonEmptySectionYOffsetOrTopY(Chunk chunk) { + int i = chunk.getHighestNonEmptySection(); + return i == chunk.getTopY() ? chunk.getBottomY() : ChunkSectionPos.getBlockCoord(chunk.sectionIndexToCoord(i)); + } + + @Override + public OptionalInt getY(Chunk chunk, int x, int z) { + return heightFinder.getY(chunk, x, z); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/RandomTeleport.java b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/RandomTeleport.java new file mode 100644 index 000000000..3af0babee --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/newbie_welcome/random_teleport/RandomTeleport.java @@ -0,0 +1,159 @@ +package io.github.sakurawald.module.initializer.newbie_welcome.random_teleport; + +import com.google.common.base.Stopwatch; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import java.util.Iterator; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Random; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.Vec3i; +import net.minecraft.world.chunk.Chunk; + +// Thanks to https://github.com/John-Paul-R/Essential-Commands + +public class RandomTeleport { + private static final Executor threadExecutor = Executors.newCachedThreadPool(runnable -> { + var thread = new Thread(runnable, "RTP Location Calculator Thread"); + thread.setUncaughtExceptionHandler((t, e) -> Fuji.LOGGER.error("Exception in RTP calculator thread", e)); + return thread; + }); + + public static void randomTeleport(ServerPlayerEntity player, ServerWorld world, boolean shouldSetSpawnPoint) { + threadExecutor.execute(() -> exec(player, world, shouldSetSpawnPoint)); + } + + private static void exec(ServerPlayerEntity player, ServerWorld world, boolean shouldSetSpawnPoint) { + Fuji.LOGGER.info("Starting RTP location search for {}", player.getGameProfile().getName()); + Stopwatch timer = Stopwatch.createStarted(); + + var centerOpt = getRtpCenter(); + if (centerOpt.isEmpty()) { + return; + } + Vec3i center = centerOpt.get(); + + final var executionContext = new ExecutionContext(world); + final var heightFinder = HeightFindingStrategy.forWorld(world); + + int timesRun = 0; + Optional pos; + do { + timesRun++; + pos = findRtpPosition(world, center, heightFinder, executionContext); + } while (pos.isEmpty() && timesRun <= Configs.configHandler.model().modules.newbie_welcome.random_teleport.max_try_times); + + if (pos.isEmpty()) { + return; + } + + // set spawn point + if (shouldSetSpawnPoint) { + player.setSpawnPoint(world.getRegistryKey(), pos.get(), 0, true, false); + } + + // teleport the player + player.teleport(world, pos.get().getX() + 0.5, pos.get().getY(), pos.get().getZ() + 0.5, 0, 0); + + var cost = timer.stop(); + Fuji.LOGGER.info("RTP: {} has been teleported to ({} {} {} {}) (cost = {})", player.getGameProfile().getName(), world.getRegistryKey().getValue(), pos.get().getX(), pos.get().getY(), pos.get().getZ(), cost); + } + + private static Optional getRtpCenter() { + return Optional.of(new Vec3i(0, 0, 0)); + } + + private static Optional findRtpPosition(ServerWorld world, Vec3i center, HeightFinder heightFinder, ExecutionContext ctx) { + // Search for a valid y-level (not in a block, underwater, out of the world, etc.) + final BlockPos targetXZ = getRandomXZ(center); + final Chunk chunk = world.getChunk(targetXZ); + + for (BlockPos.Mutable candidateBlock : getChunkCandidateBlocks(chunk.getPos())) { + final int x = candidateBlock.getX(); + final int z = candidateBlock.getZ(); + final OptionalInt yOpt = heightFinder.getY(chunk, x, z); + if (yOpt.isEmpty()) { + continue; + } + final int y = yOpt.getAsInt(); + + if (isSafePosition(chunk, new BlockPos(x, y - 2, z), ctx)) { + return Optional.of(new BlockPos(x, y, z)); + } + } + + // This creates an infinite recursive call in the case where all positions on RTP circle are in water. + // Addressed by adding timesRun limit. + return Optional.empty(); + } + + private static BlockPos getRandomXZ(Vec3i center) { + // Calculate position on circle perimeter + var rand = new Random(); + int r_max = Configs.configHandler.model().modules.newbie_welcome.random_teleport.max_distance; + int r_min = Configs.configHandler.model().modules.newbie_welcome.random_teleport.min_distance; + int r = r_max == r_min + ? r_max + : rand.nextInt(r_min, r_max); + final double angle = rand.nextDouble() * 2 * Math.PI; + final double delta_x = r * Math.cos(angle); + final double delta_z = r * Math.sin(angle); + + final int new_x = center.getX() + (int) delta_x; + final int new_z = center.getZ() + (int) delta_z; + return new BlockPos(new_x, 0, new_z); + } + + private static boolean isSafePosition(Chunk chunk, BlockPos pos, ExecutionContext ctx) { + if (pos.getY() <= chunk.getBottomY()) { + return false; + } + + BlockState blockState = chunk.getBlockState(pos); + return pos.getY() < ctx.topY && blockState.getFluidState().isEmpty() && blockState.getBlock() != Blocks.FIRE; + } + + public static Iterable getChunkCandidateBlocks(ChunkPos chunkPos) { + return () -> new Iterator<>() { + private final BlockPos.Mutable _pos = new BlockPos.Mutable(); + private int _idx = -1; + + @Override + public boolean hasNext() { + return _idx < 4; + } + + @Override + public BlockPos.Mutable next() { + _idx++; + return switch (_idx) { + case 0 -> _pos.set(chunkPos.getStartX(), 0, chunkPos.getStartZ()); + case 1 -> _pos.set(chunkPos.getStartX(), 0, chunkPos.getEndZ()); + case 2 -> _pos.set(chunkPos.getEndX(), 0, chunkPos.getStartZ()); + case 3 -> _pos.set(chunkPos.getEndX(), 0, chunkPos.getEndZ()); + case 4 -> _pos.set(chunkPos.getCenterX(), 0, chunkPos.getCenterZ()); + default -> throw new IllegalStateException("Unexpected value: " + _idx); + }; + } + }; + } + + final static class ExecutionContext { + public final int topY; + public final int bottomY; + + public ExecutionContext(ServerWorld world) { + this.topY = world.getTopY(); + this.bottomY = world.getBottomY(); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/ping/PingModule.java b/src/main/java/io/github/sakurawald/module/initializer/ping/PingModule.java new file mode 100644 index 000000000..c5740efbc --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/ping/PingModule.java @@ -0,0 +1,41 @@ +package io.github.sakurawald.module.initializer.ping; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import static net.minecraft.server.command.CommandManager.argument; + + +public class PingModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("ping").executes(this::$ping) + .then(argument("player", EntityArgumentType.player()).executes(this::$ping)) + ); + } + + @SuppressWarnings("SameReturnValue") + private int $ping(CommandContext ctx) { + try { + ServerPlayerEntity target = EntityArgumentType.getPlayer(ctx, "player"); + String name = target.getGameProfile().getName(); + int latency = target.networkHandler.getLatency(); + MessageUtil.sendMessage(ctx.getSource(), "ping.player", name, latency); + } catch (Exception e) { + MessageUtil.sendMessage(ctx.getSource(), "ping.target.no_found"); + } + + return Command.SINGLE_SUCCESS; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/profiler/ProfilerModule.java b/src/main/java/io/github/sakurawald/module/initializer/profiler/ProfilerModule.java new file mode 100644 index 000000000..67a1e96dc --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/profiler/ProfilerModule.java @@ -0,0 +1,156 @@ +package io.github.sakurawald.module.initializer.profiler; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; +import me.lucko.spark.api.Spark; +import me.lucko.spark.api.SparkProvider; +import me.lucko.spark.api.gc.GarbageCollector; +import me.lucko.spark.api.statistic.StatisticWindow; +import me.lucko.spark.api.statistic.misc.DoubleAverageInfo; +import me.lucko.spark.api.statistic.types.DoubleStatistic; +import me.lucko.spark.api.statistic.types.GenericStatistic; +import net.kyori.adventure.text.Component; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + + +public class ProfilerModule extends ModuleInitializer { + public String formatBytes(long bytes) { + if (bytes == -1) return "N/A"; + if (bytes < 1024) { + return bytes + "B"; + } else if (bytes < 1024 * 1024) { + return String.format("%.2fK", (double) bytes / 1024); + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2fM", (double) bytes / (1024 * 1024)); + } else { + return String.format("%.2fG", (double) bytes / (1024 * 1024 * 1024)); + } + } + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("profiler").executes(this::$profiler)); + } + + private int $profiler(CommandContext ctx) { + ServerCommandSource source = ctx.getSource(); + CompletableFuture.runAsync(() -> { + /* get instance */ + Spark spark = null; + try { + spark = SparkProvider.get(); + } catch (Exception ignored) { + } + if (spark == null) { + MessageUtil.sendMessage(source, "profiler.spark.no_instance"); + return; + } + + /* format */ + String os_name = ManagementFactory.getOperatingSystemMXBean().getName(); + String os_version = ManagementFactory.getOperatingSystemMXBean().getVersion(); + String os_arch = ManagementFactory.getOperatingSystemMXBean().getArch(); + + String vmName = ManagementFactory.getRuntimeMXBean().getVmName(); + String vmVersion = ManagementFactory.getRuntimeMXBean().getVmVersion(); + + double tps_5s = 0; + double tps_10s = 0; + double tps_1m = 0; + double tps_5m = 0; + double tps_15m = 0; + DoubleStatistic tps = spark.tps(); + if (tps != null) { + tps_5s = tps.poll(StatisticWindow.TicksPerSecond.SECONDS_5); + tps_10s = tps.poll(StatisticWindow.TicksPerSecond.SECONDS_10); + tps_1m = tps.poll(StatisticWindow.TicksPerSecond.MINUTES_1); + tps_5m = tps.poll(StatisticWindow.TicksPerSecond.MINUTES_5); + tps_15m = tps.poll(StatisticWindow.TicksPerSecond.MINUTES_15); + } + + GenericStatistic mspt = spark.mspt(); + + double mspt_10s_min = 0; + double mspt_10s_median = 0; + double mspt_10s_95percentile = 0; + double mspt_10s_max = 0; + double mspt_1m_min = 0; + double mspt_1m_median = 0; + double mspt_1m_95percentile = 0; + double mspt_1m_max = 0; + if (mspt != null) { + DoubleAverageInfo mspt_10s = mspt.poll(StatisticWindow.MillisPerTick.SECONDS_10); + DoubleAverageInfo mspt_1m = mspt.poll(StatisticWindow.MillisPerTick.MINUTES_1); + mspt_10s_min = mspt_10s.min(); + mspt_10s_median = mspt_10s.median(); + mspt_10s_95percentile = mspt_10s.percentile(0.95); + mspt_10s_max = mspt_10s.max(); + mspt_1m_min = mspt_1m.min(); + mspt_1m_median = mspt_1m.median(); + mspt_1m_95percentile = mspt_1m.percentile(0.95); + mspt_1m_max = mspt_1m.max(); + } + + + DoubleStatistic cpuProcess = spark.cpuProcess(); + DoubleStatistic cpuSystem = spark.cpuSystem(); + double cpu_process_10s = cpuProcess.poll(StatisticWindow.CpuUsage.SECONDS_10) * 100; + double cpu_process_1m = cpuProcess.poll(StatisticWindow.CpuUsage.MINUTES_1) * 100; + double cpu_process_15m = cpuProcess.poll(StatisticWindow.CpuUsage.MINUTES_15) * 100; + double cpu_system_10s = cpuSystem.poll(StatisticWindow.CpuUsage.SECONDS_10) * 100; + double cpu_system_1m = cpuSystem.poll(StatisticWindow.CpuUsage.MINUTES_1) * 100; + double cpu_system_15m = cpuSystem.poll(StatisticWindow.CpuUsage.MINUTES_15) * 100; + + Map gc = spark.gc(); + Component gcComponent = MessageUtil.ofComponent(source, "profiler.format.gc.head"); + int i = 0; + for (GarbageCollector garbageCollector : gc.values()) { + String name = garbageCollector.name(); + double avgFrequency = (double) garbageCollector.avgFrequency() / 1000; + double avgTime = garbageCollector.avgTime(); + long totalCollections = garbageCollector.totalCollections(); + long totalTime = garbageCollector.totalTime(); + gcComponent = gcComponent.append(MessageUtil.ofComponent(source, i == gc.values().size() - 1 ? "profiler.format.gc.last" : "profiler.format.gc.no_last", name, avgFrequency, avgTime, totalCollections, totalTime)); + i++; + } + + Component memComponent = MessageUtil.ofComponent(source, "profiler.format.mem.head"); + List memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); + i = 0; + for (MemoryPoolMXBean memoryPoolMXBean : memoryPoolMXBeans) { + String name = memoryPoolMXBean.getName(); + MemoryType type = memoryPoolMXBean.getType(); + MemoryUsage memoryUsage = memoryPoolMXBean.getUsage(); + String init = formatBytes(memoryUsage.getInit()); + String used = formatBytes(memoryUsage.getUsed()); + String committed = formatBytes(memoryUsage.getCommitted()); + String max = formatBytes(memoryUsage.getMax()); + memComponent = memComponent.append(MessageUtil.ofComponent(source, i == memoryPoolMXBeans.size() - 1 ? "profiler.format.mem.last" : "profiler.format.mem.no_last", name, type, init, used, committed, max)); + i++; + } + + /* output */ + Component formatComponent = MessageUtil.ofComponent(source, "profiler.format" + , os_name, os_version, os_arch + , vmName, vmVersion + , tps_5s, tps_10s, tps_1m, tps_5m, tps_15m + , mspt_10s_min, mspt_10s_median, mspt_10s_95percentile, mspt_10s_max, mspt_1m_min, mspt_1m_median, mspt_1m_95percentile, mspt_1m_max + , cpu_system_10s, cpu_system_1m, cpu_system_15m, cpu_process_10s, cpu_process_1m, cpu_process_15m); + source.sendMessage(formatComponent.appendNewline().append(memComponent).appendNewline().append(gcComponent)); + }); + + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/pvp/PvpModule.java b/src/main/java/io/github/sakurawald/module/initializer/pvp/PvpModule.java new file mode 100644 index 000000000..39a764433 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/pvp/PvpModule.java @@ -0,0 +1,96 @@ +package io.github.sakurawald.module.initializer.pvp; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import java.util.HashSet; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class PvpModule extends ModuleInitializer { + + @Override + public void onInitialize() { + Configs.pvpHandler.loadFromDisk(); + } + + @Override + public void onReload() { + Configs.pvpHandler.loadFromDisk(); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("pvp") + .then(CommandManager.literal("on").executes(this::$on)) + .then(CommandManager.literal("off").executes(this::$off)) + .then(CommandManager.literal("list").executes(this::$list)) + .then(CommandManager.literal("status").executes(this::$status)) + ); + } + + private int $on(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + HashSet whitelist = Configs.pvpHandler.model().whitelist; + String name = player.getGameProfile().getName(); + if (!whitelist.contains(name)) { + whitelist.add(name); + Configs.pvpHandler.saveToDisk(); + + MessageUtil.sendMessage(player, "pvp.on"); + + return Command.SINGLE_SUCCESS; + } + + MessageUtil.sendMessage(player, "pvp.on.already"); + return Command.SINGLE_SUCCESS; + }); + } + + private int $off(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + HashSet whitelist = Configs.pvpHandler.model().whitelist; + String name = player.getGameProfile().getName(); + if (whitelist.contains(name)) { + whitelist.remove(name); + Configs.pvpHandler.saveToDisk(); + + MessageUtil.sendMessage(player, "pvp.off"); + return Command.SINGLE_SUCCESS; + } + + MessageUtil.sendMessage(player, "pvp.off.already"); + return 0; + }); + } + + @SuppressWarnings("SameReturnValue") + private int $status(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + HashSet whitelist = Configs.pvpHandler.model().whitelist; + player.sendMessage(MessageUtil.ofComponent(player, "pvp.status") + .append(whitelist.contains(player.getGameProfile().getName()) ? MessageUtil.ofComponent(player, "on") : MessageUtil.ofComponent(player, "off"))); + return Command.SINGLE_SUCCESS; + }); + } + + private int $list(CommandContext ctx) { + HashSet whitelist = Configs.pvpHandler.model().whitelist; + MessageUtil.sendMessage(ctx.getSource(), "pvp.list", whitelist); + return Command.SINGLE_SUCCESS; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean contains(String name) { + return Configs.pvpHandler.model().whitelist.contains(name); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/repair/RepairModule.java b/src/main/java/io/github/sakurawald/module/initializer/repair/RepairModule.java new file mode 100644 index 000000000..46a5f8930 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/repair/RepairModule.java @@ -0,0 +1,31 @@ +package io.github.sakurawald.module.initializer.repair; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class RepairModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("repair").executes(this::$repair)); + } + + @SuppressWarnings("SameReturnValue") + private int $repair(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.getMainHandStack().setDamage(0); + MessageUtil.sendMessage(player, "repair"); + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/reply/ReplyModule.java b/src/main/java/io/github/sakurawald/module/initializer/reply/ReplyModule.java new file mode 100644 index 000000000..d34a6bc3e --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/reply/ReplyModule.java @@ -0,0 +1,51 @@ +package io.github.sakurawald.module.initializer.reply; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import java.util.HashMap; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +import static net.minecraft.server.command.CommandManager.argument; + + +public class ReplyModule extends ModuleInitializer { + + private final HashMap player2target = new HashMap<>(); + + public void updateReplyTarget(String player, String target) { + this.player2target.put(player, target); + } + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("reply").then(argument("message", StringArgumentType.greedyString()).executes(this::$reply))); + } + + @SuppressWarnings("SameReturnValue") + private int $reply(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + + String target = this.player2target.get(player.getGameProfile().getName()); + String message = StringArgumentType.getString(ctx, "message"); + + try { + Fuji.SERVER.getCommandManager().getDispatcher().execute("msg %s %s".formatted(target, message), player.getCommandSource()); + } catch (CommandSyntaxException e) { + MessageUtil.sendMessage(player, "reply.no_target"); + } + + return Command.SINGLE_SUCCESS; + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/FilteredRegistry.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/FilteredRegistry.java new file mode 100644 index 000000000..5533227b8 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/FilteredRegistry.java @@ -0,0 +1,180 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import com.google.common.collect.Iterators; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Lifecycle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Stream; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.entry.RegistryEntryList; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; + +@SuppressWarnings({"unused", "InfiniteRecursion", "LombokGetterMayBeUsed"}) +public class FilteredRegistry extends SimpleRegistry { + private final Registry source; + private final Predicate check; + + public FilteredRegistry(Registry source, Predicate check) { + super(source.getKey(), source.getLifecycle()); + this.source = source; + this.check = check; + } + + public Registry getSource() { + return this.source; + } + + @Nullable + @Override + public Identifier getId(T value) { + return check.test(value) ? this.source.getId(value) : null; + } + + @Override + public Optional> getKey(T entry) { + return check.test(entry) ? this.source.getKey(entry) : Optional.empty(); + } + + @Override + public int getRawId(@Nullable T value) { + return check.test(value) ? this.source.getRawId(value) : -1; + } + + @Nullable + @Override + public T get(int index) { + return this.source.get(index); + } + + @Override + public int size() { + return this.source.size(); + } + + @Nullable + @Override + public T get(@Nullable RegistryKey key) { + return this.source.get(key); + } + + @Nullable + @Override + public T get(@Nullable Identifier id) { + return this.get(id); + } + + @Override + public Lifecycle getEntryLifecycle(T entry) { + return this.source.getEntryLifecycle(entry); + } + + @Override + public Lifecycle getLifecycle() { + return this.source.getLifecycle(); + } + + @Override + public Set getIds() { + return this.getIds(); + } + + @Override + public Set, T>> getEntrySet() { + Set, T>> set = new HashSet<>(); + for (Map.Entry, T> e : this.source.getEntrySet()) { + if (this.check.test(e.getValue())) { + set.add(e); + } + } + return set; + } + + @Override + public Set> getKeys() { + return null; + } + + @Override + public Optional> getRandom(net.minecraft.util.math.random.Random random) { + return Optional.empty(); + } + + @Override + public boolean containsId(Identifier id) { + return this.source.containsId(id); + } + + @Override + public boolean contains(RegistryKey key) { + return this.source.contains(key); + } + + @Override + public Registry freeze() { + return this; + } + + @Override + public RegistryEntry.Reference createEntry(T value) { + return null; + } + + @Override + public Optional> getEntry(int rawId) { + return this.source.getEntry(rawId); + } + + @Override + public Optional> getEntry(RegistryKey key) { + return this.source.getEntry(key); + } + + @Override + public Stream> streamEntries() { + return this.source.streamEntries().filter((e) -> this.check.test(e.value)); + } + + @Override + public Optional> getEntryList(TagKey tag) { + return Optional.empty(); + } + + @Override + public RegistryEntryList.Named getOrCreateEntryList(TagKey tag) { + return null; + } + + @Override + public Stream, RegistryEntryList.Named>> streamTagsAndEntries() { + return null; + } + + @Override + public Stream> streamTags() { + return null; + } + + @Override + public void clearTags() { + + } + + @Override + public void populateTags(Map, List>> tagEntries) { + + } + + @NotNull + @Override + public Iterator iterator() { + return Iterators.filter(this.source.iterator(), this.check::test); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorld.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorld.java new file mode 100644 index 000000000..a52fab5a3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorld.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.WorldGenerationProgressListener; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.random.RandomSequencesState; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionOptions; +import net.minecraft.world.level.ServerWorldProperties; +import net.minecraft.world.level.storage.LevelStorage; +import net.minecraft.world.spawner.SpecialSpawner; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.Executor; + +public class ResourceWorld extends ServerWorld { + + public ResourceWorld(MinecraftServer server, Executor workerExecutor, LevelStorage.Session session, ServerWorldProperties properties, RegistryKey worldKey, DimensionOptions dimensionOptions, WorldGenerationProgressListener worldGenerationProgressListener, boolean debugWorld, long seed, List spawners, boolean shouldTickTime, @Nullable RandomSequencesState randomSequencesState) { + super(server, workerExecutor, session, properties, worldKey, dimensionOptions, worldGenerationProgressListener, debugWorld, seed, spawners, shouldTickTime, randomSequencesState); + } + + /* + The main issue is that the runtime world must return the custom seed through World#getSeed, including within the ServerWorld constructor. + The solution is to override World#getSeed in a way that the seed is initialized before it is called. + Please note that: all the resource world will not save its data (properties) into level.dat, so if you restart the server. + then the seed of resource world will be changed randomly (and then the chunk generator will generate new chunks with the new seed). + */ + @Override + public long getSeed() { + return ((ResourceWorldProperties) this.properties).getSeed(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldManager.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldManager.java new file mode 100644 index 000000000..6f9e32ab9 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldManager.java @@ -0,0 +1,120 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.resource_world.interfaces.SimpleRegistryMixinInterface; +import io.github.sakurawald.module.initializer.teleport_warmup.Position; +import io.github.sakurawald.module.initializer.teleport_warmup.TeleportTicket; +import io.github.sakurawald.module.initializer.teleport_warmup.TeleportWarmupModule; +import io.github.sakurawald.module.mixin.resource_world.MinecraftServerAccessor; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionOptions; +import net.minecraft.world.level.storage.LevelStorage; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ResourceWorldManager { + + private static final TeleportWarmupModule teleportWarmupModule = ModuleManager.getInitializer(TeleportWarmupModule.class); + private static final Set deletionQueue = new ReferenceOpenHashSet<>(); + + static { + ServerTickEvents.START_SERVER_TICK.register(server -> tick()); + } + + public static void enqueueWorldDeletion(ServerWorld world) { + MinecraftServer server = world.getServer(); + server.submit(() -> { + deletionQueue.add(world); + }); + } + + private static void tick() { + if (!deletionQueue.isEmpty()) { + deletionQueue.removeIf(ResourceWorldManager::tickDeleteWorld); + } + } + + private static boolean tickDeleteWorld(ServerWorld world) { + if (isWorldUnloaded(world)) { + delete(world); + return true; + } else { + kickPlayers(world); + return false; + } + } + + private static void kickPlayers(ServerWorld world) { + if (world.getPlayers().isEmpty()) { + return; + } + + ServerWorld overworld = world.getServer().getOverworld(); + BlockPos spawnPos = overworld.getSpawnPos(); + + List players = new ArrayList<>(world.getPlayers()); + for (ServerPlayerEntity player : players) { + // fix: if the player is inside resource-world while resetting the worlds, then resource worlds will delay its deletion until the player left the resource-world. + if (teleportWarmupModule != null) { + teleportWarmupModule.tickets.put(player.getGameProfile().getName(), + new TeleportTicket(player + , Position.of(player), new Position(overworld, spawnPos.getX() + 0.5, spawnPos.getY() + 0.5, spawnPos.getZ() + 0.5, 0, 0) + , true)); + } + player.teleport(overworld, spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5, overworld.getSpawnAngle(), 0.0F); + } + } + + private static boolean isWorldUnloaded(ServerWorld world) { + return world.getPlayers().isEmpty() && world.getChunkManager().getLoadedChunkCount() <= 0; + } + + private static SimpleRegistry getDimensionsRegistry(MinecraftServer server) { + DynamicRegistryManager registryManager = server.getCombinedDynamicRegistries().getCombinedRegistryManager(); + return (SimpleRegistry) registryManager.get(RegistryKeys.DIMENSION); + } + + private static void delete(ServerWorld world) { + MinecraftServer server = world.getServer(); + MinecraftServerAccessor serverAccess = (MinecraftServerAccessor) server; + + RegistryKey dimensionKey = world.getRegistryKey(); + if (serverAccess.getWorlds().remove(dimensionKey, world)) { + ServerWorldEvents.UNLOAD.invoker().onWorldUnload(server, world); + SimpleRegistry dimensionsRegistry = getDimensionsRegistry(server); + SimpleRegistryMixinInterface.remove(dimensionsRegistry, dimensionKey.getValue()); + LevelStorage.Session session = serverAccess.getSession(); + File worldDirectory = session.getWorldDirectory(dimensionKey).toFile(); + cleanFiles(worldDirectory); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void cleanFiles(File file) { + if (file.exists() && file.isDirectory()) { + File[] files = file.listFiles(); + if (files == null) return; + for (File child : files) { + if (child.isDirectory()) { + cleanFiles(child); + } else { + child.delete(); + } + } + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldModule.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldModule.java new file mode 100644 index 000000000..8dad05721 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldModule.java @@ -0,0 +1,265 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.Lifecycle; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.config.model.ConfigModel; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.newbie_welcome.random_teleport.RandomTeleport; +import io.github.sakurawald.module.initializer.resource_world.interfaces.DimensionOptionsMixinInterface; +import io.github.sakurawald.module.initializer.resource_world.interfaces.SimpleRegistryMixinInterface; +import io.github.sakurawald.module.mixin.resource_world.MinecraftServerAccessor; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.boss.dragon.EnderDragonFight; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.random.RandomSeed; +import net.minecraft.world.World; +import net.minecraft.world.biome.source.BiomeAccess; +import net.minecraft.world.dimension.DimensionOptions; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.dimension.DimensionTypes; +import net.minecraft.world.gen.chunk.ChunkGenerator; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import static net.minecraft.server.command.CommandManager.literal; + + +public class ResourceWorldModule extends ModuleInitializer { + + private final String DEFAULT_RESOURCE_WORLD_NAMESPACE = "resource_world"; + private final String DEFAULT_THE_NETHER_PATH = "the_nether"; + private final String DEFAULT_THE_END_PATH = "the_end"; + private final String DEFAULT_OVERWORLD_PATH = "overworld"; + + @Override + public void onInitialize() { + ServerWorldEvents.UNLOAD.register(this::onWorldUnload); + ServerLifecycleEvents.SERVER_STARTED.register((server) -> { + this.loadWorlds(server); + this.registerScheduleTask(server); + }); + } + + public void registerScheduleTask(MinecraftServer server) { + ScheduleUtil.addJob(ResourceWorldAutoResetJob.class, null, null, Configs.configHandler.model().modules.resource_world.auto_reset_cron, new JobDataMap() { + { + this.put(MinecraftServer.class.getName(), server); + this.put(ResourceWorldModule.class.getName(), ResourceWorldModule.this); + } + }); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("rw") + .then(literal("reset").requires(source -> source.hasPermissionLevel(4)).executes(this::$reset)) + .then(literal("delete").requires(source -> source.hasPermissionLevel(4)).then(literal(DEFAULT_OVERWORLD_PATH).executes(this::$delete)) + .then(literal(DEFAULT_THE_NETHER_PATH).executes(this::$delete)) + .then(literal(DEFAULT_THE_END_PATH).executes(this::$delete))) + .then(literal("tp").then(literal(DEFAULT_OVERWORLD_PATH).executes(this::$tp)) + .then(literal(DEFAULT_THE_NETHER_PATH).executes(this::$tp)) + .then(literal(DEFAULT_THE_END_PATH).executes(this::$tp)) + ) + ); + } + + private int $reset(CommandContext ctx) { + resetWorlds(ctx.getSource().getServer()); + return Command.SINGLE_SUCCESS; + } + + private void resetWorlds(MinecraftServer server) { + MessageUtil.sendBroadcast("resource_world.world.reset"); + Configs.configHandler.model().modules.resource_world.seed = RandomSeed.getSeed(); + Configs.configHandler.saveToDisk(); + deleteWorld(server, DEFAULT_OVERWORLD_PATH); + deleteWorld(server, DEFAULT_THE_NETHER_PATH); + deleteWorld(server, DEFAULT_THE_END_PATH); + } + + public void loadWorlds(MinecraftServer server) { + long seed = Configs.configHandler.model().modules.resource_world.seed; + + ConfigModel.Modules.ResourceWorld.ResourceWorlds resourceWorlds = Configs.configHandler.model().modules.resource_world.resource_worlds; + if (resourceWorlds.enable_overworld) { + createWorld(server, DimensionTypes.OVERWORLD, DEFAULT_OVERWORLD_PATH, seed); + } + if (resourceWorlds.enable_the_nether) { + createWorld(server, DimensionTypes.THE_NETHER, DEFAULT_THE_NETHER_PATH, seed); + } + if (resourceWorlds.enable_the_end) { + createWorld(server, DimensionTypes.THE_END, DEFAULT_THE_END_PATH, seed); + } + } + + @SuppressWarnings("DataFlowIssue") + private ChunkGenerator getChunkGenerator(MinecraftServer server, RegistryKey dimensionTypeRegistryKey) { + if (dimensionTypeRegistryKey == DimensionTypes.OVERWORLD) { + return server.getWorld(World.OVERWORLD).getChunkManager().getChunkGenerator(); + } + if (dimensionTypeRegistryKey == DimensionTypes.THE_NETHER) { + return server.getWorld(World.NETHER).getChunkManager().getChunkGenerator(); + } + if (dimensionTypeRegistryKey == DimensionTypes.THE_END) { + return server.getWorld(World.END).getChunkManager().getChunkGenerator(); + } + return null; + } + + private DimensionOptions createDimensionOptions(MinecraftServer server, RegistryKey dimensionTypeRegistryKey) { + RegistryEntry dimensionTypeRegistryEntry = getDimensionTypeRegistryEntry(server, dimensionTypeRegistryKey); + ChunkGenerator chunkGenerator = getChunkGenerator(server, dimensionTypeRegistryKey); + //noinspection DataFlowIssue + return new DimensionOptions(dimensionTypeRegistryEntry, chunkGenerator); + } + + private RegistryEntry getDimensionTypeRegistryEntry(MinecraftServer server, RegistryKey dimensionTypeRegistryKey) { + return server.getRegistryManager().get(RegistryKeys.DIMENSION_TYPE).getEntry(dimensionTypeRegistryKey).orElse(null); + } + + private RegistryKey getDimensionTypeRegistryKeyByPath(String path) { + if (path.equals(DEFAULT_OVERWORLD_PATH)) return DimensionTypes.OVERWORLD; + if (path.equals(DEFAULT_THE_NETHER_PATH)) return DimensionTypes.THE_NETHER; + if (path.equals(DEFAULT_THE_END_PATH)) return DimensionTypes.THE_END; + return null; + } + + + private SimpleRegistry getDimensionOptionsRegistry(MinecraftServer server) { + DynamicRegistryManager registryManager = server.getCombinedDynamicRegistries().getCombinedRegistryManager(); + return (SimpleRegistry) registryManager.get(RegistryKeys.DIMENSION); + } + + @SuppressWarnings("deprecation") + private void createWorld(MinecraftServer server, RegistryKey dimensionTypeRegistryKey, String path, long seed) { + /* create the world */ + // note: we use the same WorldData from OVERWORLD + ResourceWorldProperties resourceWorldProperties = new ResourceWorldProperties(server.getSaveProperties(), seed); + RegistryKey worldRegistryKey = RegistryKey.of(RegistryKeys.WORLD, new Identifier(DEFAULT_RESOURCE_WORLD_NAMESPACE, path)); + DimensionOptions dimensionOptions = createDimensionOptions(server, dimensionTypeRegistryKey); + MinecraftServerAccessor serverAccessor = (MinecraftServerAccessor) server; + ServerWorld world = new ResourceWorld(server, + Util.getMainWorkerExecutor(), + serverAccessor.getSession(), + resourceWorldProperties, + worldRegistryKey, + dimensionOptions, + VoidWorldGenerationProgressListener.INSTANCE, + false, + BiomeAccess.hashSeed(seed), + ImmutableList.of(), + true, + null); + + if (dimensionTypeRegistryKey == DimensionTypes.THE_END) { + world.setEnderDragonFight(new EnderDragonFight(world, world.getSeed(), EnderDragonFight.Data.DEFAULT)); + } + + /* register the world */ + ((DimensionOptionsMixinInterface) (Object) dimensionOptions).fuji$setSaveProperties(false); + + SimpleRegistry dimensionsRegistry = getDimensionOptionsRegistry(server); + boolean isFrozen = ((SimpleRegistryMixinInterface) dimensionsRegistry).fuji$isFrozen(); + ((SimpleRegistryMixinInterface) dimensionsRegistry).fuji$setFrozen(false); + var dimensionOptionsRegistryKey = RegistryKey.of(RegistryKeys.DIMENSION, worldRegistryKey.getValue()); + if (!dimensionsRegistry.contains(dimensionOptionsRegistryKey)) { + dimensionsRegistry.add(dimensionOptionsRegistryKey, dimensionOptions, Lifecycle.stable()); + } + ((SimpleRegistryMixinInterface) dimensionsRegistry).fuji$setFrozen(isFrozen); + + serverAccessor.getWorlds().put(world.getRegistryKey(), world); + ServerWorldEvents.LOAD.invoker().onWorldLoad(server, world); + world.tick(() -> true); + MessageUtil.sendBroadcast("resource_world.world.created", path); + } + + private ServerWorld getResourceWorldByPath(MinecraftServer server, String path) { + RegistryKey worldKey = RegistryKey.of(RegistryKeys.WORLD, new Identifier(DEFAULT_RESOURCE_WORLD_NAMESPACE, path)); + return server.getWorld(worldKey); + } + + private int $tp(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + String path = ctx.getNodes().get(2).getNode().getName(); + ServerWorld world = getResourceWorldByPath(ctx.getSource().getServer(), path); + if (world == null) { + MessageUtil.sendMessage(ctx.getSource(), "resource_world.world.not_found", path); + return 0; + } + + if (world.getDimensionKey() == DimensionTypes.THE_END) { + ServerWorld.createEndSpawnPlatform(world); + BlockPos endSpawnPos = ServerWorld.END_SPAWN_POS; + player.teleport(world, endSpawnPos.getX() + 0.5, endSpawnPos.getY(), endSpawnPos.getZ() + 0.5, 90, 0); + } else { + MessageUtil.sendActionBar(player, "resource_world.world.tp.tip"); + RandomTeleport.randomTeleport(player, world, false); + } + + return Command.SINGLE_SUCCESS; + }); + } + + + private void deleteWorld(MinecraftServer server, String path) { + ServerWorld world = getResourceWorldByPath(server, path); + if (world == null) return; + + ResourceWorldManager.enqueueWorldDeletion(world); + MessageUtil.sendBroadcast("resource_world.world.deleted", path); + } + + private int $delete(CommandContext ctx) { + String path = ctx.getNodes().get(2).getNode().getName(); + deleteWorld(ctx.getSource().getServer(), path); + return Command.SINGLE_SUCCESS; + } + + public void onWorldUnload(MinecraftServer server, ServerWorld world) { + if (server.isRunning()) { + String namespace = world.getRegistryKey().getValue().getNamespace(); + String path = world.getRegistryKey().getValue().getPath(); + // IMPORTANT: only delete the world if it's a resource world + if (!namespace.equals(DEFAULT_RESOURCE_WORLD_NAMESPACE)) return; + + Fuji.LOGGER.info("onWorldUnload() -> Creating world {} ...", path); + long seed = Configs.configHandler.model().modules.resource_world.seed; + this.createWorld(server, this.getDimensionTypeRegistryKeyByPath(path), path, seed); + } + } + + public static class ResourceWorldAutoResetJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + Fuji.LOGGER.info("Start to reset resource worlds."); + MinecraftServer server = (MinecraftServer) context.getJobDetail().getJobDataMap().get(MinecraftServer.class.getName()); + ResourceWorldModule module = (ResourceWorldModule) context.getJobDetail().getJobDataMap().get(ResourceWorldModule.class.getName()); + server.execute(() -> module.resetWorlds(server)); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldProperties.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldProperties.java new file mode 100644 index 000000000..f786c4c68 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/ResourceWorldProperties.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import net.minecraft.world.SaveProperties; +import net.minecraft.world.level.UnmodifiableLevelProperties; + +/** + * The only purpose of this class is to warp the seed. + **/ +@SuppressWarnings("LombokGetterMayBeUsed") +public final class ResourceWorldProperties extends UnmodifiableLevelProperties { + + private final long seed; + + public ResourceWorldProperties(SaveProperties saveProperties, long seed) { + super(saveProperties, saveProperties.getMainWorldProperties()); + this.seed = seed; + } + + public long getSeed() { + return seed; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/SafeIterator.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/SafeIterator.java new file mode 100644 index 000000000..5d569aa6b --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/SafeIterator.java @@ -0,0 +1,24 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import java.util.Collection; +import java.util.Iterator; + +public final class SafeIterator implements Iterator { + private final Object[] values; + private int index = 0; + + public SafeIterator(Collection source) { + this.values = source.toArray(); + } + + @Override + public boolean hasNext() { + return this.values.length > this.index; + } + + @SuppressWarnings("unchecked") + @Override + public T next() { + return (T) this.values[this.index++]; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/VoidWorldGenerationProgressListener.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/VoidWorldGenerationProgressListener.java new file mode 100644 index 000000000..03bfc2828 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/VoidWorldGenerationProgressListener.java @@ -0,0 +1,31 @@ +package io.github.sakurawald.module.initializer.resource_world; + +import net.minecraft.server.WorldGenerationProgressListener; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.chunk.ChunkStatus; +import org.jetbrains.annotations.Nullable; + +public class VoidWorldGenerationProgressListener implements WorldGenerationProgressListener { + + public static final VoidWorldGenerationProgressListener INSTANCE = new VoidWorldGenerationProgressListener(); + + @Override + public void start(ChunkPos spawnPos) { + + } + + @Override + public void setChunkStatus(ChunkPos pos, @Nullable ChunkStatus status) { + + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/DimensionOptionsMixinInterface.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/DimensionOptionsMixinInterface.java new file mode 100644 index 000000000..528a2a6a9 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/DimensionOptionsMixinInterface.java @@ -0,0 +1,15 @@ +package io.github.sakurawald.module.initializer.resource_world.interfaces; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Predicate; +import net.minecraft.world.dimension.DimensionOptions; + +@ApiStatus.Internal +public interface DimensionOptionsMixinInterface { + Predicate SAVE_PROPERTIES_PREDICATE = (e) -> ((DimensionOptionsMixinInterface) (Object) e).fuji$getSaveProperties(); + + void fuji$setSaveProperties(boolean value); + + boolean fuji$getSaveProperties(); +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/SimpleRegistryMixinInterface.java b/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/SimpleRegistryMixinInterface.java new file mode 100644 index 000000000..50dd7eda7 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/resource_world/interfaces/SimpleRegistryMixinInterface.java @@ -0,0 +1,26 @@ +package io.github.sakurawald.module.initializer.resource_world.interfaces; + +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface SimpleRegistryMixinInterface { + @SuppressWarnings("unchecked") + static boolean remove(SimpleRegistry registry, Identifier key) { + return ((SimpleRegistryMixinInterface) registry).fuji$remove(key); + } + + @SuppressWarnings("unchecked") + static boolean remove(SimpleRegistry registry, T value) { + return ((SimpleRegistryMixinInterface) registry).fuji$remove(value); + } + + boolean fuji$remove(T value); + + boolean fuji$remove(Identifier key); + + void fuji$setFrozen(boolean value); + + boolean fuji$isFrozen(); +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/scheduler/ScheduleJob.java b/src/main/java/io/github/sakurawald/module/initializer/scheduler/ScheduleJob.java new file mode 100644 index 000000000..29b41bdc8 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/scheduler/ScheduleJob.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.scheduler; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; +import java.util.Random; + +@Data +@AllArgsConstructor + +public class ScheduleJob { + String name; + boolean enable; + int left_trigger_times; + List crons; + List> commands_list; + + public void trigger() { + Fuji.LOGGER.info("Trigger ScheduleJob {}", this.getName()); + + if (left_trigger_times > 0) { + left_trigger_times--; + if (left_trigger_times == 0) { + this.enable = false; + } + Configs.schedulerHandler.saveToDisk(); + } + + List commands = this.commands_list.get(new Random().nextInt(this.commands_list.size())); + SpecializedCommand.runSpecializedCommands(Fuji.SERVER, commands); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/scheduler/SchedulerModule.java b/src/main/java/io/github/sakurawald/module/initializer/scheduler/SchedulerModule.java new file mode 100644 index 000000000..f154cc981 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/scheduler/SchedulerModule.java @@ -0,0 +1,91 @@ +package io.github.sakurawald.module.initializer.scheduler; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.ScheduleUtil; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +import static net.minecraft.server.command.CommandManager.argument; + + +public class SchedulerModule extends ModuleInitializer { + + private void updateJobs() { + ScheduleUtil.removeJobs(ScheduleJobJob.class.getName()); + Configs.schedulerHandler.model().scheduleJobs.forEach(scheduleJob -> { + + if (scheduleJob.enable) { + scheduleJob.crons.forEach(cron -> ScheduleUtil.addJob(ScheduleJobJob.class, null, null, cron, new JobDataMap() { + { + this.put("job", scheduleJob); + } + })); + Fuji.LOGGER.info("SchedulerModule: Add ScheduleJob {}", scheduleJob); + } + }); + } + + @Override + public void onInitialize() { + Configs.schedulerHandler.loadFromDisk(); + updateJobs(); + } + + @Override + public void onReload() { + Configs.schedulerHandler.loadFromDisk(); + updateJobs(); + } + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("scheduler_trigger").requires(s -> s.hasPermissionLevel(4)) + .then(argument("name", StringArgumentType.word()).suggests(new SchedulerJobSuggestionProvider()).executes(this::$scheduler_trigger))); + } + + private int $scheduler_trigger(CommandContext ctx) { + String name = StringArgumentType.getString(ctx, "name"); + + Configs.schedulerHandler.model().scheduleJobs.forEach(job -> { + if (job.name.equals(name)) { + job.trigger(); + } + }); + return Command.SINGLE_SUCCESS; + } + + + private static class SchedulerJobSuggestionProvider implements SuggestionProvider { + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { + Configs.schedulerHandler.model().scheduleJobs.forEach(job -> builder.suggest(job.name)); + return builder.buildFuture(); + } + } + + public static class ScheduleJobJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + ScheduleJob job = (ScheduleJob) context.getJobDetail().getJobDataMap().get("job"); + job.trigger(); + } + } +} + diff --git a/src/main/java/io/github/sakurawald/module/initializer/scheduler/SpecializedCommand.java b/src/main/java/io/github/sakurawald/module/initializer/scheduler/SpecializedCommand.java new file mode 100644 index 000000000..8a178600a --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/scheduler/SpecializedCommand.java @@ -0,0 +1,67 @@ +package io.github.sakurawald.module.initializer.scheduler; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.sakurawald.Fuji; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import java.util.List; +import java.util.Random; + + +public class SpecializedCommand { + + // TODO: a language parser is needed here (supports some expressions solver) + + private static final String RANDOM_PLAYER = "!random_player!"; + private static final String ALL_PLAYER = "!all_player!"; + + public static void runSpecializedCommands(MinecraftServer server, List commands) { + + /* context */ + String randomPlayer = null; + String[] onlinePlayers = server.getPlayerNames(); + + /* resolve */ + for (String command : commands) { + /* resolve random player */ + if (command.contains(RANDOM_PLAYER)) { + if (randomPlayer == null) { + randomPlayer = onlinePlayers[new Random().nextInt(onlinePlayers.length)]; + } + command = command.replace(RANDOM_PLAYER, randomPlayer); + } + + /* resolve all players */ + if (command.contains(ALL_PLAYER)) { + for (String onlinePlayer : onlinePlayers) { + executeCommand(server, command.replace(ALL_PLAYER, onlinePlayer)); + } + } else { + executeCommand(server, command); + } + } + } + + public static void executeCommand(MinecraftServer server, String command) { + try { + server.getCommandManager().getDispatcher().execute(command, server.getCommandSource()); + } catch (CommandSyntaxException e) { + Fuji.LOGGER.error(e.toString()); + } + } + + public static void executeCommands(ServerPlayerEntity player, List commands) { + commands.forEach(command -> executeCommand(player, command)); + } + + public static void executeCommand(ServerPlayerEntity player, String command) { + try { + Fuji.SERVER.getCommandManager().getDispatcher().execute(command, player.getCommandSource()); + } catch (CommandSyntaxException e) { + player.sendMessage(Component.text(e.getMessage()).color(NamedTextColor.RED)); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/seen/GameProfileCacheEx.java b/src/main/java/io/github/sakurawald/module/initializer/seen/GameProfileCacheEx.java new file mode 100644 index 000000000..b44b4127e --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/seen/GameProfileCacheEx.java @@ -0,0 +1,9 @@ +package io.github.sakurawald.module.initializer.seen; + +import java.util.Collection; + +public interface GameProfileCacheEx { + + Collection fuji$getNames(); + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/seen/SeenModule.java b/src/main/java/io/github/sakurawald/module/initializer/seen/SeenModule.java new file mode 100644 index 000000000..cd6da3796 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/seen/SeenModule.java @@ -0,0 +1,53 @@ +package io.github.sakurawald.module.initializer.seen; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.handler.ConfigHandler; +import io.github.sakurawald.config.handler.ObjectConfigHandler; +import io.github.sakurawald.config.model.SeenModel; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.DateUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +@SuppressWarnings("LombokGetterMayBeUsed") + +public class SeenModule extends ModuleInitializer { + + @Getter + private final ConfigHandler data = new ObjectConfigHandler<>("seen.json", SeenModel.class); + + @Override + public void onInitialize() { + data.loadFromDisk(); + } + + @Override + public void onReload() { + data.loadFromDisk(); + } + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("seen").then(CommandUtil.offlinePlayerArgument().executes(this::$seen))); + } + + @SuppressWarnings("SameReturnValue") + private int $seen(CommandContext ctx) { + String target = StringArgumentType.getString(ctx, "player"); + if (data.model().player2seen.containsKey(target)) { + Long time = data.model().player2seen.get(target); + MessageUtil.sendMessage(ctx.getSource(), "seen.success", target, DateUtil.toStandardDateFormat(time)); + } else { + MessageUtil.sendMessage(ctx.getSource(), "seen.fail"); + } + return Command.SINGLE_SUCCESS; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/SkinRestorer.java b/src/main/java/io/github/sakurawald/module/initializer/skin/SkinRestorer.java new file mode 100644 index 000000000..0144bcca1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/SkinRestorer.java @@ -0,0 +1,129 @@ +package io.github.sakurawald.module.initializer.skin; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.skin.io.SkinIO; +import io.github.sakurawald.module.initializer.skin.io.SkinStorage; +import it.unimi.dsi.fastutil.Pair; +import lombok.Getter; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.s2c.play.EntitiesDestroyS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityPositionS2CPacket; +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityStatusEffectS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; +import net.minecraft.network.packet.s2c.play.ExperienceBarUpdateS2CPacket; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; +import net.minecraft.network.packet.s2c.play.PlayerRemoveS2CPacket; +import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket; +import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static io.github.sakurawald.Fuji.LOGGER; + +// Thanks to: https://modrinth.com/mod/skinrestorer + +public class SkinRestorer { + + private static final Gson gson = new Gson(); + @Getter + private static final SkinStorage skinStorage = new SkinStorage(new SkinIO(Fuji.CONFIG_PATH.resolve("skin"))); + + + public static CompletableFuture, Collection>> setSkinAsync(MinecraftServer server, Collection targets, Supplier skinSupplier) { + return CompletableFuture.>>supplyAsync(() -> { + HashSet acceptedProfiles = new HashSet<>(); + Property skin = skinSupplier.get(); + + LOGGER.debug("skinSupplier.get() -> skin = {}", skin); + if (skin == null) { + LOGGER.error("Cannot get the skin for {}", targets.stream().findFirst().orElseThrow()); + return Pair.of(null, Collections.emptySet()); + } + + for (GameProfile profile : targets) { + SkinRestorer.getSkinStorage().setSkin(profile.getId(), skin); + acceptedProfiles.add(profile); + } + + return Pair.of(skin, acceptedProfiles); + })., Collection>>thenApplyAsync(pair -> { + Property skin = pair.left(); + if (skin == null) + return Pair.of(Collections.emptySet(), Collections.emptySet()); + + Collection acceptedProfiles = pair.right(); + HashSet acceptedPlayers = new HashSet<>(); + JsonObject newSkinJson = gson.fromJson(new String(Base64.getDecoder().decode(skin.value()), StandardCharsets.UTF_8), JsonObject.class); + newSkinJson.remove("timestamp"); + for (GameProfile profile : acceptedProfiles) { + ServerPlayerEntity player = server.getPlayerManager().getPlayer(profile.getId()); + + if (player == null || arePropertiesEquals(newSkinJson, player.getGameProfile())) + continue; + + applyRestoredSkin(player, skin); + for (PlayerEntity observer : player.getWorld().getPlayers()) { + ServerPlayerEntity observer1 = (ServerPlayerEntity) observer; + observer1.networkHandler.sendPacket(new PlayerRemoveS2CPacket(Collections.singletonList(player.getUuid()))); + observer1.networkHandler.sendPacket(new PlayerListS2CPacket(PlayerListS2CPacket.Action.ADD_PLAYER, player)); // refresh the player information + if (player != observer1 && observer1.canSee(player)) { + observer1.networkHandler.sendPacket(new EntitiesDestroyS2CPacket(player.getId())); + observer1.networkHandler.sendPacket(new EntitySpawnS2CPacket(player)); + observer1.networkHandler.sendPacket(new EntityPositionS2CPacket(player)); + observer1.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(player.getId(), player.getDataTracker().getChangedEntries())); + } else if (player == observer1) { + observer1.networkHandler.sendPacket(new PlayerRespawnS2CPacket(player.createCommonPlayerSpawnInfo(player.getServerWorld()), (byte) 2)); + observer1.networkHandler.sendPacket(new UpdateSelectedSlotS2CPacket(observer1.getInventory().selectedSlot)); + observer1.sendAbilitiesUpdate(); + observer1.playerScreenHandler.updateToClient(); + for (StatusEffectInstance instance : observer1.getStatusEffects()) { + observer1.networkHandler.sendPacket(new EntityStatusEffectS2CPacket(observer1.getId(), instance)); + } + observer1.networkHandler.requestTeleport(observer1.getX(), observer1.getY(), observer1.getZ(), observer1.getYaw(), observer1.getPitch()); + observer1.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(player.getId(), player.getDataTracker().getChangedEntries())); + observer1.networkHandler.sendPacket(new ExperienceBarUpdateS2CPacket(player.experienceProgress, player.totalExperience, player.experienceLevel)); + } + } + acceptedPlayers.add(player); + } + return Pair.of(acceptedPlayers, acceptedProfiles); + }, server).orTimeout(10, TimeUnit.SECONDS).exceptionally(e -> Pair.of(Collections.emptySet(), Collections.emptySet())); + } + + private static void applyRestoredSkin(ServerPlayerEntity playerEntity, Property skin) { + playerEntity.getGameProfile().getProperties().removeAll("textures"); + playerEntity.getGameProfile().getProperties().put("textures", skin); + } + + private static boolean arePropertiesEquals(@NotNull JsonObject x, @NotNull GameProfile y) { + Property py = y.getProperties().get("textures").stream().findFirst().orElse(null); + if (py == null) + return false; + + try { + JsonObject jy = gson.fromJson(new String(Base64.getDecoder().decode(py.value()), StandardCharsets.UTF_8), JsonObject.class); + jy.remove("timestamp"); + return x.equals(jy); + } catch (Exception ex) { + LOGGER.info("Can not compare skin", ex); + return false; + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/command/SkinModule.java b/src/main/java/io/github/sakurawald/module/initializer/skin/command/SkinModule.java new file mode 100644 index 000000000..3d1ac8c18 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/command/SkinModule.java @@ -0,0 +1,100 @@ +package io.github.sakurawald.module.initializer.skin.command; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.skin.SkinRestorer; +import io.github.sakurawald.module.initializer.skin.enums.SkinVariant; +import io.github.sakurawald.module.initializer.skin.provider.MineSkinSkinProvider; +import io.github.sakurawald.module.initializer.skin.provider.MojangSkinProvider; +import io.github.sakurawald.util.MessageUtil; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.argument.GameProfileArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + + +public class SkinModule extends ModuleInitializer { + + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess commandBuildContext, CommandManager.RegistrationEnvironment commandSelection) { + dispatcher.register(literal("skin") + .then(literal("set") + .then(literal("mojang") + .then(argument("skin_name", StringArgumentType.word()) + .executes(context -> + skinAction(context.getSource(), + () -> MojangSkinProvider.getSkin(StringArgumentType.getString(context, "skin_name")))) + .then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(4)) + .executes(context -> + skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true, + () -> MojangSkinProvider.getSkin(StringArgumentType.getString(context, "skin_name"))))))) + .then(literal("web") + .then(literal("classic") + .then(argument("url", StringArgumentType.string()) + .executes(context -> + skinAction(context.getSource(), + () -> MineSkinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.CLASSIC))) + .then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(4)) + .executes(context -> + skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true, + () -> MineSkinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.CLASSIC)))))) + .then(literal("slim") + .then(argument("url", StringArgumentType.string()) + .executes(context -> + skinAction(context.getSource(), + () -> MineSkinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.SLIM))) + .then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(4)) + .executes(context -> + skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true, + () -> MineSkinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.SLIM)))))))) + .then(literal("clear") + .executes(context -> + skinAction(context.getSource(), + () -> SkinRestorer.getSkinStorage().getDefaultSkin())) + .then(argument("targets", GameProfileArgumentType.gameProfile()).executes(context -> + skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true, + () -> SkinRestorer.getSkinStorage().getDefaultSkin())))) + ); + } + + private int skinAction(ServerCommandSource src, Collection targets, boolean setByOperator, Supplier skinSupplier) { + SkinRestorer.setSkinAsync(src.getServer(), targets, skinSupplier).thenAccept(pair -> { + Collection profiles = pair.right(); + Collection players = pair.left(); + + if (profiles.isEmpty()) { + MessageUtil.sendMessage(src, "skin.action.failed"); + return; + } + if (setByOperator) { + MessageUtil.sendMessage(src, "skin.action.affected_profile", String.join(", ", profiles.stream().map(GameProfile::getName).toList())); + if (!players.isEmpty()) { + MessageUtil.sendMessage(src, "skin.action.affected_player", String.join(", ", players.stream().map(p -> p.getGameProfile().getName()).toList())); + } + } else { + MessageUtil.sendMessage(src, "skin.action.ok"); + } + }); + return targets.size(); + } + + private int skinAction(ServerCommandSource src, Supplier skinSupplier) { + if (src.getPlayer() == null) + return 0; + + return skinAction(src, Collections.singleton(src.getPlayer().getGameProfile()), false, skinSupplier); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/enums/SkinVariant.java b/src/main/java/io/github/sakurawald/module/initializer/skin/enums/SkinVariant.java new file mode 100644 index 000000000..17aff1ce1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/enums/SkinVariant.java @@ -0,0 +1,18 @@ +package io.github.sakurawald.module.initializer.skin.enums; + +public enum SkinVariant { + + CLASSIC("classic"), + SLIM("slim"); + + private final String name; + + SkinVariant(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinIO.java b/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinIO.java new file mode 100644 index 000000000..7ec27e1d9 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinIO.java @@ -0,0 +1,42 @@ +package io.github.sakurawald.module.initializer.skin.io; + +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.config.handler.ConfigHandler; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.UUID; + +import static io.github.sakurawald.Fuji.LOGGER; + +public class SkinIO { + + private static final String FILE_EXTENSION = ".json"; + + private final Path savePath; + + public SkinIO(Path savePath) { + this.savePath = savePath; + } + + public Property loadSkin(UUID uuid) { + File file = savePath.resolve(uuid + FILE_EXTENSION).toFile(); + try { + String string = org.apache.commons.io.FileUtils.readFileToString(file, StandardCharsets.UTF_8); + return ConfigHandler.getGson().fromJson(string, Property.class); + } catch (IOException e) { + LOGGER.error("Load skin failed: " + e.getMessage()); + } + return null; + } + + public void saveSkin(UUID uuid, Property skin) { + try { + org.apache.commons.io.FileUtils.writeStringToFile(new File(savePath.toFile(), uuid + FILE_EXTENSION), ConfigHandler.getGson().toJson(skin), StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.error("Save skin failed: " + e.getMessage()); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinStorage.java b/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinStorage.java new file mode 100644 index 000000000..6246afce2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/io/SkinStorage.java @@ -0,0 +1,56 @@ +package io.github.sakurawald.module.initializer.skin.io; + +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.config.Configs; +import lombok.Getter; + +import java.util.*; + +public class SkinStorage { + + private final Map skinMap = new HashMap<>(); + + @Getter + private final SkinIO skinIO; + + public SkinStorage(SkinIO skinIO) { + this.skinIO = skinIO; + } + + public Property getRandomSkin(UUID uuid) { + if (!skinMap.containsKey(uuid)) { + ArrayList defaultSkins = Configs.configHandler.model().modules.skin.random_skins; + Property skin = defaultSkins.get(new Random().nextInt(defaultSkins.size())); + setSkin(uuid, skin); + } + + return skinMap.get(uuid); + } + + public Property getDefaultSkin() { + return Configs.configHandler.model().modules.skin.default_skin; + } + + public Property getSkin(UUID uuid) { + if (!skinMap.containsKey(uuid)) { + Property skin = skinIO.loadSkin(uuid); + setSkin(uuid, skin); + } + + return skinMap.get(uuid); + } + + public void removeSkin(UUID uuid) { + if (skinMap.containsKey(uuid)) { + skinIO.saveSkin(uuid, skinMap.get(uuid)); + } + } + + public void setSkin(UUID uuid, Property skin) { + // if a player has no skin, use default skin. + if (skin == null) + skin = this.getDefaultSkin(); + + skinMap.put(uuid, skin); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MineSkinSkinProvider.java b/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MineSkinSkinProvider.java new file mode 100644 index 000000000..1785ff561 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MineSkinSkinProvider.java @@ -0,0 +1,29 @@ +package io.github.sakurawald.module.initializer.skin.provider; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.module.initializer.skin.enums.SkinVariant; +import io.github.sakurawald.util.HttpUtil; + +import java.io.IOException; +import java.net.URI; + +public class MineSkinSkinProvider { + + private static final String API_SERVER = "https://api.mineskin.org/generate/url"; + private static final String USER_AGENT = "SkinRestorer"; + private static final String TYPE = "application/json"; + + public static Property getSkin(String url, SkinVariant variant) { + try { + String param = ("{\"variant\":\"%s\",\"name\":\"%s\",\"visibility\":%d,\"url\":\"%s\"}") + .formatted(variant.toString(), "none", 1, url); + JsonObject texture = JsonParser.parseString(HttpUtil.post(URI.create(API_SERVER), param)).getAsJsonObject() + .getAsJsonObject("data").getAsJsonObject("texture"); + return new Property("textures", texture.get("value").getAsString(), texture.get("signature").getAsString()); + } catch (IOException e) { + return null; + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MojangSkinProvider.java b/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MojangSkinProvider.java new file mode 100644 index 000000000..bf00ca7ca --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/skin/provider/MojangSkinProvider.java @@ -0,0 +1,32 @@ +package io.github.sakurawald.module.initializer.skin.provider; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.util.HttpUtil; + +import java.io.IOException; +import java.net.URI; +import java.util.UUID; + +public class MojangSkinProvider { + + private static final String API_SERVER = "https://api.mojang.com/users/profiles/minecraft/"; + private static final String SESSION_SERVER = "https://sessionserver.mojang.com/session/minecraft/profile/"; + + public static Property getSkin(String name) { + try { + UUID uuid = getOnlineUUID(name); + JsonObject texture = JsonParser.parseString(HttpUtil.get(URI.create(SESSION_SERVER + uuid + "?unsigned=false"))).getAsJsonObject().getAsJsonArray("properties").get(0).getAsJsonObject(); + + return new Property("textures", texture.get("value").getAsString(), texture.get("signature").getAsString()); + } catch (Exception e) { + return null; + } + } + + private static UUID getOnlineUUID(String name) throws IOException { + return UUID.fromString(JsonParser.parseString(HttpUtil.get(URI.create(API_SERVER + name))).getAsJsonObject().get("id").getAsString() + .replaceFirst("(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5")); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/stonecutter/StoneCutterModule.java b/src/main/java/io/github/sakurawald/module/initializer/stonecutter/StoneCutterModule.java new file mode 100644 index 000000000..dd1c2c21d --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/stonecutter/StoneCutterModule.java @@ -0,0 +1,34 @@ +package io.github.sakurawald.module.initializer.stonecutter; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.screen.StonecutterScreenHandler; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class StoneCutterModule extends ModuleInitializer { + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("stonecutter").executes(this::$stonecutter)); + } + + private int $stonecutter(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> new StonecutterScreenHandler(i, inventory, ScreenHandlerContext.create(p.getWorld(), p.getBlockPos())) { + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + }, Text.translatable("container.stonecutter"))); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/suicide/SuicideModule.java b/src/main/java/io/github/sakurawald/module/initializer/suicide/SuicideModule.java new file mode 100644 index 000000000..7c1057311 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/suicide/SuicideModule.java @@ -0,0 +1,30 @@ +package io.github.sakurawald.module.initializer.suicide; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + + +public class SuicideModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("suicide").executes(this::$suicide)); + } + + @SuppressWarnings("SameReturnValue") + private int $suicide(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.kill(); + return Command.SINGLE_SUCCESS; + }); + } + + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/Position.java b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/Position.java new file mode 100644 index 000000000..5e4dfa611 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/Position.java @@ -0,0 +1,62 @@ +package io.github.sakurawald.module.initializer.teleport_warmup; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.util.MessageUtil; +import lombok.Data; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +@Data + +public class Position { + private String level; + private double x; + private double y; + private double z; + private float yaw; + private float pitch; + + public Position(String level, double x, double y, double z, float yaw, float pitch) { + this.level = level; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public Position(World level, double x, double y, double z, float yaw, float pitch) { + this(level.getRegistryKey().getValue().toString(), x, y, z, yaw, pitch); + } + + public static Position of(ServerPlayerEntity player) { + return new Position(player.getWorld().getRegistryKey().getValue().toString(), player.getX(), player.getY(), player.getZ(), player.getYaw(), player.getPitch()); + } + + public boolean sameLevel(World level) { + return this.level.equals(level.getRegistryKey().getValue().toString()); + } + + public double distanceToSqr(Position position) { + if (!this.level.equals(position.level)) return Double.MAX_VALUE; + double x = this.x - position.x; + double y = this.y - position.y; + double z = this.z - position.z; + return x * x + y * y + z * z; + } + + public void teleport(ServerPlayerEntity player) { + RegistryKey worldKey = RegistryKey.of(RegistryKeys.WORLD, new Identifier(this.level)); + ServerWorld serverLevel = Fuji.SERVER.getWorld(worldKey); + if (serverLevel == null) { + MessageUtil.sendMessage(player, "level.no_exists", this.level); + return; + } + + player.teleport(serverLevel, this.x, this.y, this.z, this.yaw, this.pitch); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/ServerPlayerAccessor.java b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/ServerPlayerAccessor.java new file mode 100644 index 000000000..923043e58 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/ServerPlayerAccessor.java @@ -0,0 +1,7 @@ +package io.github.sakurawald.module.initializer.teleport_warmup; + +public interface ServerPlayerAccessor { + + boolean fuji$inCombat(); + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportTicket.java b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportTicket.java new file mode 100644 index 000000000..a35859a98 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportTicket.java @@ -0,0 +1,24 @@ +package io.github.sakurawald.module.initializer.teleport_warmup; + +import io.github.sakurawald.util.MessageUtil; +import net.kyori.adventure.bossbar.BossBar; +import net.minecraft.server.network.ServerPlayerEntity; + +public class TeleportTicket { + + public ServerPlayerEntity player; + public Position source; + public Position destination; + public boolean ready; + public BossBar bossbar; + + public TeleportTicket(ServerPlayerEntity player, Position source, Position destination, boolean ready) { + this.player = player; + this.source = source; + this.destination = destination; + this.ready = ready; + this.bossbar = BossBar.bossBar(MessageUtil.ofComponent(player, "teleport_warmup.bossbar.name"), 0f, BossBar.Color.BLUE, BossBar.Overlay.PROGRESS); + bossbar.addViewer(player); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportWarmupModule.java b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportWarmupModule.java new file mode 100644 index 000000000..81b01a2d2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/teleport_warmup/TeleportWarmupModule.java @@ -0,0 +1,74 @@ +package io.github.sakurawald.module.initializer.teleport_warmup; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.MessageUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.kyori.adventure.bossbar.BossBar; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + + +public class TeleportWarmupModule extends ModuleInitializer { + + public final HashMap tickets = new HashMap<>(); + + + @Override + public void onInitialize() { + ServerTickEvents.START_SERVER_TICK.register(this::onServerTick); + } + + @SuppressWarnings("unused") + public void onServerTick(MinecraftServer server) { + if (tickets.isEmpty()) return; + + Iterator> iterator = tickets.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry pair = iterator.next(); + TeleportTicket ticket = pair.getValue(); + BossBar bossbar = ticket.bossbar; + + // fix: bossbar.progress() may be greater than 1.0F and throw an IllegalArgumentException. + final float MAX_VALUE = 20 * Configs.configHandler.model().modules.teleport_warmup.warmup_second; + final float DELFA_PERCENT = 1F / MAX_VALUE; + try { + bossbar.progress(Math.min(1f, bossbar.progress() + DELFA_PERCENT)); + } catch (Exception e) { + // fix: if the player is disconnected, the bossbar.progress() will be throw. + iterator.remove(); + return; + } + + ServerPlayerEntity player = ticket.player; + if (((ServerPlayerAccessor) player).fuji$inCombat()) { + bossbar.removeViewer(player); + iterator.remove(); + MessageUtil.sendActionBar(player, "teleport_warmup.in_combat"); + continue; + } + + final double INTERRUPT_DISTANCE = Configs.configHandler.model().modules.teleport_warmup.interrupt_distance; + if (player.getPos().squaredDistanceTo(ticket.source.getX(), ticket.source.getY(), ticket.source.getZ()) >= INTERRUPT_DISTANCE) { + bossbar.removeViewer(player); + iterator.remove(); + continue; + } + + // even the ServerPlayer is disconnected, the bossbar will still be ticked. + if (Float.compare(bossbar.progress(), 1f) == 0) { + bossbar.removeViewer(player); + + // don't change the order of the following two lines. + ticket.ready = true; + ticket.destination.teleport(player); + iterator.remove(); + } + } + + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/test/TestModule.java b/src/main/java/io/github/sakurawald/module/initializer/test/TestModule.java new file mode 100644 index 000000000..6c19f8000 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/test/TestModule.java @@ -0,0 +1,66 @@ +package io.github.sakurawald.module.initializer.test; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import lombok.SneakyThrows; +import net.kyori.adventure.text.Component; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.world.World; + + +public class TestModule extends ModuleInitializer { + + @SneakyThrows + private static int simulateLag(CommandContext ctx) { + Fuji.SERVER.getCommandManager().getDispatcher().execute("execute in minecraft:overworld run test fake-players", ctx.getSource()); + Fuji.SERVER.getCommandManager().getDispatcher().execute("execute in minecraft:overworld run time set midnight", ctx.getSource()); + Fuji.SERVER.getCommandManager().getDispatcher().execute("execute in minecraft:the_nether run test fake-players", ctx.getSource()); + Fuji.SERVER.getCommandManager().getDispatcher().execute("execute in minecraft:the_end run test fake-players", ctx.getSource()); + + return Command.SINGLE_SUCCESS; + } + + @SuppressWarnings({"ConstantValue", "ReassignedVariable", "PointlessArithmeticExpression", "DataFlowIssue"}) + @SneakyThrows + private static int fakePlayers(CommandContext ctx) { + int amount = 25; + int startIndex = 0; + if (ctx.getSource().getWorld().getRegistryKey() == World.OVERWORLD) startIndex = amount * 0; + if (ctx.getSource().getWorld().getRegistryKey() == World.NETHER) startIndex = amount * 1; + if (ctx.getSource().getWorld().getRegistryKey() == World.END) startIndex = amount * 2; + for (int i = 0; i < amount; i++) { + int distance = i * 100; + Fuji.SERVER.getCommandManager().getDispatcher().execute("player %d spawn at %d 96 %d".formatted(startIndex++, distance, distance), ctx.getSource()); + } + return Command.SINGLE_SUCCESS; + } + + private static int clearChat(CommandContext ctx) { + for (int i = 0; i < 50; i++) { + ctx.getSource().sendMessage(Component.empty()); + } + return Command.SINGLE_SUCCESS; + } + + private static int magic(CommandContext ctx) { + + return 1; + } + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("test").requires(s -> s.hasPermissionLevel(4)) + .then(CommandManager.literal("fake-players").executes(TestModule::fakePlayers)) + .then(CommandManager.literal("simulate-lag").executes(TestModule::simulateLag)) + .then(CommandManager.literal("clear-chat").executes(TestModule::clearChat)) + .then(CommandManager.literal("magic").executes(TestModule::magic)) + ); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/tick_chunk_cache/ITickableChunkSource.java b/src/main/java/io/github/sakurawald/module/initializer/tick_chunk_cache/ITickableChunkSource.java new file mode 100644 index 000000000..cf1f7bd21 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/tick_chunk_cache/ITickableChunkSource.java @@ -0,0 +1,10 @@ +package io.github.sakurawald.module.initializer.tick_chunk_cache; + + +import net.minecraft.server.world.ChunkHolder; + +public interface ITickableChunkSource { + + Iterable fuji$tickableChunksIterator(); + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/top_chunks/ChunkScore.java b/src/main/java/io/github/sakurawald/module/initializer/top_chunks/ChunkScore.java new file mode 100644 index 000000000..eecdfb106 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/top_chunks/ChunkScore.java @@ -0,0 +1,132 @@ +package io.github.sakurawald.module.initializer.top_chunks; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; + + +public class ChunkScore implements Comparable { + private final HashMap type2amount = new HashMap<>(); + private final HashMap type2transform_type = new HashMap<>() { + { + this.put("block.minecraft.mob_spawner", "block.minecraft.spawner"); + this.put("block.minecraft.brushable_block", "block.minecraft.suspicious_sand"); + this.put("block.minecraft.sign", "block.minecraft.oak_sign"); + this.put("block.minecraft.bed", "block.minecraft.white_bed"); + this.put("block.minecraft.skull", "block.minecraft.player_head"); + this.put("block.minecraft.banner", "block.minecraft.banner.base.white"); + } + }; + + @Getter + private final World dimension; + @Getter + private final ChunkPos chunkPos; + @Getter + private final ArrayList players = new ArrayList<>(); + @Getter + private int score; + + public ChunkScore(World dimension, ChunkPos chunkPos) { + this.dimension = dimension; + this.chunkPos = chunkPos; + } + + public void addEntity(Entity entity) { + String type = entity.getType().getTranslationKey(); + type = type2transform_type.getOrDefault(type, type); + + type2amount.putIfAbsent(type, 0); + type2amount.put(type, type2amount.get(type) + 1); + + if (entity instanceof ServerPlayerEntity player) { + this.players.add(player.getGameProfile().getName()); + } + } + + public void addBlockEntity(BlockEntity blockEntity) { + Identifier id = BlockEntityType.getId(blockEntity.getType()); + if (id == null) return; + + // fix: add the prefix of BlockEntity + String type = id.toTranslationKey("block"); + // fix: some block entity has an error translatable key, like mob_spawner + type = type2transform_type.getOrDefault(type, type); + type2amount.putIfAbsent(type, 0); + type2amount.put(type, type2amount.get(type) + 1); + } + + public void sumUpScore() { + this.score = 0; + for (String type : this.type2amount.keySet()) { + HashMap type2score = Configs.configHandler.model().modules.top_chunks.type2score; + this.score += type2score.getOrDefault(type, type2score.get("default")) * type2amount.get(type); + } + } + + @Override + public String toString() { + return String.format("%-5d", this.score); + } + + @Override + public int compareTo(@NotNull ChunkScore that) { + return Integer.compare(that.score, this.score); + } + + + private Component formatTypes(ServerCommandSource source) { + TextComponent.Builder ret = Component.text(); + this.type2amount.forEach((k, v) -> { + Component component = MessageUtil.ofComponent(source, "top_chunks.prop.types.entry", v) + .replaceText(TextReplacementConfig.builder().matchLiteral("[type]").replacement(Component.translatable(k)).build()); + ret.append(component); + }); + return ret.asComponent(); + } + + public Component asComponent(ServerCommandSource source) { + + String chunkLocation; + if (Configs.configHandler.model().modules.top_chunks.hide_location) { + chunkLocation = MessageUtil.ofString(source, "top_chunks.prop.hidden"); + if (source.hasPermissionLevel(4)) { + chunkLocation = MessageUtil.ofString(source, "top_chunks.prop.hidden.bypass", this.getChunkPos().toString()); + } + } else { + chunkLocation = this.getChunkPos().toString(); + } + + Component hoverTextComponent = Component.text().color(NamedTextColor.GOLD) + .append(MessageUtil.ofComponent(source, "top_chunks.prop.dimension", this.dimension.getRegistryKey().getValue())) + .append(Component.newline()) + .append(MessageUtil.ofComponent(source, "top_chunks.prop.chunk", chunkLocation)) + .append(Component.newline()) + .append(MessageUtil.ofComponent(source, "top_chunks.prop.score", this.score)) + .append(Component.newline()) + .append(MessageUtil.ofComponent(source, "top_chunks.prop.players", this.players)) + .append(Component.newline()) + .append(MessageUtil.ofComponent(source, "top_chunks.prop.types")) + .append(formatTypes(source)).build(); + return Component.text() + .color(this.players.isEmpty() ? NamedTextColor.GRAY : NamedTextColor.DARK_GREEN) + .append(Component.text(this.toString())).hoverEvent(HoverEvent.showText(hoverTextComponent)).build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/initializer/top_chunks/TopChunksModule.java b/src/main/java/io/github/sakurawald/module/initializer/top_chunks/TopChunksModule.java new file mode 100644 index 000000000..8c6b852e5 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/top_chunks/TopChunksModule.java @@ -0,0 +1,113 @@ +package io.github.sakurawald.module.initializer.top_chunks; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.config.model.ConfigModel; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.mixin.top_chunks.ThreadedAnvilChunkStorageMixin; +import io.github.sakurawald.util.MessageUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.WorldChunk; +import java.util.HashMap; +import java.util.PriorityQueue; +import java.util.concurrent.CompletableFuture; + + +public class TopChunksModule extends ModuleInitializer { + + + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("chunks").executes(this::$chunks) + ); + } + + private int $chunks(CommandContext ctx) { + CompletableFuture.runAsync(() -> { + PriorityQueue PQ = new PriorityQueue<>(); + /* iter worlds */ + MinecraftServer server = ctx.getSource().getServer(); + for (ServerWorld world : server.getWorlds()) { + HashMap chunkPos2ChunkScore = new HashMap<>(); + + /* entity in this world */ + for (Entity entity : world.iterateEntities()) { + ChunkPos pos = entity.getChunkPos(); + chunkPos2ChunkScore.putIfAbsent(pos, new ChunkScore(world, pos)); + chunkPos2ChunkScore.get(pos).addEntity(entity); + } + + /* block-entity in this world */ + ThreadedAnvilChunkStorageMixin threadedAnvilChunkStorage = (ThreadedAnvilChunkStorageMixin) world.getChunkManager().threadedAnvilChunkStorage; + Iterable chunkHolders = threadedAnvilChunkStorage.$getChunks(); + for (ChunkHolder chunkHolder : chunkHolders) { + WorldChunk worldChunk = chunkHolder.getWorldChunk(); + if (worldChunk == null) continue; + + /* count for block entities */ + for (BlockEntity blockEntity : worldChunk.getBlockEntities().values()) { + ChunkPos pos = worldChunk.getPos(); + chunkPos2ChunkScore.putIfAbsent(pos, new ChunkScore(world, pos)); + chunkPos2ChunkScore.get(pos).addBlockEntity(blockEntity); + } + } + + /* add all ChunkScore in this world */ + chunkPos2ChunkScore.values().forEach(chunkScore -> { + chunkScore.sumUpScore(); + PQ.add(chunkScore); + }); + } + + /* send output */ + ConfigModel.Modules.TopChunks topChunks = Configs.configHandler.model().modules.top_chunks; + calculateNearestPlayer(ctx.getSource(), PQ, topChunks.rows * topChunks.columns); + + TextComponent.Builder textComponentBuilder = Component.text(); + outer: + for (int j = 0; j < topChunks.rows; j++) { + for (int i = 0; i < topChunks.columns; i++) { + if (PQ.isEmpty()) break outer; + textComponentBuilder.append(PQ.poll().asComponent(ctx.getSource())).appendSpace(); + } + textComponentBuilder.append(Component.newline()); + } + + ctx.getSource().sendMessage(textComponentBuilder.asComponent()); + }); + + return Command.SINGLE_SUCCESS; + } + + private void calculateNearestPlayer(ServerCommandSource source, PriorityQueue PQ, int limit) { + int count = 0; + for (ChunkScore chunkScore : PQ) { + if (count++ >= limit) break; + + World world = chunkScore.getDimension(); + ChunkPos chunkPos = chunkScore.getChunkPos(); + BlockPos blockPos = chunkPos.getStartPos(); + PlayerEntity nearestPlayer = world.getClosestPlayer(blockPos.getX(), blockPos.getY(), blockPos.getZ(), Configs.configHandler.model().modules.top_chunks.nearest_distance, false); + if (nearestPlayer != null) { + chunkScore.getPlayers().add(MessageUtil.ofString(source, "top_chunks.prop.players.nearest", nearestPlayer.getGameProfile().getName())); + } + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaModule.java b/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaModule.java new file mode 100644 index 000000000..39f740d93 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaModule.java @@ -0,0 +1,155 @@ +package io.github.sakurawald.module.initializer.tpa; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.chat.mention.MentionPlayersJob; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import java.util.ArrayList; +import java.util.Optional; + +import static net.minecraft.server.command.CommandManager.argument; + +@SuppressWarnings("LombokGetterMayBeUsed") + +public class TpaModule extends ModuleInitializer { + + @Getter + private final ArrayList requests = new ArrayList<>(); + + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register( + CommandManager.literal("tpa").then(argument("player", EntityArgumentType.player()).executes(this::$tpa)) + ); + dispatcher.register( + CommandManager.literal("tpahere").then(argument("player", EntityArgumentType.player()).executes(this::$tpahere)) + ); + dispatcher.register( + CommandManager.literal("tpaaccept").then(argument("player", EntityArgumentType.player()).executes(this::$tpaaccept)) + ); + dispatcher.register( + CommandManager.literal("tpadeny").then(argument("player", EntityArgumentType.player()).executes(this::$tpadeny)) + ); + dispatcher.register( + CommandManager.literal("tpacancel").then(argument("player", EntityArgumentType.player()).executes(this::$tpacancel)) + ); + } + + private int $tpa(CommandContext ctx) { + return doRequest(ctx, false); + } + + private int $tpahere(CommandContext ctx) { + return doRequest(ctx, true); + } + + private int $tpaaccept(CommandContext ctx) { + return doResponse(ctx, ResponseStatus.ACCEPT); + } + + private int $tpadeny(CommandContext ctx) { + return doResponse(ctx, ResponseStatus.DENY); + } + + private int $tpacancel(CommandContext ctx) { + return doResponse(ctx, ResponseStatus.CANCEL); + } + + @SuppressWarnings("SameReturnValue") + private int doResponse(CommandContext ctx, ResponseStatus status) { + return CommandUtil.playerOnlyCommand(ctx, source -> { + ServerPlayerEntity target; + try { + target = EntityArgumentType.getPlayer(ctx, "player"); + } catch (CommandSyntaxException e) { + MessageUtil.sendActionBar(source, "tpa.player_not_found"); + return Command.SINGLE_SUCCESS; + } + + /* resolve relative request */ + Optional requestOptional = requests.stream() + .filter(request -> + status == ResponseStatus.CANCEL ? + (request.getSender().equals(source) && request.getReceiver().equals(target)) + : (request.getSender().equals(target) && request.getReceiver().equals(source))) + .findFirst(); + if (requestOptional.isEmpty()) { + MessageUtil.sendActionBar(source, "tpa.no_relative_ticket"); + return Command.SINGLE_SUCCESS; + } + + TpaRequest request = requestOptional.get(); + if (status == ResponseStatus.ACCEPT) { + request.getSender().sendActionBar(request.asSenderComponent$Accepted()); + request.getReceiver().sendMessage(request.asReceiverComponent$Accepted()); + + ServerPlayerEntity who = request.getTeleportWho(); + ServerPlayerEntity to = request.getTeleportTo(); + MentionPlayersJob.scheduleJob(request.isTpahere() ? to : who); + who.teleport((ServerWorld) to.getWorld(), to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); + } else if (status == ResponseStatus.DENY) { + request.getSender().sendActionBar(request.asSenderComponent$Denied()); + request.getReceiver().sendMessage(request.asReceiverComponent$Denied()); + } else if (status == ResponseStatus.CANCEL) { + request.getSender().sendMessage(request.asSenderComponent$Cancelled()); + request.getReceiver().sendMessage(request.asReceiverComponent$Cancelled()); + } + + request.cancelTimeout(); + requests.remove(request); + return Command.SINGLE_SUCCESS; + }); + } + + @SuppressWarnings("SameReturnValue") + private int doRequest(CommandContext ctx, boolean tpahere) { + return CommandUtil.playerOnlyCommand(ctx, source -> { + ServerPlayerEntity target; + try { + target = EntityArgumentType.getPlayer(ctx, "player"); + } catch (CommandSyntaxException e) { + MessageUtil.sendActionBar(source, "tpa.player_not_found"); + return Command.SINGLE_SUCCESS; + } + + /* add request */ + TpaRequest request = new TpaRequest(source, target, tpahere); + + /* has similar request ? */ + if (request.getSender().equals(request.getReceiver())) { + MessageUtil.sendActionBar(request.getSender(), "tpa.request_to_self"); + + return Command.SINGLE_SUCCESS; + } + + if (requests.stream().anyMatch(request::similarTo)) { + MessageUtil.sendActionBar(request.getSender(), "tpa.similar_request_exists"); + return Command.SINGLE_SUCCESS; + } + + requests.add(request); + request.startTimeout(); + + /* feedback */ + request.getReceiver().sendMessage(request.asReceiverComponent$Sent()); + MentionPlayersJob.scheduleJob(request.getReceiver()); + request.getSender().sendMessage(request.asSenderComponent$Sent()); + return Command.SINGLE_SUCCESS; + }); + } + + private enum ResponseStatus {ACCEPT, DENY, CANCEL} +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaRequest.java b/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaRequest.java new file mode 100644 index 000000000..f66cba761 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/tpa/TpaRequest.java @@ -0,0 +1,131 @@ +package io.github.sakurawald.module.initializer.tpa; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.util.MessageUtil; +import lombok.Getter; +import lombok.ToString; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.server.network.ServerPlayerEntity; +import java.util.Timer; +import java.util.TimerTask; + +@ToString +public class TpaRequest { + + private static final String CIRCLE = "●"; + private static final String TICK = "[✔]"; + private static final String CROSS = "[❌]"; + private static final TpaModule module = ModuleManager.getInitializer(TpaModule.class); + @Getter + private final ServerPlayerEntity sender; + @Getter + private final ServerPlayerEntity receiver; + @Getter + private final boolean tpahere; + private Timer timer; + + public TpaRequest(ServerPlayerEntity sender, ServerPlayerEntity receiver, boolean tpahere) { + this.sender = sender; + this.receiver = receiver; + this.tpahere = tpahere; + } + + boolean similarTo(TpaRequest other) { + return (this.sender.equals(other.sender) && this.receiver.equals(other.receiver)) || + (this.sender.equals(other.receiver) && this.receiver.equals(other.sender)); + } + + public ServerPlayerEntity getTeleportWho() { + return tpahere ? getReceiver() : getSender(); + } + + public ServerPlayerEntity getTeleportTo() { + return tpahere ? getSender() : getReceiver(); + } + + public void startTimeout() { + var that = this; + timer = new Timer(); + timer.schedule( + new TimerTask() { + @Override + public void run() { + getSender().sendMessage(asSenderComponent$Cancelled()); + getReceiver().sendMessage(asReceiverComponent$Cancelled()); + // don't forget to remove this request + module.getRequests().remove(that); + } + }, + Configs.configHandler.model().modules.tpa.timeout * 1000L + ); + } + + public void cancelTimeout() { + timer.cancel(); + } + + public Component asSenderComponent$Description() { + return tpahere ? MessageUtil.ofComponent(getSender(), "tpa.others_to_you", receiver.getGameProfile().getName()) + : MessageUtil.ofComponent(getSender(), "tpa.you_to_others", receiver.getGameProfile().getName()); + } + + public Component asSenderComponent$Sent() { + TextComponent cancelComponent = Component + .text(CROSS).color(NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(MessageUtil.ofComponent(getSender(), "cancel"))) + .clickEvent(ClickEvent.runCommand("/tpacancel %s".formatted(getReceiver().getGameProfile().getName()))); + + return asSenderComponent$Description() + .appendSpace() + .append(cancelComponent); + } + + public Component asReceiverComponent$Description() { + return tpahere ? MessageUtil.ofComponent(getReceiver(), "tpa.you_to_others", sender.getGameProfile().getName()) + : MessageUtil.ofComponent(getReceiver(), "tpa.others_to_you", sender.getGameProfile().getName()); + } + + public Component asReceiverComponent$Sent() { + Component acceptComponent = Component.text(TICK).color(NamedTextColor.GREEN) + .hoverEvent(HoverEvent.showText(MessageUtil.ofComponent(getReceiver(), "accept"))) + .clickEvent(ClickEvent.runCommand("/tpaaccept %s".formatted(sender.getGameProfile().getName()))); + Component denyComponent = Component.text(CROSS).color(NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(MessageUtil.ofComponent(getReceiver(), "deny"))) + .clickEvent(ClickEvent.runCommand("/tpadeny %s".formatted(sender.getGameProfile().getName()))); + return asReceiverComponent$Description() + .appendSpace() + .append(acceptComponent) + .appendSpace() + .append(denyComponent); + } + + public Component asSenderComponent$Accepted() { + return asSenderComponent$Description().appendSpace().append(Component.text(CIRCLE, NamedTextColor.GREEN)); + } + + public Component asReceiverComponent$Accepted() { + return asReceiverComponent$Description().appendSpace().append(Component.text(CIRCLE, NamedTextColor.GREEN)); + } + + public Component asSenderComponent$Denied() { + return asSenderComponent$Description().appendSpace().append(Component.text(CIRCLE, NamedTextColor.RED)); + } + + public Component asReceiverComponent$Denied() { + return asReceiverComponent$Description().appendSpace().append(Component.text(CIRCLE, NamedTextColor.RED)); + } + + public Component asSenderComponent$Cancelled() { + return asSenderComponent$Description().decoration(TextDecoration.STRIKETHROUGH, true); + } + + public Component asReceiverComponent$Cancelled() { + return asReceiverComponent$Description().decoration(TextDecoration.STRIKETHROUGH, true); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/workbench/WorkbenchModule.java b/src/main/java/io/github/sakurawald/module/initializer/workbench/WorkbenchModule.java new file mode 100644 index 000000000..07a6d6a23 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/workbench/WorkbenchModule.java @@ -0,0 +1,36 @@ +package io.github.sakurawald.module.initializer.workbench; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.util.CommandUtil; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.CraftingScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.stat.Stats; +import net.minecraft.text.Text; + +public class WorkbenchModule extends ModuleInitializer { + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("workbench").executes(this::$workbench)); + } + + private int $workbench(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + player.openHandledScreen(new SimpleNamedScreenHandlerFactory((i, inventory, p) -> new CraftingScreenHandler(i, inventory, ScreenHandlerContext.create(p.getWorld(), p.getBlockPos())) { + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + }, Text.translatable("container.crafting"))); + player.incrementStat(Stats.INTERACT_WITH_CRAFTING_TABLE); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/ScheduleMethod.java b/src/main/java/io/github/sakurawald/module/initializer/works/ScheduleMethod.java new file mode 100644 index 000000000..cfe63fb14 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/ScheduleMethod.java @@ -0,0 +1,5 @@ +package io.github.sakurawald.module.initializer.works; + +public interface ScheduleMethod { + void onSchedule(); +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/WorksCache.java b/src/main/java/io/github/sakurawald/module/initializer/works/WorksCache.java new file mode 100644 index 000000000..9018a35fe --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/WorksCache.java @@ -0,0 +1,44 @@ +package io.github.sakurawald.module.initializer.works; + +import io.github.sakurawald.module.initializer.works.work_type.Work; +import lombok.Getter; +import net.minecraft.util.math.BlockPos; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +public class WorksCache { + @Getter + private static final ConcurrentHashMap> blockpos2works = new ConcurrentHashMap<>(); + @Getter + private static final ConcurrentHashMap> entity2works = new ConcurrentHashMap<>(); + + public static void bind(BlockPos blockPos, Work work) { + blockpos2works.computeIfAbsent(blockPos, k -> new HashSet<>()).add(work); + } + + public static void bind(Integer entityID, Work work) { + entity2works.computeIfAbsent(entityID, k -> new HashSet<>()).add(work); + } + + public static void unbind(Work work) { + Iterator>> iter1 = blockpos2works.entrySet().iterator(); + while (iter1.hasNext()) { + Map.Entry> entry = iter1.next(); + entry.getValue().remove(work); + if (entry.getValue().isEmpty()) { + iter1.remove(); + } + } + Iterator>> iter2 = entity2works.entrySet().iterator(); + while (iter2.hasNext()) { + Map.Entry> entry = iter2.next(); + entry.getValue().remove(work); + if (entry.getValue().isEmpty()) { + iter2.remove(); + } + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/WorksModule.java b/src/main/java/io/github/sakurawald/module/initializer/works/WorksModule.java new file mode 100644 index 000000000..c1f87d94f --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/WorksModule.java @@ -0,0 +1,283 @@ +package io.github.sakurawald.module.initializer.works; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.initializer.works.gui.InputSignGui; +import io.github.sakurawald.module.initializer.works.work_type.NonProductionWork; +import io.github.sakurawald.module.initializer.works.work_type.ProductionWork; +import io.github.sakurawald.module.initializer.works.work_type.Work; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.GuiUtil; +import io.github.sakurawald.util.MessageUtil; +import io.github.sakurawald.util.ScheduleUtil; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.item.Items; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; + +import java.util.HashSet; +import java.util.List; + +@SuppressWarnings("SameReturnValue") + +public class WorksModule extends ModuleInitializer { + + private final int PAGE_SIZE = 9 * 5; + + + @Override + public void onInitialize() { + Configs.worksHandler.loadFromDisk(); + ServerLifecycleEvents.SERVER_STARTED.register(this::registerScheduleTask); + } + + @Override + public void onReload() { + Configs.worksHandler.loadFromDisk(); + } + + @SuppressWarnings("unused") + public void registerScheduleTask(MinecraftServer server) { + ScheduleUtil.addJob(WorksScheduleJob.class, null, null, ScheduleUtil.CRON_EVERY_MINUTE, new JobDataMap() { + { + this.put(MinecraftServer.class.getName(), server); + } + }); + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("works").executes(this::$works)); + } + + private void $addWork(ServerPlayerEntity player) { + new InputSignGui(player, MessageUtil.ofString(player, "works.work.add.prompt.input.name")) { + @Override + public void onClose() { + /* input name */ + String name = this.getLine(0).getString().trim(); + if (name.isBlank()) { + MessageUtil.sendActionBar(player, "works.work.add.empty_name"); + return; + } + + /* input type */ + SimpleGui selectWorkTypeGui = new SimpleGui(ScreenHandlerType.GENERIC_9X3, player, false); + selectWorkTypeGui.setLockPlayerInventory(true); + selectWorkTypeGui.setTitle(MessageUtil.ofVomponent(player, "works.work.add.select_work_type.title")); + for (int i = 0; i < 27; i++) { + selectWorkTypeGui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + selectWorkTypeGui.setSlot(11, new GuiElementBuilder().setItem(Items.GUNPOWDER).setName(MessageUtil.ofVomponent(player, "works.non_production_work.name")).setCallback(() -> { + // add + Configs.worksHandler.model().works.add(0, new NonProductionWork(player, name)); + MessageUtil.sendActionBar(player, "works.work.add.done"); + MessageUtil.sendBroadcast("works.work.add.broadcast", player.getGameProfile().getName(), name); + selectWorkTypeGui.close(); + })); + selectWorkTypeGui.setSlot(15, new GuiElementBuilder().setItem(Items.REDSTONE).setName(MessageUtil.ofVomponent(player, "works.production_work.name")).setCallback(() -> { + // add + ProductionWork work = new ProductionWork(player, name); + Configs.worksHandler.model().works.add(0, work); + MessageUtil.sendActionBar(player, "works.work.add.done"); + MessageUtil.sendBroadcast("works.work.add.broadcast", player.getGameProfile().getName(), name); + selectWorkTypeGui.close(); + + // input sample distance + work.openInputSampleDistanceGui(player); + })); + selectWorkTypeGui.open(); + + } + }.open(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean hasPermission(ServerPlayerEntity player, Work work) { + return player.getGameProfile().getName().equals(work.creator) || player.hasPermissionLevel(4); + } + + private void $listWorks(ServerPlayerEntity player, @Nullable List source, int page) { + if (source == null) { + source = Configs.worksHandler.model().works; + } + + final SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X6, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(MessageUtil.ofVomponent(player, "works.list.title", page + 1)); + + /* draw content */ + for (int slotIndex = 0; slotIndex < PAGE_SIZE; slotIndex++) { + int workIndex = PAGE_SIZE * page + slotIndex; + if (workIndex < 0 || workIndex >= source.size()) break; + Work work = source.get(workIndex); + gui.setSlot(slotIndex, new GuiElementBuilder() + .setItem(work.asItem()) + .setName(MessageUtil.ofVomponent(work.name)) + .setLore(work.asLore(player)) + .setCallback((index, clickType, actionType) -> { + /* left click -> visit */ + if (clickType.isLeft) { + RegistryKey worldKey = RegistryKey.of(RegistryKeys.WORLD, new Identifier(work.level)); + ServerWorld level = Fuji.SERVER.getWorld(worldKey); + //noinspection DataFlowIssue + player.teleport(level, work.x, work.y, work.z, work.yaw, work.pitch); + gui.close(); + return; + } + /* shift + right click -> specialized settings */ + if (clickType.isRight && clickType.shift) { + if (!hasPermission(player, work)) { + MessageUtil.sendActionBar(player, "works.work.set.no_perm"); + return; + } + work.openSpecializedSettingsGui(player, gui); + gui.close(); + return; + } + /* right click -> general settings */ + if (clickType.isRight) { + // check permission + if (!hasPermission(player, work)) { + MessageUtil.sendActionBar(player, "works.work.set.no_perm"); + return; + } + work.openGeneralSettingsGui(player, gui); + gui.close(); + } + })); + } + + /* draw navigator */ + for (int i = 45; i < 54; i++) { + gui.setSlot(i, new GuiElementBuilder().setItem(Items.PINK_STAINED_GLASS_PANE)); + } + List finalSource = source; + gui.setSlot(45, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "previous_page")) + .setSkullOwner(GuiUtil.PREVIOUS_PAGE_ICON) + .setCallback(() -> { + if (page == 0) return; + $listWorks(player, finalSource, page - 1); + })); + gui.setSlot(48, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "works.list.add")) + .setSkullOwner(GuiUtil.PLUS_ICON) + .setCallback(() -> $addWork(player)) + ); + if (source == Configs.worksHandler.model().works) { + gui.setSlot(49, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "works.list.my_works")) + .setSkullOwner(GuiUtil.HEART_ICON) + .setCallback(() -> $myWorks(player)) + ); + } else { + gui.setSlot(49, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "works.list.all_works")) + .setSkullOwner(GuiUtil.A_ICON) + .setCallback(() -> $listWorks(player, null, 0)) + ); + } + gui.setSlot(50, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "works.list.help")) + .setSkullOwner(GuiUtil.QUESTION_MARK_ICON) + .setLore(MessageUtil.ofVomponents(player, "works.list.help.lore"))); + gui.setSlot(52, new GuiElementBuilder() + .setItem(Items.COMPASS) + .setName(MessageUtil.ofVomponent(player, "search")) + .setCallback(() -> $searchWorks(player, finalSource)) + ); + gui.setSlot(53, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setName(MessageUtil.ofVomponent(player, "next_page")) + .setSkullOwner(GuiUtil.NEXT_PAGE_ICON) + .setCallback(() -> { + if ((page + 1) * PAGE_SIZE >= finalSource.size()) return; + $listWorks(player, finalSource, page + 1); + }) + ); + gui.open(); + } + + private void $searchWorks(ServerPlayerEntity player, List source) { + /* input keywords */ + new InputSignGui(player, null) { + @Override + public void onClose() { + List filterWorks = null; + String key = combineAllLinesReturnNull(); + if (key != null) { + filterWorks = source.stream().filter(w -> + w.creator.contains(key) + || w.name.contains(key) + || (w.introduction != null && w.introduction.contains(key)) + || w.level.contains(key) + || w.getIcon().contains(key) + || (w instanceof ProductionWork pw && pw.sample.sampleCounter != null && pw.sample.sampleCounter.keySet().stream().anyMatch(k -> k.contains(key))) + ).toList(); + } + $listWorks(player, filterWorks, 0); + } + }.open(); + + } + + private void $myWorks(ServerPlayerEntity player) { + List works = Configs.worksHandler.model().works; + List myWorks = works.stream().filter(w -> w.creator.equals(player.getGameProfile().getName())).toList(); + $listWorks(player, myWorks, 0); + } + + private int $works(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + $listWorks(player, null, 0); + return Command.SINGLE_SUCCESS; + }); + } + + public static class WorksScheduleJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + // save current works data + MinecraftServer server = (MinecraftServer) context.getJobDetail().getJobDataMap().get(MinecraftServer.class.getName()); + if (server.isRunning()) { + Configs.worksHandler.saveToDisk(); + } + + // run schedule method + HashSet works = new HashSet<>(); + WorksCache.getBlockpos2works().values().forEach(works::addAll); + WorksCache.getEntity2works().values().forEach(works::addAll); + works.forEach(work -> { + if (work instanceof ScheduleMethod sm) sm.onSchedule(); + }); + } + } +} + diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/gui/ConfirmGui.java b/src/main/java/io/github/sakurawald/module/initializer/works/gui/ConfirmGui.java new file mode 100644 index 000000000..d2aa09f67 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/gui/ConfirmGui.java @@ -0,0 +1,21 @@ +package io.github.sakurawald.module.initializer.works.gui; + +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.server.network.ServerPlayerEntity; + +public abstract class ConfirmGui extends InputSignGui { + public ConfirmGui(ServerPlayerEntity player) { + super(player, MessageUtil.ofString(player, "prompt.input.confirm")); + } + + @Override + public void onClose() { + if (!this.getLine(0).getString().equals("confirm")) { + MessageUtil.sendActionBar(player, "operation.cancelled"); + return; + } + onConfirm(); + } + + public abstract void onConfirm(); +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/gui/InputSignGui.java b/src/main/java/io/github/sakurawald/module/initializer/works/gui/InputSignGui.java new file mode 100644 index 000000000..d307165bf --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/gui/InputSignGui.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.initializer.works.gui; + +import eu.pb4.sgui.api.gui.SignGui; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.block.Blocks; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.DyeColor; + +public class InputSignGui extends SignGui { + + public InputSignGui(ServerPlayerEntity player, String promptKey) { + super(player); + this.setSignType(Blocks.CHERRY_WALL_SIGN); + this.setColor(DyeColor.BLACK); + if (promptKey != null) { + this.setLine(3, MessageUtil.ofVomponent(promptKey)); + } + this.setAutoUpdate(false); + } + + public String combineAllLines() { + StringBuilder sb = new StringBuilder(); + sb.delete(0, sb.length()); + for (int i = 0; i < 4; i++) { + sb.append(this.getLine(i).getString().trim()); + } + return sb.toString().trim(); + } + + public String combineAllLinesReturnNull() { + String lines = combineAllLines(); + if (lines.isBlank()) return null; + return lines; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/work_type/NonProductionWork.java b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/NonProductionWork.java new file mode 100644 index 000000000..2e744a365 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/NonProductionWork.java @@ -0,0 +1,28 @@ +package io.github.sakurawald.module.initializer.works.work_type; + +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.util.MessageUtil; +import lombok.NoArgsConstructor; +import net.minecraft.server.network.ServerPlayerEntity; + +@NoArgsConstructor +public class NonProductionWork extends Work { + public NonProductionWork(ServerPlayerEntity player, String name) { + super(player, name); + } + + @Override + protected String getType() { + return WorkTypeAdapter.WorkType.NonProductionWork.name(); + } + + @Override + protected String getDefaultIcon() { + return "minecraft:gunpowder"; + } + + @Override + public void openSpecializedSettingsGui(ServerPlayerEntity player, SimpleGui parentGui) { + MessageUtil.sendActionBar(player, "works.non_production_work.specialized_settings.not_found"); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/work_type/ProductionWork.java b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/ProductionWork.java new file mode 100644 index 000000000..61ebf639c --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/ProductionWork.java @@ -0,0 +1,285 @@ +package io.github.sakurawald.module.initializer.works.work_type; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.works.ScheduleMethod; +import io.github.sakurawald.module.initializer.works.WorksCache; +import io.github.sakurawald.module.initializer.works.gui.ConfirmGui; +import io.github.sakurawald.module.initializer.works.gui.InputSignGui; +import io.github.sakurawald.module.mixin.top_chunks.ThreadedAnvilChunkStorageMixin; +import io.github.sakurawald.util.DateUtil; +import io.github.sakurawald.util.GuiUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.NoArgsConstructor; +import net.kyori.adventure.text.TextReplacementConfig; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.HopperBlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.vehicle.HopperMinecartEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.chunk.WorldChunk; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +@NoArgsConstructor + +public class ProductionWork extends Work implements ScheduleMethod { + + public @NotNull Sample sample = new Sample(); + + public ProductionWork(ServerPlayerEntity player, String name) { + super(player, name); + } + + @Override + protected String getType() { + return WorkTypeAdapter.WorkType.ProductionWork.name(); + } + + private List formatSampleCounter(ServerPlayerEntity player) { + List ret = new ArrayList<>(); + long currentTimeMS = System.currentTimeMillis(); + + Stream> sortedStream = this.sample.sampleCounter.entrySet().stream().sorted((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())); + + sortedStream.forEach(entry -> { + String key = entry.getKey(); + double rate = entry.getValue() * ((double) (3600 * 1000) / ((Math.min(this.sample.sampleEndTimeMS, currentTimeMS)) - this.sample.sampleStartTimeMS)); + net.kyori.adventure.text.Component component = MessageUtil.ofComponent(player, "works.production_work.prop.sample_counter.entry", entry.getValue(), rate) + .replaceText(TextReplacementConfig.builder().matchLiteral("[item]").replacement(Text.translatable(key)).build()); + ret.add(MessageUtil.toVomponent(component)); + }); + + if (ret.isEmpty()) { + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_counter.empty")); + } + return ret; + } + + @Override + public List asLore(ServerPlayerEntity player) { + /* construct lore */ + List ret = super.asLore(player); + // note: hide sample info in lore if sample not exists + if (this.sample.sampleStartTimeMS == 0) { + ret.addAll((MessageUtil.ofVomponents(player, "works.production_work.sample.not_exists"))); + return ret; + } + + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_start_time", DateUtil.toStandardDateFormat(this.sample.sampleStartTimeMS))); + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_end_time", DateUtil.toStandardDateFormat(this.sample.sampleEndTimeMS))); + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_dimension", this.sample.sampleDimension)); + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_coordinate", this.sample.sampleX, this.sample.sampleY, this.sample.sampleZ)); + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_distance", this.sample.sampleDistance)); + + // check npe to avoid broken + if (this.sample.sampleCounter != null) { + // trim counter + if (this.sample.sampleCounter.size() > Configs.configHandler.model().modules.works.sample_counter_top_n) { + trimCounter(); + } + ret.add(MessageUtil.ofVomponent(player, "works.production_work.prop.sample_counter")); + ret.addAll(formatSampleCounter(player)); + } + return ret; + } + + @Override + protected String getDefaultIcon() { + return "minecraft:redstone"; + } + + public void openInputSampleDistanceGui(ServerPlayerEntity player) { + new InputSignGui(player, MessageUtil.ofString(player, "works.production_work.prompt.input.sample_distance")) { + @Override + public void onClose() { + int limit = Configs.configHandler.model().modules.works.sample_distance_limit; + int current; + try { + current = Integer.parseInt(this.getLine(0).getString()); + } catch (NumberFormatException e) { + MessageUtil.sendActionBar(player, "input.syntax.error"); + return; + } + + if (current > limit) { + MessageUtil.sendActionBar(player, "input.limit.error"); + return; + } + + // set sample distance + sample.sampleDistance = current; + + // start/restart sample + if (isSampling()) { + endSample(); + } + startSample(player); + } + }.open(); + } + + @Override + public void openSpecializedSettingsGui(ServerPlayerEntity player, SimpleGui parentGui) { + final SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X1, player, false); + gui.setTitle(MessageUtil.ofVomponent(player, "works.work.set.specialized_settings.title")); + gui.setLockPlayerInventory(true); + gui.addSlot(new GuiElementBuilder() + .setItem(Items.CLOCK) + .setName(MessageUtil.ofVomponent(player, "works.production_work.set.sample")) + .setLore(MessageUtil.ofVomponents(player, "works.production_work.set.sample.lore")) + .setCallback(() -> new ConfirmGui(player) { + @Override + public void onConfirm() { + openInputSampleDistanceGui(player); + } + }.open() + ) + ); + gui.setSlot(8, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setSkullOwner(GuiUtil.PREVIOUS_PAGE_ICON) + .setName(MessageUtil.ofVomponent(player, "back")) + .setCallback(parentGui::open) + ); + + gui.open(); + } + + public boolean isSampling() { + return System.currentTimeMillis() < this.sample.sampleEndTimeMS; + } + + @Override + public Item asItem() { + return super.asItem(); + } + + private boolean insideSampleDistance(BlockPos position, BlockPos blockPos) { + float deltaX = Math.abs(blockPos.getX() - position.getX()); + float deltaZ = Math.abs(blockPos.getZ() - position.getZ()); + return deltaX <= this.sample.sampleDistance && deltaZ <= this.sample.sampleDistance; + } + + @SuppressWarnings("unused") + private String formatBlockPosList(ArrayList blockPosList) { + StringBuilder sb = new StringBuilder(); + for (BlockPos blockPos : blockPosList) { + sb.append("(").append(blockPos.getX()).append(",").append(blockPos.getY()).append(",").append(blockPos.getZ()).append(")").append(" "); + } + return sb.toString(); + } + + public int resolveHoppers(ServerPlayerEntity player) { + // clear cache entry + WorksCache.unbind(this); + + // add cache entry + int hopperBlockCount = 0; + int minecartHopperCount = 0; + ServerWorld world = player.getServerWorld(); + ThreadedAnvilChunkStorageMixin threadedAnvilChunkStorage = (ThreadedAnvilChunkStorageMixin) world.getChunkManager().threadedAnvilChunkStorage; + Iterable chunkHolders = threadedAnvilChunkStorage.$getChunks(); + for (ChunkHolder chunkHolder : chunkHolders) { + WorldChunk worldChunk = chunkHolder.getWorldChunk(); + if (worldChunk == null) continue; + /* count for block entities */ + for (BlockEntity blockEntity : worldChunk.getBlockEntities().values()) { + // improve: check type first for performance + if (blockEntity instanceof HopperBlockEntity) { + if (insideSampleDistance(player.getBlockPos(), blockEntity.getPos())) { + WorksCache.bind(blockEntity.getPos(), this); + hopperBlockCount++; + } + } + } + } + for (Entity entity : world.iterateEntities()) { + if (entity instanceof HopperMinecartEntity) { + if (insideSampleDistance(player.getBlockPos(), entity.getBlockPos())) { + WorksCache.bind(entity.getId(), this); + minecartHopperCount++; + } + } + } + + MessageUtil.sendMessage(player, "works.production_work.sample.resolve_hoppers.response", hopperBlockCount, minecartHopperCount); + return hopperBlockCount + minecartHopperCount; + } + + @Override + public void onSchedule() { + if (System.currentTimeMillis() >= this.sample.sampleEndTimeMS) { + this.endSample(); + } + } + + public void startSample(ServerPlayerEntity player) { + this.sample.sampleStartTimeMS = System.currentTimeMillis(); + this.sample.sampleEndTimeMS = this.sample.sampleStartTimeMS + Configs.configHandler.model().modules.works.sample_time_ms; + this.sample.sampleDimension = player.getServerWorld().getRegistryKey().getValue().toString(); + this.sample.sampleX = player.getX(); + this.sample.sampleY = player.getY(); + this.sample.sampleZ = player.getZ(); + this.sample.sampleCounter = new HashMap<>(); + if (this.resolveHoppers(player) == 0) { + MessageUtil.sendMessage(player, "operation.cancelled"); + return; + } + + MessageUtil.sendBroadcast("works.production_work.sample.start", name, this.creator); + } + + public void endSample() { + // unbind all block pos + WorksCache.unbind(this); + MessageUtil.sendBroadcast("works.production_work.sample.end", this.name, this.creator); + + // trim counter to avoid spam + trimCounter(); + } + + public void trimCounter() { + List> sortedEntries = this.sample.sampleCounter.entrySet() + .stream() + .sorted((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())) + .toList(); + + int N = Configs.configHandler.model().modules.works.sample_counter_top_n; + this.sample.sampleCounter.clear(); + for (int i = 0; i < N && i < sortedEntries.size(); i++) { + this.sample.sampleCounter.put(sortedEntries.get(i).getKey(), sortedEntries.get(i).getValue()); + } + } + + public void addCounter(ItemStack itemStack) { + HashMap counter = this.sample.sampleCounter; + String key = itemStack.getTranslationKey(); + counter.put(key, counter.getOrDefault(key, 0L) + itemStack.getCount()); + } + + public static class Sample { + public String sampleDimension; + public double sampleX; + public double sampleY; + public double sampleZ; + public long sampleStartTimeMS; + public long sampleEndTimeMS; + public int sampleDistance; + public HashMap sampleCounter; + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/works/work_type/Work.java b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/Work.java new file mode 100644 index 000000000..6b9dd8db9 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/works/work_type/Work.java @@ -0,0 +1,230 @@ +package io.github.sakurawald.module.initializer.works.work_type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.works.gui.ConfirmGui; +import io.github.sakurawald.module.initializer.works.gui.InputSignGui; +import io.github.sakurawald.util.DateUtil; +import io.github.sakurawald.util.GuiUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.Data; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.Registries; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Data + +public abstract class Work { + + public String type; + public String id; + public long createTimeMS; + public String creator; + public String name; + public @Nullable String introduction; + public String level; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public @Nullable String icon; + + @SuppressWarnings("unused") + public Work() { + // for gson + } + + public Work(ServerPlayerEntity player, String name) { + this.type = getType(); + this.id = generateID(); + this.createTimeMS = System.currentTimeMillis(); + this.creator = player.getGameProfile().getName(); + this.name = name; + this.introduction = null; + this.level = player.getWorld().getRegistryKey().getValue().toString(); + this.x = player.getPos().x; + this.y = player.getPos().y; + this.z = player.getPos().z; + this.yaw = player.getYaw(); + this.pitch = player.getPitch(); + this.icon = null; + } + + private static Work getWorkByID(String uuid) { + List works = Configs.worksHandler.model().works; + for (Work work : works) { + if (work.getId().equals(uuid)) { + return work; + } + } + return null; + } + + protected abstract String getType(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Work work = (Work) o; + return id.equals(work.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + protected abstract String getDefaultIcon(); + + public abstract void openSpecializedSettingsGui(ServerPlayerEntity player, SimpleGui parentGui); + + + public void openGeneralSettingsGui(ServerPlayerEntity player, SimpleGui parentGui) { + Work work = this; + final SimpleGui gui = new SimpleGui(ScreenHandlerType.GENERIC_9X1, player, false); + gui.setLockPlayerInventory(true); + gui.setTitle(MessageUtil.ofVomponent(player, "works.work.set.general_settings.title")); + gui.addSlot(new GuiElementBuilder() + .setItem(Items.NAME_TAG) + .setName(MessageUtil.ofVomponent(player, "works.work.set.target.name")) + .setCallback(() -> new InputSignGui(player, null) { + @Override + public void onClose() { + String newValue = this.combineAllLinesReturnNull(); + if (newValue == null) { + MessageUtil.sendActionBar(player, "works.work.add.empty_name"); + return; + } + work.name = newValue; + MessageUtil.sendMessage(player, "works.work.set.done", work.name); + } + }.open()) + ); + gui.addSlot(new GuiElementBuilder() + .setItem(Items.CHERRY_HANGING_SIGN) + .setName(MessageUtil.ofVomponent(player, "works.work.set.target.introduction")) + .setCallback(() -> new InputSignGui(player, null) { + @Override + public void onClose() { + work.introduction = this.combineAllLinesReturnNull(); + MessageUtil.sendMessage(player, "works.work.set.done", work.introduction); + } + }.open()) + ); + gui.addSlot(new GuiElementBuilder() + .setItem(Items.END_PORTAL_FRAME) + .setName(MessageUtil.ofVomponent(player, "works.work.set.target.position")) + .setCallback(() -> { + work.level = player.getServerWorld().getRegistryKey().getValue().toString(); + work.x = player.getPos().x; + work.y = player.getPos().y; + work.z = player.getPos().z; + MessageUtil.sendMessage(player, "works.work.set.done", "(%s, %f, %f, %f)".formatted(work.level, work.x, work.y, work.z)); + gui.close(); + }) + ); + gui.addSlot(new GuiElementBuilder() + .setItem(Items.PAINTING) + .setName(MessageUtil.ofVomponent(player, "works.work.set.target.icon")) + .setCallback(() -> { + ItemStack mainHandItem = player.getMainHandStack(); + if (mainHandItem.isEmpty()) { + MessageUtil.sendActionBar(player, "works.work.set.target.icon.no_item"); + gui.close(); + return; + } + work.icon = Registries.ITEM.getId(mainHandItem.getItem()).toString(); + MessageUtil.sendMessage(player, "works.work.set.done", work.icon); + gui.close(); + }) + ); + + gui.addSlot(new GuiElementBuilder() + .setItem(Items.BARRIER) + .setName(MessageUtil.ofVomponent(player, "works.work.set.target.delete")) + .setCallback(() -> new ConfirmGui(player) { + @Override + public void onConfirm() { + Configs.worksHandler.model().works.remove(work); + MessageUtil.sendActionBar(player, "works.work.delete.done"); + } + }.open()) + + ); + + gui.setSlot(8, new GuiElementBuilder() + .setItem(Items.PLAYER_HEAD) + .setSkullOwner(GuiUtil.PREVIOUS_PAGE_ICON) + .setName(MessageUtil.ofVomponent(player, "works.list.back")) + .setCallback(parentGui::open) + ); + + // let's open it now + gui.open(); + } + + public Item asItem() { + NbtCompound rootTag = new NbtCompound(); + rootTag.putString("id", this.getIcon()); + rootTag.putInt("Count", 1); + return ItemStack.fromNbt(rootTag).getItem(); + } + + public @NotNull String getIcon() { + return this.icon == null ? getDefaultIcon() : this.icon; + } + + public List asLore(ServerPlayerEntity player) { + ArrayList ret = new ArrayList<>(); + ret.add(MessageUtil.ofVomponent(player, "works.work.prop.creator", this.creator)); + if (this.introduction != null) + ret.add(MessageUtil.ofVomponent(player, "works.work.prop.introduction", this.introduction)); + ret.add(MessageUtil.ofVomponent(player, "works.work.prop.time", DateUtil.toStandardDateFormat(this.createTimeMS))); + ret.add(MessageUtil.ofVomponent(player, "works.work.prop.dimension", this.level)); + ret.add(MessageUtil.ofVomponent(player, "works.work.prop.coordinate", this.x, this.y, this.z)); + return ret; + } + + private String generateID() { + String ret = null; + while (ret == null || getWorkByID(ret) != null) { + ret = UUID.randomUUID().toString().substring(0, 8); + } + return ret; + } + + + public static class WorkTypeAdapter implements JsonDeserializer { + @Override + public Work deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String type = json.getAsJsonObject().get("type").getAsString(); + if (type.equals(WorkType.NonProductionWork.name())) + return context.deserialize(json, NonProductionWork.class); + if (type.equals(WorkType.ProductionWork.name())) return context.deserialize(json, ProductionWork.class); + return null; + } + + public enum WorkType {NonProductionWork, ProductionWork} + } +} + + diff --git a/src/main/java/io/github/sakurawald/module/initializer/world_downloader/FileDownloadHandler.java b/src/main/java/io/github/sakurawald/module/initializer/world_downloader/FileDownloadHandler.java new file mode 100644 index 000000000..b07485213 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/world_downloader/FileDownloadHandler.java @@ -0,0 +1,76 @@ +package io.github.sakurawald.module.initializer.world_downloader; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import io.github.sakurawald.Fuji; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; + +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +@AllArgsConstructor + +public class FileDownloadHandler implements HttpHandler { + + private static final long NANO_TO_S = 1000000000L; + private final WorldDownloaderModule module; + private final File file; + private final int bytesPerSecond; + + @SuppressWarnings("BusyWait") + @Override + @SneakyThrows + public void handle(HttpExchange exchange) { + Fuji.LOGGER.info("Download file: {}", file.getAbsolutePath()); + + /* consume this context */ + module.safelyRemoveContext(exchange.getHttpContext()); + + /* transfer */ + if ("GET".equals(exchange.getRequestMethod())) { + if (file.exists() && file.isFile()) { + exchange.getResponseHeaders().set("Content-Disposition", "attachment; filename=" + file.getName()); + exchange.getResponseHeaders().set("Content-Type", "application/octet-stream"); + long fileLength = file.length(); + exchange.sendResponseHeaders(200, fileLength); + OutputStream os = exchange.getResponseBody(); + FileInputStream fis = new FileInputStream(file); + byte[] buffer = new byte[1024]; + int bytesRead; + + long startTime = System.nanoTime(); + long bytesReadCount = 0; + while ((bytesRead = fis.read(buffer)) != -1) { + long currentTime = System.nanoTime(); + long elapsedTime = currentTime - startTime; + long bytesReadExpected = (elapsedTime * bytesPerSecond) / NANO_TO_S; + if (bytesReadCount + bytesRead > bytesReadExpected) { + try { + long sleepTime = ((bytesReadCount + bytesRead - bytesReadExpected) * NANO_TO_S) + / bytesPerSecond; + Thread.sleep(TimeUnit.NANOSECONDS.toMillis(sleepTime)); + } catch (InterruptedException e) { + Fuji.LOGGER.warn("Interrupted while sleeping for throttling", e); + return; + } + } + + os.write(buffer, 0, bytesRead); + bytesReadCount += bytesRead; + } + fis.close(); + os.close(); + } else { + String response = "File not found."; + exchange.sendResponseHeaders(404, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + Fuji.LOGGER.info("Delete file: {} -> {}", file.getAbsolutePath(), file.delete()); + } +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/world_downloader/WorldDownloaderModule.java b/src/main/java/io/github/sakurawald/module/initializer/world_downloader/WorldDownloaderModule.java new file mode 100644 index 000000000..0cfc31ee8 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/world_downloader/WorldDownloaderModule.java @@ -0,0 +1,178 @@ +package io.github.sakurawald.module.initializer.world_downloader; + +import com.google.common.collect.EvictingQueue; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpServer; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.mixin.resource_world.MinecraftServerAccessor; +import io.github.sakurawald.util.CommandUtil; +import io.github.sakurawald.util.MessageUtil; +import lombok.SneakyThrows; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.level.storage.LevelStorage; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.UUID; + + +public class WorldDownloaderModule extends ModuleInitializer { + + private EvictingQueue contextQueue; + private HttpServer server; + + + @Override + public void onInitialize() { + contextQueue = EvictingQueue.create(Configs.configHandler.model().modules.world_downloader.context_cache_size); + } + + @Override + public void onReload() { + this.initServer(); + } + + public void initServer() { + if (server != null) { + server.stop(0); + } + + try { + server = HttpServer.create(new InetSocketAddress(Configs.configHandler.model().modules.world_downloader.port), 0); + server.start(); + } catch (IOException e) { + Fuji.LOGGER.error("Failed to start http server: " + e.getMessage()); + } + } + + @SuppressWarnings("unused") + @Override + public void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher.register(CommandManager.literal("download").executes(this::$download)); + } + + public void safelyRemoveContext(String path) { + try { + this.server.removeContext(path); + } catch (IllegalArgumentException e) { + // do nothing + } + } + + public void safelyRemoveContext(HttpContext httpContext) { + safelyRemoveContext(httpContext.getPath()); + } + + @SuppressWarnings("SameReturnValue") + @SneakyThrows + private int $download(CommandContext ctx) { + return CommandUtil.playerOnlyCommand(ctx, player -> { + /* init server */ + if (server == null) { + initServer(); + } + + /* remove redundant contexts */ + if (contextQueue.remainingCapacity() == 0) { + Fuji.LOGGER.info("contexts is full, remove the oldest context. {}", contextQueue.peek()); + safelyRemoveContext(contextQueue.poll()); + } + + /* create context */ + String url = Configs.configHandler.model().modules.world_downloader.url_format; + + int port = Configs.configHandler.model().modules.world_downloader.port; + url = url.replace("%port%", String.valueOf(port)); + + String path = "/download/" + UUID.randomUUID(); + url = url.replace("%path%", path); + + contextQueue.add(path); + File file = compressRegionFile(player); + double BYTE_TO_MEGABYTE = 1.0 * 1024 * 1024; + MessageUtil.sendBroadcast("world_downloader.request", player.getGameProfile().getName(), file.length() / BYTE_TO_MEGABYTE); + server.createContext(path, new FileDownloadHandler(this, file, Configs.configHandler.model().modules.world_downloader.bytes_per_second_limit)); + MessageUtil.sendMessage(player, "world_downloader.response", url); + return Command.SINGLE_SUCCESS; + }); + } + + public File compressRegionFile(ServerPlayerEntity player) { + /* get region location */ + ChunkPos chunkPos = player.getChunkPos(); + int regionX = chunkPos.getRegionX(); + int regionZ = chunkPos.getRegionZ(); + + /* get world folder */ + ServerWorld world = player.getServerWorld(); + MinecraftServer server = world.getServer(); + MinecraftServerAccessor serverAccess = (MinecraftServerAccessor) server; + RegistryKey dimensionKey = world.getRegistryKey(); + LevelStorage.Session session = serverAccess.getSession(); + File worldDirectory = session.getWorldDirectory(dimensionKey).toFile(); + + /* compress file */ + String regionName = "r." + regionX + "." + regionZ + ".mca"; + File[] input = { + new File(worldDirectory, "region" + File.separator + regionName), + new File(worldDirectory, "poi" + File.separator + regionName), + new File(worldDirectory, "entities" + File.separator + regionName) + }; + File output; + try { + output = Files.createTempFile(regionName + "#", ".zip").toFile(); + compressFiles(input, output); + } catch (IOException e) { + throw new RuntimeException(e); + } + Fuji.LOGGER.info("Generate region file: {}", output.getAbsolutePath()); + return output; + } + + @SneakyThrows + public void compressFiles(File[] input, File output) { + try (FileOutputStream fos = new FileOutputStream(output); + ArchiveOutputStream archiveOut = new ZipArchiveOutputStream(fos)) { + for (File file : input) { + if (file.isFile() && file.exists()) { + ArchiveEntry entry = new ZipArchiveEntry(file, getEntryName(file)); + archiveOut.putArchiveEntry(entry); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + archiveOut.write(buffer, 0, len); + } + } + archiveOut.closeArchiveEntry(); + } + } + } + } + + private String getEntryName(File file) { + return file.getParentFile().getName() + File.separator + file.getName(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/initializer/zero_command_permission/ZeroCommandPermissionModule.java b/src/main/java/io/github/sakurawald/module/initializer/zero_command_permission/ZeroCommandPermissionModule.java new file mode 100644 index 000000000..cbe19f435 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/initializer/zero_command_permission/ZeroCommandPermissionModule.java @@ -0,0 +1,63 @@ +package io.github.sakurawald.module.initializer.zero_command_permission; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import io.github.sakurawald.module.initializer.ModuleInitializer; +import io.github.sakurawald.module.mixin.zero_command_permission.CommandNodeAccessor; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import java.util.function.Predicate; + + +public class ZeroCommandPermissionModule extends ModuleInitializer { + + @Override + public void onInitialize() { + ServerLifecycleEvents.SERVER_STARTED.register(this::alterCommandPermission + ); + } + + public void alterCommandPermission(MinecraftServer server) { + CommandDispatcher dispatcher = server.getCommandManager().getDispatcher(); + alterCommandNode(dispatcher, dispatcher.getRoot()); + } + + private String buildCommandNodePath(CommandDispatcher dispatcher, CommandNode node) { + String[] array = dispatcher.getPath(node).toArray(new String[]{}); + return String.join(".", array); + } + + @SuppressWarnings("unchecked") + private void alterCommandNode(CommandDispatcher dispatcher, CommandNode node) { + var commandPath = buildCommandNodePath(dispatcher, node); + for (CommandNode child : node.getChildren()) { + alterCommandNode(dispatcher, child); + } + ((CommandNodeAccessor) node).setRequirement(createZeroPermission(commandPath, node.getRequirement())); + } + + private Predicate createZeroPermission(String commandPath, Predicate original) { + return source -> { + // ignore the non-player command source + if (source.getPlayer() == null) return original.test(source); + + try { + /* By default, command /seed has no permission. So we can create a zero-permission "zero.seed" + and then grant this permission to anyone so that he can use /seed command. + And also set other's permission zero.seed false to dis-allow them to use /seed command. + If a command doesn't have a zero-permission, then it will use the original requirement-supplier. + + Only valid command has its command path (command-alias also has its path, but it will redirect the execution to the real command-path) + */ + TriState triState = Permissions.getPermissionValue(source, "zero.%s".formatted(commandPath)); + return triState.orElseGet(() -> original.test(source)); + } catch (Throwable use_original_predicate_if_failed) { + return original.test(source); + } + }; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/ModuleMixinConfigPlugin.java b/src/main/java/io/github/sakurawald/module/mixin/ModuleMixinConfigPlugin.java new file mode 100644 index 000000000..981dc703c --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/ModuleMixinConfigPlugin.java @@ -0,0 +1,59 @@ +package io.github.sakurawald.module.mixin; + +import com.google.gson.JsonElement; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + + +public class ModuleMixinConfigPlugin implements IMixinConfigPlugin { + + private static final JsonElement mixinConfigs; + + static { + Configs.configHandler.loadFromDisk(); + mixinConfigs = Configs.configHandler.toJsonElement(); + } + + @Override + public void onLoad(String mixinPackage) { + // no-op + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + String basePackageName = ModuleManager.calculateBasePackageName(this.getClass(), mixinClassName); + if (basePackageName.startsWith("_")) return true; + return ModuleManager.enableModule(mixinConfigs, basePackageName); + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + // no-op + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + // no-op + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + // no-op + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/_internal/server_instance/MinecraftServerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/_internal/server_instance/MinecraftServerMixin.java new file mode 100644 index 000000000..80dbdc85b --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/_internal/server_instance/MinecraftServerMixin.java @@ -0,0 +1,21 @@ +package io.github.sakurawald.module.mixin._internal.server_instance; + +import io.github.sakurawald.Fuji; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static io.github.sakurawald.Fuji.LOGGER; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + + @Inject(method = "", at = @At("RETURN")) + private void $init(CallbackInfo ci) { + MinecraftServer server = (MinecraftServer) (Object) this; + LOGGER.debug("MinecraftServerMixin: $init: " + server); + Fuji.SERVER = server; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/afk/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/afk/PlayerListMixin.java new file mode 100644 index 000000000..cab219df3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/afk/PlayerListMixin.java @@ -0,0 +1,21 @@ +package io.github.sakurawald.module.mixin.afk; + +import io.github.sakurawald.module.initializer.afk.ServerPlayerAccessor_afk; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerManager.class) + +public abstract class PlayerListMixin { + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect") + private void $onPlayerConnect(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + ServerPlayerAccessor_afk afk_player = (ServerPlayerAccessor_afk) serverPlayer; + afk_player.fuji$setLastLastActionTime(serverPlayer.getLastActionTime()); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/mixin/afk/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/afk/ServerPlayerMixin.java new file mode 100644 index 000000000..7cc9a8bce --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/afk/ServerPlayerMixin.java @@ -0,0 +1,84 @@ +package io.github.sakurawald.module.mixin.afk; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.afk.ServerPlayerAccessor_afk; +import io.github.sakurawald.util.MessageUtil; +import net.kyori.adventure.text.TextReplacementConfig; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import static io.github.sakurawald.util.MessageUtil.ofComponent; +import static io.github.sakurawald.util.MessageUtil.toVomponent; + +@Mixin(ServerPlayerEntity.class) + +public abstract class ServerPlayerMixin implements ServerPlayerAccessor_afk { + + @Unique + private final ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + @Shadow + @Final + public MinecraftServer server; + + @Unique + private boolean afk = false; + + @Unique + private long lastLastActionTime = 0; + + @Inject(method = "getPlayerListName", at = @At("HEAD"), cancellable = true) + public void $getPlayerListName(CallbackInfoReturnable cir) { + ServerPlayerAccessor_afk accessor = (ServerPlayerAccessor_afk) player; + + if (accessor.fuji$isAfk()) { + cir.setReturnValue(Text.literal("afk " + player.getGameProfile().getName())); + net.kyori.adventure.text.@NotNull Component component = ofComponent(Configs.configHandler.model().modules.afk.format) + .replaceText(TextReplacementConfig.builder().match("%player_display_name%").replacement(player.getDisplayName()).build()); + cir.setReturnValue(toVomponent(component)); + } else { + cir.setReturnValue(null); + } + } + + + @Inject(method = "updateLastActionTime", at = @At("HEAD")) + public void $updateLastActionTime(CallbackInfo ci) { + if (fuji$isAfk()) { + fuji$setAfk(false); + } + } + + @Override + public void fuji$setAfk(boolean flag) { + this.afk = flag; + this.server.getPlayerManager().sendToAll(new PlayerListS2CPacket(PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME, (ServerPlayerEntity) (Object) this)); + MessageUtil.sendBroadcast(this.afk ? "afk.on.broadcast" : "afk.off.broadcast", this.player.getGameProfile().getName()); + } + + @Override + public boolean fuji$isAfk() { + return this.afk; + } + + @Override + public void fuji$setLastLastActionTime(long lastActionTime) { + this.lastLastActionTime = lastActionTime; + } + + @Override + public long fuji$getLastLastActionTime() { + return this.lastLastActionTime; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/back/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/back/ServerPlayerMixin.java new file mode 100644 index 000000000..8b830a0bb --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/back/ServerPlayerMixin.java @@ -0,0 +1,41 @@ +package io.github.sakurawald.module.mixin.back; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.back.BackModule; +import io.github.sakurawald.module.initializer.teleport_warmup.TeleportWarmupModule; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayerEntity.class) + +public abstract class ServerPlayerMixin { + + + @Unique + private static final BackModule module = ModuleManager.getInitializer(BackModule.class); + @Unique + private static final TeleportWarmupModule teleportWarmupModule = ModuleManager.getInitializer(TeleportWarmupModule.class); + + @Inject(method = "onDeath", at = @At("HEAD")) + public void $onDeath(DamageSource damageSource, CallbackInfo ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + module.updatePlayer(player); + } + + @Inject(method = "teleport(Lnet/minecraft/server/world/ServerWorld;DDDFF)V", at = @At("HEAD")) + public void $teleport(ServerWorld targetWorld, double x, double y, double z, float yaw, float pitch, CallbackInfo ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + + // note: if TeleportWarmupModule don't update back-position for us, we do it ourselves. + if (teleportWarmupModule == null) { + module.updatePlayer(player); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerCommandMixin.java b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerCommandMixin.java new file mode 100644 index 000000000..6b119f8bc --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerCommandMixin.java @@ -0,0 +1,83 @@ +package io.github.sakurawald.module.mixin.better_fake_player; + +import carpet.commands.PlayerCommand; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.better_fake_player.BetterFakePlayerModule; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@SuppressWarnings("DataFlowIssue") +@Mixin(PlayerCommand.class) +public abstract class PlayerCommandMixin { + + @Unique + private static final BetterFakePlayerModule module = ModuleManager.getInitializer(BetterFakePlayerModule.class); + + @Unique + private static String transformFakePlayerName(String fakePlayerName) { + return Configs.configHandler.model().modules.better_fake_player.transform_name.replace("%name%", fakePlayerName); + } + + @Redirect(method = "cantSpawn", at = @At( + value = "INVOKE", + target = "Lcom/mojang/brigadier/arguments/StringArgumentType;getString(Lcom/mojang/brigadier/context/CommandContext;Ljava/lang/String;)Ljava/lang/String;" + ), remap = false) + private static String $canSpawn(final CommandContext context, final String name) { + return transformFakePlayerName(StringArgumentType.getString(context, name)); + } + + @Redirect(method = "spawn", at = @At( + value = "INVOKE", + target = "Lcom/mojang/brigadier/arguments/StringArgumentType;getString(Lcom/mojang/brigadier/context/CommandContext;Ljava/lang/String;)Ljava/lang/String;" + ), remap = false) + private static String $spawn(final CommandContext context, final String name) { + return transformFakePlayerName(StringArgumentType.getString(context, name)); + } + + @Inject(method = "spawn", at = @At("HEAD"), remap = false, cancellable = true) + private static void $spawn_head(CommandContext context, CallbackInfoReturnable cir) { + ServerPlayerEntity player = context.getSource().getPlayer(); + if (player == null) return; + + if (!module.canSpawnFakePlayer(player)) { + MessageUtil.sendMessage(player, "better_fake_player.spawn.limit_exceed"); + cir.setReturnValue(0); + } + + /* fix: fake-player auth network laggy */ + if (Configs.configHandler.model().modules.better_fake_player.use_local_random_skins_for_fake_player) { + String fakePlayerName = StringArgumentType.getString(context, "player"); + fakePlayerName = transformFakePlayerName(fakePlayerName); + Fuji.SERVER.getUserCache().add(module.createOfflineGameProfile(fakePlayerName)); + } + } + + @Inject(method = "spawn", at = @At("TAIL"), remap = false) + private static void $spawn_tail(CommandContext context, CallbackInfoReturnable cir) { + ServerPlayerEntity player = context.getSource().getPlayer(); + String fakePlayerName = StringArgumentType.getString(context, "player"); + fakePlayerName = transformFakePlayerName(fakePlayerName); + module.addFakePlayer(player, fakePlayerName); + module.renewFakePlayers(player); + } + + @Inject(method = "cantManipulate", at = @At("HEAD"), remap = false, cancellable = true) + private static void $cantManipulate(CommandContext context, CallbackInfoReturnable cir) { + String fakePlayerName = StringArgumentType.getString(context, "player"); + if (!module.canManipulateFakePlayer(context, fakePlayerName)) { + MessageUtil.sendMessage(context.getSource(), "better_fake_player.manipulate.forbidden"); + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerListMixin.java new file mode 100644 index 000000000..ceb0d99fd --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerListMixin.java @@ -0,0 +1,28 @@ +package io.github.sakurawald.module.mixin.better_fake_player; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.better_fake_player.BetterFakePlayerModule; +import io.github.sakurawald.util.CarpetUtil; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerManager.class) +public abstract class PlayerListMixin { + @Unique + private static final BetterFakePlayerModule module = ModuleManager.getInitializer(BetterFakePlayerModule.class); + + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect") + private void $onPlayerConnect(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + if (CarpetUtil.isFakePlayer(serverPlayer)) return; + if (module.hasFakePlayers(serverPlayer)) { + module.renewFakePlayers(serverPlayer); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerMixin.java new file mode 100644 index 000000000..b55deafa0 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/better_fake_player/PlayerMixin.java @@ -0,0 +1,51 @@ +package io.github.sakurawald.module.mixin.better_fake_player; + +import carpet.patches.EntityPlayerMPFake; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.better_fake_player.BetterFakePlayerModule; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + + +// the carpet-fabric default event handler priority is 1000 +@Mixin(value = PlayerEntity.class, priority = 999) + +public abstract class PlayerMixin extends LivingEntity { + + @Unique + private static final BetterFakePlayerModule betterFakePlayerModule = ModuleManager.getInitializer(BetterFakePlayerModule.class); + + protected PlayerMixin(EntityType entityType, World level) { + super(entityType, level); + } + + @SuppressWarnings("DataFlowIssue") + @Inject(method = "interact", at = @At("HEAD"), cancellable = true) + private void $interact(Entity target, Hand hand, CallbackInfoReturnable cir) { + if (target instanceof EntityPlayerMPFake fakePlayer) { + ServerPlayerEntity source = (ServerPlayerEntity) (Object) this; + if (!betterFakePlayerModule.isMyFakePlayer(source, fakePlayer)) { + // cancel this event + cir.setReturnValue(ActionResult.FAIL); + + // main-hand and off-hand will both trigger this event + if (hand == Hand.MAIN_HAND) { + MessageUtil.sendMessage(source, "better_fake_player.manipulate.forbidden"); + } + } + } + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/better_info/InfoCommandMixin.java b/src/main/java/io/github/sakurawald/module/mixin/better_info/InfoCommandMixin.java new file mode 100644 index 000000000..9a2de926e --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/better_info/InfoCommandMixin.java @@ -0,0 +1,40 @@ +package io.github.sakurawald.module.mixin.better_info; + +import carpet.commands.InfoCommand; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtHelper; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(InfoCommand.class) + +public class InfoCommandMixin { + + @Inject(method = "infoBlock", at = @At(value = "INVOKE", target = "Lcarpet/utils/BlockInfo;blockInfo(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/server/world/ServerWorld;)Ljava/util/List;", shift = At.Shift.AFTER)) + private static void blockInfo(ServerCommandSource source, BlockPos pos, String grep, CallbackInfoReturnable cir) { + // is player ? + ServerPlayerEntity player = source.getPlayer(); + if (player == null) return; + + // is block entity ? + BlockEntity blockEntity = player.getWorld().getBlockEntity(pos); + + MutableText output = Text.empty().append("\n"); + if (blockEntity == null) { + player.sendMessage(output.append(Text.literal("No block entity found at " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ()))); + return; + } + + // send nbt data + NbtCompound compoundTag = blockEntity.createNbtWithIdentifyingData(); + player.sendMessage(output.append(Text.translatable("commands.data.block.query", pos.getX(), pos.getY(), pos.getZ(), NbtHelper.toPrettyPrintedText(compoundTag)))); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/biome_lookup_cache/NaturalSpawnerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/biome_lookup_cache/NaturalSpawnerMixin.java new file mode 100644 index 000000000..32c070104 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/biome_lookup_cache/NaturalSpawnerMixin.java @@ -0,0 +1,29 @@ +package io.github.sakurawald.module.mixin.biome_lookup_cache; + +import io.github.sakurawald.module.initializer.biome_lookup_cache.ChunkManager; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.SpawnHelper; +import net.minecraft.world.biome.Biome; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/* + * Carpet Mod also includes some optimization for entity-spawn, please enable lagFreeSpawn in carpet + * */ +@Mixin(SpawnHelper.class) +public abstract class NaturalSpawnerMixin { + @Redirect( + method = {"mobsAt", "getRandomSpawnMobAt"}, + require = 0, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;" + ) + ) + private static RegistryEntry $getRandomSpawnMobAt(ServerWorld level, BlockPos pos) { + return ChunkManager.getRoughBiome(level, pos); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/mixin/bypass_chat_speed/ServerGamePacketListenerImplMixin.java b/src/main/java/io/github/sakurawald/module/mixin/bypass_chat_speed/ServerGamePacketListenerImplMixin.java new file mode 100644 index 000000000..463612003 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/bypass_chat_speed/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.mixin.bypass_chat_speed; + +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerGamePacketListenerImplMixin { + + @Shadow + public ServerPlayerEntity player; + + @Inject(method = "checkForSpam", at = @At("HEAD"), cancellable = true) + public void $checkForSpam(CallbackInfo ci) { + ci.cancel(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/bypass_max_player_limit/DedicatedPlayerManagerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/bypass_max_player_limit/DedicatedPlayerManagerMixin.java new file mode 100644 index 000000000..fe86101d2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/bypass_max_player_limit/DedicatedPlayerManagerMixin.java @@ -0,0 +1,23 @@ +package io.github.sakurawald.module.mixin.bypass_max_player_limit; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.dedicated.DedicatedPlayerManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@SuppressWarnings("unused") +@Mixin(DedicatedPlayerManager.class) +public abstract class DedicatedPlayerManagerMixin { + + @ModifyExpressionValue( + method = "canBypassPlayerLimit", + at = @At( + value = "INVOKE", target = "Lnet/minecraft/server/OperatorList;canBypassPlayerLimit(Lcom/mojang/authlib/GameProfile;)Z" + ) + ) + public boolean disablePlayerLimit(boolean original, GameProfile profile) { + return true; + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/bypass_move_speed/ServerGamePacketListenerImplMixin.java b/src/main/java/io/github/sakurawald/module/mixin/bypass_move_speed/ServerGamePacketListenerImplMixin.java new file mode 100644 index 000000000..53baeb97d --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/bypass_move_speed/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,38 @@ +package io.github.sakurawald.module.mixin.bypass_move_speed; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@SuppressWarnings("unused") +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerGamePacketListenerImplMixin { + + @Shadow + public ServerPlayerEntity player; + + @ModifyExpressionValue( + method = "onPlayerMove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerPlayNetworkHandler;isHost()Z" + ) + ) + public boolean disablePlayerMoveTooQuickly(boolean original) { + return true; + } + + @ModifyExpressionValue( + method = "onVehicleMove", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerPlayNetworkHandler;isHost()Z" + ) + ) + public boolean disableVehicleMoveTooQuickly(boolean original) { + return true; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/chat/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/chat/PlayerListMixin.java new file mode 100644 index 000000000..a9e4af039 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/chat/PlayerListMixin.java @@ -0,0 +1,27 @@ +package io.github.sakurawald.module.mixin.chat; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.chat.ChatModule; +import io.github.sakurawald.util.CarpetUtil; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = PlayerManager.class, priority = 999) +public abstract class PlayerListMixin { + + @Unique + private static final ChatModule module = ModuleManager.getInitializer(ChatModule.class); + + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect") + private void $onPlayerConnect(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + if (CarpetUtil.isFakePlayer(serverPlayer)) return; + module.getChatHistory().forEach(serverPlayer::sendMessage); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/chat/ServerGamePacketListenerImplMixin.java b/src/main/java/io/github/sakurawald/module/mixin/chat/ServerGamePacketListenerImplMixin.java new file mode 100644 index 000000000..8c557ec26 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/chat/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,27 @@ +package io.github.sakurawald.module.mixin.chat; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.chat.ChatModule; +import net.minecraft.network.message.SignedMessage; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = ServerPlayNetworkHandler.class, priority = 1001) +public abstract class ServerGamePacketListenerImplMixin { + @Unique + private static final ChatModule module = ModuleManager.getInitializer(ChatModule.class); + @Shadow + public ServerPlayerEntity player; + + @Inject(method = "handleDecoratedMessage", at = @At(value = "HEAD"), cancellable = true) + public void handleChat(SignedMessage playerChatMessage, CallbackInfo ci) { + module.broadcastChatMessage(player, playerChatMessage.getContent().getString()); + ci.cancel(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/command_cooldown/CommandsMixin.java b/src/main/java/io/github/sakurawald/module/mixin/command_cooldown/CommandsMixin.java new file mode 100644 index 000000000..5baa240ee --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/command_cooldown/CommandsMixin.java @@ -0,0 +1,36 @@ +package io.github.sakurawald.module.mixin.command_cooldown; + +import com.mojang.brigadier.ParseResults; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.command_cooldown.CommandCooldownModule; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(CommandManager.class) + +public class CommandsMixin { + + @Unique + private static final CommandCooldownModule module = ModuleManager.getInitializer(CommandCooldownModule.class); + + // If you issue "///abcdefg", then commandLine = "//abcdefg" + @Inject(method = "execute", at = @At("HEAD"), cancellable = true) + public void $execute(ParseResults parseResults, String string, CallbackInfo ci) { + ServerPlayerEntity player = parseResults.getContext().getSource().getPlayer(); + if (player == null) return; + + long cooldown = module.calculateCommandCooldown(player, string); + if (cooldown > 0) { + MessageUtil.sendActionBar(player, "command_cooldown.cooldown", cooldown / 1000); + ci.cancel(); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/command_interactive/SignBlockMixin.java b/src/main/java/io/github/sakurawald/module/mixin/command_interactive/SignBlockMixin.java new file mode 100644 index 000000000..56060e3e4 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/command_interactive/SignBlockMixin.java @@ -0,0 +1,72 @@ +package io.github.sakurawald.module.mixin.command_interactive; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.scheduler.SpecializedCommand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.block.AbstractSignBlock; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.SignBlockEntity; +import net.minecraft.block.entity.SignText; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +@Mixin(AbstractSignBlock.class) + +public class SignBlockMixin { + @Inject(method = "onUse", at = @At("HEAD"), cancellable = true) + private void $onUse(BlockState blockState, World level, BlockPos blockPos, PlayerEntity player, Hand interactionHand, BlockHitResult blockHitResult, CallbackInfoReturnable cir) { + // bypass if player is sneaking + if (player.isSneaking()) return; + + // interact with sign + if (player instanceof ServerPlayerEntity serverPlayer) { + BlockEntity blockEntity = level.getBlockEntity(blockPos); + if (blockEntity instanceof SignBlockEntity signBlockEntity) { + SignText signText = signBlockEntity.getText(signBlockEntity.isPlayerFacingFront(player)); + String text = combineLines(signText).replace("@u", serverPlayer.getGameProfile().getName()); + if (text.contains("//")) { + cir.setReturnValue(ActionResult.CONSUME); + List commands = resolveCommands(text); + if (Configs.configHandler.model().modules.command_interactive.log_use) { + Fuji.LOGGER.info("Player {} execute commands: {}", serverPlayer.getName().getString(), commands); + } + SpecializedCommand.executeCommands(serverPlayer, commands); + } + } + } + } + + @Unique + public String combineLines(SignText signText) { + return Arrays.stream(signText.getMessages(false)).map(Text::getString).reduce("", String::concat); + } + + @Unique + /* text must contains "//" */ + public List resolveCommands(String text) { + int left = text.indexOf("//"); + // strip comments + text = text.substring(left + 2); + + // split commands + String[] split = text.split("//"); + return Arrays.stream(split).map(String::trim).collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/command_spy/CommandsMixin.java b/src/main/java/io/github/sakurawald/module/mixin/command_spy/CommandsMixin.java new file mode 100644 index 000000000..ebf26634e --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/command_spy/CommandsMixin.java @@ -0,0 +1,27 @@ +package io.github.sakurawald.module.mixin.command_spy; + +import com.mojang.brigadier.ParseResults; +import io.github.sakurawald.Fuji; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(CommandManager.class) + +public class CommandsMixin { + + // If you issue "///abcdefg", then commandLine = "//abcdefg" + @Inject(method = "execute", at = @At("HEAD")) + public void $execute(ParseResults parseResults, String string, CallbackInfo ci) { + ServerPlayerEntity player = parseResults.getContext().getSource().getPlayer(); + if (player == null) return; + + // fix: fabric console will not log the command issue + Fuji.LOGGER.info("{} issued server command: {}", player.getGameProfile().getName(), string); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/deathlog/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/deathlog/ServerPlayerMixin.java new file mode 100644 index 000000000..c43ba26d3 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/deathlog/ServerPlayerMixin.java @@ -0,0 +1,26 @@ +package io.github.sakurawald.module.mixin.deathlog; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.deathlog.DeathLogModule; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayerEntity.class) +public abstract class ServerPlayerMixin { + @Unique + private static final DeathLogModule module = ModuleManager.getInitializer(DeathLogModule.class); + + @Inject(method = "onDeath", at = @At("HEAD")) + public void $onDeath(DamageSource damageSource, CallbackInfo ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + // don't store empty inventory + if (player.getInventory().isEmpty()) return; + module.store(player); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/language/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/language/ServerPlayerMixin.java new file mode 100644 index 000000000..4e7f3109f --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/language/ServerPlayerMixin.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.mixin.language; + +import io.github.sakurawald.module.initializer.teleport_warmup.ServerPlayerAccessor; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.network.packet.c2s.common.SyncedClientOptions; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + + +@Mixin(ServerPlayerEntity.class) + +public abstract class ServerPlayerMixin implements ServerPlayerAccessor { + + @Inject(method = "setClientOptions", at = @At("HEAD")) + public void $setClientOptions(SyncedClientOptions clientInformation, CallbackInfo ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + MessageUtil.getPlayer2lang().put(player.getGameProfile().getName(), clientInformation.comp_1951()); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/main_stats/BlockMixin.java b/src/main/java/io/github/sakurawald/module/mixin/main_stats/BlockMixin.java new file mode 100644 index 000000000..b7ec7b026 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/main_stats/BlockMixin.java @@ -0,0 +1,31 @@ +package io.github.sakurawald.module.mixin.main_stats; + +import io.github.sakurawald.module.initializer.main_stats.MainStats; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Block.class) +public class BlockMixin { + + @Inject(method = "afterBreak", at = @At("HEAD")) + private void $afterBreak(World world, PlayerEntity player, BlockPos pos, BlockState state, BlockEntity blockEntity, ItemStack stack, CallbackInfo ci) { + MainStats.uuid2stats.get(player.getUuid().toString()).mined += 1; + } + + @Inject(method = "onPlaced", at = @At("HEAD")) + public void $onPlaced(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack, CallbackInfo ci) { + if (!(placer instanceof ServerPlayerEntity player)) return; + MainStats.uuid2stats.get(player.getUuid().toString()).placed += 1; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/main_stats/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/main_stats/PlayerListMixin.java new file mode 100644 index 000000000..d9c11cc34 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/main_stats/PlayerListMixin.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.mixin.main_stats; + +import io.github.sakurawald.module.initializer.main_stats.MainStats; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerManager.class) +public abstract class PlayerListMixin { + + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect") + private void $onPlayerConnect(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + String uuid = serverPlayer.getUuid().toString(); + MainStats stats = MainStats.calculatePlayerMainStats(uuid); + MainStats.uuid2stats.put(uuid, stats); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/main_stats/ServerPlayNetworkHandlerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/main_stats/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 000000000..c14586821 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/main_stats/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,25 @@ +package io.github.sakurawald.module.mixin.main_stats; + + +import io.github.sakurawald.module.initializer.main_stats.MainStats; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public class ServerPlayNetworkHandlerMixin { + + @Shadow + public ServerPlayerEntity player; + + @Inject(at = @At("HEAD"), method = "onDisconnected") + private void $disconnect(Text reason, CallbackInfo info) { + String uuid = player.getUuid().toString(); + MainStats.uuid2stats.remove(uuid); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/motd/ServerStatusPacketListenerImplMixin.java b/src/main/java/io/github/sakurawald/module/mixin/motd/ServerStatusPacketListenerImplMixin.java new file mode 100644 index 000000000..9b9c2b5b4 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/motd/ServerStatusPacketListenerImplMixin.java @@ -0,0 +1,55 @@ +/* + * This file is part of MiniMOTD, licensed under the MIT License. + * + * Copyright (c) 2020-2023 Jason Penilla + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.github.sakurawald.module.mixin.motd; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.motd.MotdModule; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.Optional; +import net.minecraft.server.ServerMetadata; +import net.minecraft.server.network.ServerQueryNetworkHandler; + +@Mixin(ServerQueryNetworkHandler.class) +abstract class ServerStatusPacketListenerImplMixin { + + @Unique + private static final MotdModule module = ModuleManager.getInitializer(MotdModule.class); + + + @Redirect(method = "onRequest", at = @At(value = "FIELD", target = "Lnet/minecraft/server/network/ServerQueryNetworkHandler;metadata:Lnet/minecraft/server/ServerMetadata;")) + public ServerMetadata $handleStatusRequest(final ServerQueryNetworkHandler instance) { + ServerMetadata vanillaStatus = Fuji.SERVER.getServerMetadata(); + if (vanillaStatus == null) { + Fuji.LOGGER.warn("ServerStatus is null, use default."); + return new ServerMetadata(module.getRandomDescription(), Optional.empty(), Optional.empty(), module.getRandomIcon(), false); + } + + return new ServerMetadata(module.getRandomDescription(), vanillaStatus.comp_1274(), vanillaStatus.comp_1275(), module.getRandomIcon(), vanillaStatus.secureChatEnforced()); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/multi_obsidian_platform/EntityMixin.java b/src/main/java/io/github/sakurawald/module/mixin/multi_obsidian_platform/EntityMixin.java new file mode 100644 index 000000000..131372756 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/multi_obsidian_platform/EntityMixin.java @@ -0,0 +1,51 @@ +package io.github.sakurawald.module.mixin.multi_obsidian_platform; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.multi_obsidian_platform.MultiObsidianPlatformModule; +import net.minecraft.entity.Entity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Entity.class) + +public abstract class EntityMixin { + + @Unique + private static final MultiObsidianPlatformModule module = ModuleManager.getInitializer(MultiObsidianPlatformModule.class); + + @Unique + BlockPos getTransformedEndSpawnPoint() { + Entity entity = (Entity) (Object) this; + return module.transform(entity.getBlockPos()); + } + + @Unique + World getEntityCurrentLevel() { + Entity entity = (Entity) (Object) this; + return entity.getWorld(); + } + + @Redirect(method = "getTeleportTarget", at = @At(value = "FIELD", target = "Lnet/minecraft/server/world/ServerWorld;END_SPAWN_POS:Lnet/minecraft/util/math/BlockPos;"), require = 1) + BlockPos $findDimensionEntryPoint(ServerWorld toLevel) { + // modify: resource_world:overworld -> minecraft:the_end (default obsidian platform) + // feature: https://bugs.mojang.com/browse/MC-252361 + if (getEntityCurrentLevel().getRegistryKey() != World.OVERWORLD) return ServerWorld.END_SPAWN_POS; + return getTransformedEndSpawnPoint(); + } + + /* This method will NOT be called when a PLAYER jump into overworld's ender-portal-frame */ + @Redirect(method = "moveToWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/world/ServerWorld;createEndSpawnPlatform(Lnet/minecraft/server/world/ServerWorld;)V"), require = 1) + public void $changeDimension(ServerWorld toLevel) { + // modify: resource_world:overworld -> minecraft:the_end (default obsidian platform) + if (getEntityCurrentLevel().getRegistryKey() != World.OVERWORLD) { + ServerWorld.createEndSpawnPlatform(toLevel); + return; + } + module.makeObsidianPlatform(toLevel, getTransformedEndSpawnPoint()); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/newbie_welcome/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/newbie_welcome/PlayerListMixin.java new file mode 100644 index 000000000..571af8d9a --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/newbie_welcome/PlayerListMixin.java @@ -0,0 +1,29 @@ +package io.github.sakurawald.module.mixin.newbie_welcome; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.newbie_welcome.NewbieWelcomeModule; +import io.github.sakurawald.util.CarpetUtil; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.stat.Stats; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerManager.class) +public abstract class PlayerListMixin { + @Unique + private static final NewbieWelcomeModule module = ModuleManager.getInitializer(NewbieWelcomeModule.class); + + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect") + private void $onPlayerConnect(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + if (CarpetUtil.isFakePlayer(serverPlayer)) return; + if (serverPlayer.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(Stats.LEAVE_GAME)) < 1) { + module.welcomeNewbiePlayer(serverPlayer); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/op_protect/ServerPlayNetworkHandlerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/op_protect/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 000000000..cf064f1b4 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/op_protect/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,28 @@ +package io.github.sakurawald.module.mixin.op_protect; + + +import io.github.sakurawald.Fuji; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + + +@Mixin(ServerPlayNetworkHandler.class) +public class ServerPlayNetworkHandlerMixin { + + @Shadow + public ServerPlayerEntity player; + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerCommonPacketListenerImpl;onDisconnect(Lnet/minecraft/network/chat/Component;)V"), method = "onDisconnect") + private void $disconnect(Text reason, CallbackInfo info) { + if (Fuji.SERVER.getPlayerManager().isOperator(player.getGameProfile())) { + Fuji.LOGGER.info("op protect -> deop " + player.getGameProfile().getName()); + Fuji.SERVER.getPlayerManager().removeFromOperators(player.getGameProfile()); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/pvp/PvpToggleMixin.java b/src/main/java/io/github/sakurawald/module/mixin/pvp/PvpToggleMixin.java new file mode 100644 index 000000000..457c52dfc --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/pvp/PvpToggleMixin.java @@ -0,0 +1,44 @@ +package io.github.sakurawald.module.mixin.pvp; + +import com.mojang.authlib.GameProfile; +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.pvp.PvpModule; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayerEntity.class) +public abstract class PvpToggleMixin extends PlayerEntity { + @Unique + private static final PvpModule module = ModuleManager.getInitializer(PvpModule.class); + + public PvpToggleMixin(World world, BlockPos pos, float yaw, GameProfile gameProfile) { + super(world, pos, yaw, gameProfile); + } + + @Inject(method = "shouldDamagePlayer", at = @At("HEAD"), cancellable = true) + public void $shouldDamagePlayer(PlayerEntity sourcePlayer, CallbackInfoReturnable cir) { + if (this == sourcePlayer) return; + + ServerPlayerEntity player = sourcePlayer.getCommandSource().getPlayer(); + if (player == null) return; + + if (!module.contains(sourcePlayer.getGameProfile().getName())) { + MessageUtil.sendMessage(player, "pvp.check.off.me"); + cir.setReturnValue(false); + return; + } + + if (!module.contains(this.getGameProfile().getName())) { + MessageUtil.sendMessage(player, "pvp.check.off.others", this.getGameProfile().getName()); + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/reply/MsgCommandMixin.java b/src/main/java/io/github/sakurawald/module/mixin/reply/MsgCommandMixin.java new file mode 100644 index 000000000..599541a41 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/reply/MsgCommandMixin.java @@ -0,0 +1,30 @@ +package io.github.sakurawald.module.mixin.reply; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.reply.ReplyModule; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collection; +import net.minecraft.network.message.SignedMessage; +import net.minecraft.server.command.MessageCommand; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +@Mixin(MessageCommand.class) +public class MsgCommandMixin { + + @Unique + private static final ReplyModule module = ModuleManager.getInitializer(ReplyModule.class); + + @Inject(method = "execute", at = @At("HEAD")) + private static void $execute(ServerCommandSource commandSourceStack, Collection collection, SignedMessage playerChatMessage, CallbackInfo ci) { + ServerPlayerEntity source = commandSourceStack.getPlayer(); + if (source == null) return; + + collection.forEach(target -> module.updateReplyTarget(target.getGameProfile().getName(), source.getGameProfile().getName())); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/EndGatewayBlockEntityMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/EndGatewayBlockEntityMixin.java new file mode 100644 index 000000000..828128796 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/EndGatewayBlockEntityMixin.java @@ -0,0 +1,18 @@ +package io.github.sakurawald.module.mixin.resource_world; + +import net.minecraft.block.entity.EndGatewayBlockEntity; +import net.minecraft.registry.RegistryKey; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(EndGatewayBlockEntity.class) +public abstract class EndGatewayBlockEntityMixin { + + // note: for a resource end, we force make World.END == World.END a true condition, so that the end gateway in resource end can work. + @Redirect(method = "tryTeleportingEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;getRegistryKey()Lnet/minecraft/registry/RegistryKey;")) + private static RegistryKey $tryTeleportingEntity(World instance) { + return World.END; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerAccessor.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerAccessor.java new file mode 100644 index 000000000..d4139857c --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerAccessor.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.mixin.resource_world; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.World; +import net.minecraft.world.level.storage.LevelStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(MinecraftServer.class) +public interface MinecraftServerAccessor { + + @Accessor + Map, ServerWorld> getWorlds(); + + @Accessor + LevelStorage.Session getSession(); + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerMixin.java new file mode 100644 index 000000000..409b89b45 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/MinecraftServerMixin.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.module.mixin.resource_world; + +import io.github.sakurawald.module.initializer.resource_world.SafeIterator; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.Collection; +import java.util.Iterator; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin { + /* After issue /rw reset, then it's possible that all the worlds will be ticked 2 times. + and do it again it's 3 times... + */ + @Redirect(method = "tickWorlds", at = @At(value = "INVOKE", target = "Ljava/lang/Iterable;iterator()Ljava/util/Iterator;", ordinal = 0), require = 0) + private Iterator fuji$copyBeforeTicking(Iterable instance) { + return new SafeIterator<>((Collection) instance); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/ServerWorldMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/ServerWorldMixin.java new file mode 100644 index 000000000..43931f0e2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/ServerWorldMixin.java @@ -0,0 +1,32 @@ +package io.github.sakurawald.module.mixin.resource_world; + +import net.minecraft.network.packet.Packet; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.world.ServerChunkManager; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerWorld.class) +public abstract class ServerWorldMixin { + + @Shadow + public abstract ServerChunkManager getChunkManager(); + + @Redirect( + method = "tickWeather", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/PlayerManager;sendToAll(Lnet/minecraft/network/packet/Packet;)V" + ) + + ) + private void dontSendWeatherPacketsToAllWorlds(PlayerManager instance, Packet packet) { + // Vanilla sends rain packets to all players when rain starts in a world, + // even if they are not in it, meaning that if it is possible to rain in the world they are in + // the rain effect will remain until the player changes dimension or reconnects. + instance.sendToDimension(packet, this.getChunkManager().getWorld().getRegistryKey()); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsMixin.java new file mode 100644 index 000000000..6f34c6f08 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsMixin.java @@ -0,0 +1,23 @@ +package io.github.sakurawald.module.mixin.resource_world.registry; + +import io.github.sakurawald.module.initializer.resource_world.interfaces.DimensionOptionsMixinInterface; +import net.minecraft.world.dimension.DimensionOptions; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(DimensionOptions.class) +public class DimensionOptionsMixin implements DimensionOptionsMixinInterface { + + @Unique + private boolean fuji$saveProperties = true; + + @Unique + public void fuji$setSaveProperties(boolean value) { + this.fuji$saveProperties = value; + } + + @Unique + public boolean fuji$getSaveProperties() { + return this.fuji$saveProperties; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsRegistryHolderMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsRegistryHolderMixin.java new file mode 100644 index 000000000..c0c1916fd --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/DimensionOptionsRegistryHolderMixin.java @@ -0,0 +1,21 @@ +package io.github.sakurawald.module.mixin.resource_world.registry; + +import io.github.sakurawald.module.initializer.resource_world.FilteredRegistry; +import io.github.sakurawald.module.initializer.resource_world.interfaces.DimensionOptionsMixinInterface; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.function.Function; +import net.minecraft.registry.Registry; +import net.minecraft.world.dimension.DimensionOptions; +import net.minecraft.world.dimension.DimensionOptionsRegistryHolder; + +@Mixin(DimensionOptionsRegistryHolder.class) +public class DimensionOptionsRegistryHolderMixin { + /* Prevent resource worlds to write in level.dat */ + @ModifyArg(method = "method_45516", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/MapCodec;forGetter(Ljava/util/function/Function;)Lcom/mojang/serialization/codecs/RecordCodecBuilder;")) + private static Function> fuji$swapRegistryGetter(Function> getter) { + return (x) -> new FilteredRegistry<>(getter.apply(x), DimensionOptionsMixinInterface.SAVE_PROPERTIES_PREDICATE); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/SimpleRegistryMixin.java b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/SimpleRegistryMixin.java new file mode 100644 index 000000000..e44fd520f --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/resource_world/registry/SimpleRegistryMixin.java @@ -0,0 +1,101 @@ +package io.github.sakurawald.module.mixin.resource_world.registry; + +import com.mojang.serialization.Lifecycle; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.resource_world.interfaces.SimpleRegistryMixinInterface; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.SimpleRegistry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.entry.RegistryEntry.Reference; +import net.minecraft.util.Identifier; + +@SuppressWarnings("unused") +@Mixin(SimpleRegistry.class) + +public abstract class SimpleRegistryMixin implements SimpleRegistryMixinInterface { + + @Shadow + @Final + private Map> valueToEntry; + + @Shadow + @Final + private Map> idToEntry; + + @Shadow + @Final + private Map, RegistryEntry.Reference> keyToEntry; + + @Shadow + @Final + private Map entryToLifecycle; + + @Shadow + @Final + private ObjectList> rawIdToEntry; + + @Shadow + @Final + private Reference2IntMap entryToRawId; + + @Shadow + private boolean frozen; + @Shadow + @Nullable + private List> cachedEntries; + + @Shadow + public abstract Optional> getEntry(int rawId); + + @Override + public boolean fuji$remove(T entry) { + var registryEntry = this.valueToEntry.get(entry); + int rawId = this.entryToRawId.removeInt(entry); + if (rawId == -1) { + return false; + } + + try { + this.rawIdToEntry.set(rawId, null); + this.idToEntry.remove(registryEntry.registryKey().getValue()); + this.keyToEntry.remove(registryEntry.registryKey()); + this.entryToLifecycle.remove(entry); + this.valueToEntry.remove(entry); + if (this.cachedEntries != null) { + this.cachedEntries.remove(registryEntry); + } + + return true; + } catch (Throwable e) { + Fuji.LOGGER.error("Failed to remove entry: " + entry.toString()); + return false; + } + } + + @Override + public boolean fuji$remove(Identifier key) { + var entry = this.idToEntry.get(key); + return entry != null && entry.hasKeyAndValue() && this.fuji$remove(entry.comp_349()); + } + + @Override + public void fuji$setFrozen(boolean value) { + this.frozen = value; + } + + @Override + public boolean fuji$isFrozen() { + return this.frozen; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/seen/GameProfileCacheMixin.java b/src/main/java/io/github/sakurawald/module/mixin/seen/GameProfileCacheMixin.java new file mode 100644 index 000000000..aa966d783 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/seen/GameProfileCacheMixin.java @@ -0,0 +1,26 @@ +package io.github.sakurawald.module.mixin.seen; + +import io.github.sakurawald.module.initializer.seen.GameProfileCacheEx; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import net.minecraft.util.UserCache; + +@Mixin(UserCache.class) +public class GameProfileCacheMixin implements GameProfileCacheEx { + @Final + @Shadow + private Map byName; + + + @Override + public Collection fuji$getNames() { + ArrayList ret = new ArrayList<>(); + byName.values().forEach(o -> ret.add(o.getProfile().getName())); + return ret; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/seen/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/seen/PlayerListMixin.java new file mode 100644 index 000000000..fc00b56a7 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/seen/PlayerListMixin.java @@ -0,0 +1,26 @@ +package io.github.sakurawald.module.mixin.seen; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.seen.SeenModule; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerManager.class) + +public abstract class PlayerListMixin { + + @Unique + private SeenModule module = ModuleManager.getInitializer(SeenModule.class); + + @Inject(method = "remove", at = @At("TAIL")) + private void remove(ServerPlayerEntity player, CallbackInfo ci) { + module.getData().model().player2seen.put(player.getGameProfile().getName(), System.currentTimeMillis()); + module.getData().saveToDisk(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/skin/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/skin/PlayerListMixin.java new file mode 100644 index 000000000..85045248c --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/skin/PlayerListMixin.java @@ -0,0 +1,49 @@ +package io.github.sakurawald.module.mixin.skin; + +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.module.initializer.skin.SkinRestorer; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collections; +import java.util.List; + +@Mixin(PlayerManager.class) + +public abstract class PlayerListMixin { + + @Shadow + @Final + private MinecraftServer server; + + @Shadow + public abstract List getPlayerList(); + + @Inject(method = "remove", at = @At("TAIL")) + private void remove(ServerPlayerEntity player, CallbackInfo ci) { + SkinRestorer.getSkinStorage().removeSkin(player.getUuid()); + } + + @Inject(method = "disconnectAllPlayers", at = @At("HEAD")) + private void disconnectAllPlayers(CallbackInfo ci) { + getPlayerList().forEach(player -> SkinRestorer.getSkinStorage().removeSkin(player.getUuid())); + } + + @Inject(method = "onPlayerConnect", at = @At("HEAD")) + private void onPlayerConnected(ClientConnection connection, ServerPlayerEntity serverPlayer, ConnectedClientData commonListenerCookie, CallbackInfo ci) { + // if the player isn't a server player entity, it must be someone's fake player + if (serverPlayer.getClass() != ServerPlayerEntity.class + && Configs.configHandler.model().modules.better_fake_player.use_local_random_skins_for_fake_player) { + SkinRestorer.setSkinAsync(server, Collections.singleton(serverPlayer.getGameProfile()), () -> SkinRestorer.getSkinStorage().getRandomSkin(serverPlayer.getUuid())); + } + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/skin/ServerLoginNetworkHandlerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/skin/ServerLoginNetworkHandlerMixin.java new file mode 100644 index 000000000..64111304b --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/skin/ServerLoginNetworkHandlerMixin.java @@ -0,0 +1,65 @@ +package io.github.sakurawald.module.mixin.skin; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import io.github.sakurawald.module.initializer.skin.SkinRestorer; +import io.github.sakurawald.module.initializer.skin.provider.MojangSkinProvider; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.server.network.ServerLoginNetworkHandler; + +@SuppressWarnings("MissingUnique") +@Mixin(ServerLoginNetworkHandler.class) +public abstract class ServerLoginNetworkHandlerMixin { + + @Shadow + @Final + static Logger LOGGER; + + @Shadow + @Nullable + private GameProfile profile; + + private CompletableFuture pendingSkins; + + private static void applyRestoredSkin(GameProfile gameProfile, Property skin) { + gameProfile.getProperties().removeAll("textures"); + gameProfile.getProperties().put("textures", skin); + } + + @Inject(method = "tickVerify", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;"), cancellable = true) + public void waitForSkin(CallbackInfo ci) { + if (pendingSkins == null) { + pendingSkins = CompletableFuture.supplyAsync(() -> { + // the first time the player join, his skin is DEFAULT_SKIN (see #applyRestoredSkinHook) + // then we try to get skin from mojang-server. if this failed, then set his skin to DEFAULT_SKIN + // note: a fake-player will not trigger waitForSkin() + LOGGER.info("Fetch skin for {}", profile.getName()); + + if (SkinRestorer.getSkinStorage().getSkin(profile.getId()) == SkinRestorer.getSkinStorage().getDefaultSkin()) { + SkinRestorer.getSkinStorage().setSkin(profile.getId(), MojangSkinProvider.getSkin(profile.getName())); + } + return SkinRestorer.getSkinStorage().getSkin(profile.getId()); + }); + } + + // cancel the player's login until we finish fetching his skin + if (!pendingSkins.isDone()) { + ci.cancel(); + } + } + + @Inject(method = "sendSuccessPacket", at = @At("HEAD")) + public void applyRestoredSkinHook(GameProfile gameProfile, CallbackInfo ci) { + if (pendingSkins != null) + applyRestoredSkin(gameProfile, pendingSkins.getNow(SkinRestorer.getSkinStorage().getDefaultSkin())); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/PlayerListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/PlayerListMixin.java new file mode 100644 index 000000000..7e8a82bac --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/PlayerListMixin.java @@ -0,0 +1,35 @@ +package io.github.sakurawald.module.mixin.stronger_player_list; + +import io.github.sakurawald.Fuji; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; + +@Mixin(PlayerManager.class) + +public abstract class PlayerListMixin { + + @Mutable + @Final + @Shadow + private List players; + + @Inject(method = "", at = @At("TAIL"), require = 1) + private void $init(CallbackInfo ci) { + players = new CopyOnWriteArrayList<>() { + { + Fuji.LOGGER.info("Patch stronger player list for Server#PlayerList"); + } + }; + } + +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/ServerLevelMixin.java b/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/ServerLevelMixin.java new file mode 100644 index 000000000..a37ae3ce8 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/stronger_player_list/ServerLevelMixin.java @@ -0,0 +1,36 @@ +package io.github.sakurawald.module.mixin.stronger_player_list; + +import io.github.sakurawald.Fuji; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +@Mixin(ServerWorld.class) + +public abstract class ServerLevelMixin { + + @Mutable + @Final + @Shadow + List players; + + @Inject(method = "", at = @At("TAIL"), require = 1) + private void $init(CallbackInfo ci) { + ServerWorld that = (ServerWorld) (Object) this; + players = new CopyOnWriteArrayList<>() { + { + Fuji.LOGGER.info("Patch stronger player list for {}", that.getRegistryKey().getValue()); + } + }; + } + +} \ No newline at end of file diff --git a/src/main/java/io/github/sakurawald/module/mixin/system_message/ComponentMixin.java b/src/main/java/io/github/sakurawald/module/mixin/system_message/ComponentMixin.java new file mode 100644 index 000000000..6c0cba628 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/system_message/ComponentMixin.java @@ -0,0 +1,49 @@ +package io.github.sakurawald.module.mixin.system_message; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.Configs; +import io.github.sakurawald.util.MessageUtil; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableTextContent; + +import static io.github.sakurawald.Fuji.LOGGER; + +@Mixin(Text.class) +public interface ComponentMixin { + + @Inject(method = "translatable(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/text/MutableText;", at = @At("RETURN"), cancellable = true) + private static void translatable(String key, Object[] args, CallbackInfoReturnable cir) { + MutableText newValue = transform(key, args); + if (newValue != null) cir.setReturnValue(newValue); + } + + @Inject(method = "translatable(Ljava/lang/String;)Lnet/minecraft/text/MutableText;", at = @At("RETURN"), cancellable = true) + private static void translatable(String key, CallbackInfoReturnable cir) { + MutableText newValue = transform(key); + if (newValue != null) cir.setReturnValue(newValue); + } + + @Unique + private static @Nullable MutableText transform(String key, Object... args) { + Map key2value = Configs.configHandler.model().modules.system_message.key2value; + if (key2value.containsKey(key)) { + if (Fuji.SERVER == null) { + LOGGER.warn("Server is null currently -> cannot hijack message key: {}", key); + return null; + } + String value = key2value.get(key); + String miniMessageSource = MutableText.of(new TranslatableTextContent("force_fallback", value, args)).getString(); + return MessageUtil.ofVomponent(miniMessageSource).copy(); + } + return null; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/teleport_warmup/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/module/mixin/teleport_warmup/ServerPlayerMixin.java new file mode 100644 index 000000000..8ec7504f1 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/teleport_warmup/ServerPlayerMixin.java @@ -0,0 +1,89 @@ +package io.github.sakurawald.module.mixin.teleport_warmup; + +import io.github.sakurawald.module.ModuleManager; +import io.github.sakurawald.module.initializer.back.BackModule; +import io.github.sakurawald.module.initializer.teleport_warmup.Position; +import io.github.sakurawald.module.initializer.teleport_warmup.ServerPlayerAccessor; +import io.github.sakurawald.module.initializer.teleport_warmup.TeleportTicket; +import io.github.sakurawald.module.initializer.teleport_warmup.TeleportWarmupModule; +import io.github.sakurawald.util.CarpetUtil; +import io.github.sakurawald.util.MessageUtil; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + + +@Mixin(ServerPlayerEntity.class) + +public abstract class ServerPlayerMixin implements ServerPlayerAccessor { + @Unique + private static final BackModule backModule = ModuleManager.getInitializer(BackModule.class); + @Unique + private static final TeleportWarmupModule module = ModuleManager.getInitializer(TeleportWarmupModule.class); + @Unique + public boolean fuji$inCombat; + + @Inject(method = "teleport(Lnet/minecraft/server/world/ServerWorld;DDDFF)V", at = @At("HEAD"), cancellable = true) + public void $teleport(ServerWorld targetWorld, double x, double y, double z, float yaw, float pitch, CallbackInfo ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + + // If we try to spawn a fake-player in the end or nether, the fake-player will initially spawn in overworld + // and teleport to the target world. This will cause the teleport warmup to be triggered. + if (CarpetUtil.isFakePlayer(player)) return; + + String playerName = player.getGameProfile().getName(); + if (!module.tickets.containsKey(playerName)) { + module.tickets.put(playerName, + new TeleportTicket(player + , Position.of(player), new Position(targetWorld, x, y, z, yaw, pitch), false)); + ci.cancel(); + return; + } else { + TeleportTicket ticket = module.tickets.get(playerName); + if (!ticket.ready) { + MessageUtil.sendActionBar(player, "teleport_warmup.another_teleportation_in_progress"); + ci.cancel(); + return; + } + } + + // let's do teleport now. + if (backModule != null) { + backModule.updatePlayer(player); + } + } + + @Inject(method = "damage", at = @At("RETURN")) + public void $damage(DamageSource damageSource, float amount, CallbackInfoReturnable cir) { + // If damage was actually applied... + if (cir.getReturnValue()) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + String playerName = player.getGameProfile().getName(); + if (module.tickets.containsKey(playerName)) { + module.tickets.get(playerName).bossbar.removeViewer(player); + module.tickets.remove(playerName); + } + } + } + + @Inject(method = "enterCombat", at = @At("RETURN")) + public void $enterCombat(CallbackInfo ci) { + fuji$inCombat = true; + } + + @Inject(method = "endCombat", at = @At("RETURN")) + public void $leaveCombat(CallbackInfo ci) { + fuji$inCombat = false; + } + + @Override + public boolean fuji$inCombat() { + return fuji$inCombat; + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ChunkMapMixin.java b/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ChunkMapMixin.java new file mode 100644 index 000000000..c75803dfb --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ChunkMapMixin.java @@ -0,0 +1,47 @@ +package io.github.sakurawald.module.mixin.tick_chunk_cache; + +import io.github.sakurawald.module.initializer.tick_chunk_cache.ITickableChunkSource; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ChunkLevelType; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import net.minecraft.util.math.ChunkPos; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ThreadedAnvilChunkStorage.class) +public class ChunkMapMixin implements ITickableChunkSource { + + @Shadow + @Final + private Long2ObjectLinkedOpenHashMap currentChunkHolders; + + @Unique + private Long2ObjectLinkedOpenHashMap tickingChunksCache = new Long2ObjectLinkedOpenHashMap<>(); + + @Inject(method = "", at = @At("RETURN")) + private void onInit(CallbackInfo ci) { + this.tickingChunksCache = new Long2ObjectLinkedOpenHashMap<>(); + } + + @Inject(method = "onChunkStatusChange", at = @At("HEAD")) + private void $onChunkStatusChange(ChunkPos chunkPos, ChunkLevelType levelType, CallbackInfo ci) { + final ChunkHolder chunkHolder = this.currentChunkHolders.get(chunkPos.toLong()); + if (chunkHolder == null) return; + if (chunkHolder.getLevelType().isAfter(ChunkLevelType.BLOCK_TICKING)) { + this.tickingChunksCache.put(chunkPos.toLong(), chunkHolder); + } else { + this.tickingChunksCache.remove(chunkPos.toLong()); + } + } + + @Override + public Iterable fuji$tickableChunksIterator() { + return this.tickingChunksCache.values(); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ServerChunkCacheMixin.java b/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ServerChunkCacheMixin.java new file mode 100644 index 000000000..ccc7575bc --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/tick_chunk_cache/ServerChunkCacheMixin.java @@ -0,0 +1,19 @@ +package io.github.sakurawald.module.mixin.tick_chunk_cache; + +import io.github.sakurawald.module.initializer.tick_chunk_cache.ITickableChunkSource; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ServerChunkManager; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerChunkManager.class) +public class ServerChunkCacheMixin { + + @Redirect(method = "tickChunks", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/world/ThreadedAnvilChunkStorage;entryIterator()Ljava/lang/Iterable;")) + private Iterable $tickChunks(ThreadedAnvilChunkStorage instance) { + return ((ITickableChunkSource) instance).fuji$tickableChunksIterator(); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/top_chunks/ThreadedAnvilChunkStorageMixin.java b/src/main/java/io/github/sakurawald/module/mixin/top_chunks/ThreadedAnvilChunkStorageMixin.java new file mode 100644 index 000000000..ca3f70691 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/top_chunks/ThreadedAnvilChunkStorageMixin.java @@ -0,0 +1,12 @@ +package io.github.sakurawald.module.mixin.top_chunks; + +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(ThreadedAnvilChunkStorage.class) +public interface ThreadedAnvilChunkStorageMixin { + @Invoker("entryIterator") + Iterable $getChunks(); +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/whitelist_fix/UserWhiteListMixin.java b/src/main/java/io/github/sakurawald/module/mixin/whitelist_fix/UserWhiteListMixin.java new file mode 100644 index 000000000..fb31a07c2 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/whitelist_fix/UserWhiteListMixin.java @@ -0,0 +1,38 @@ +package io.github.sakurawald.module.mixin.whitelist_fix; + +import com.mojang.authlib.GameProfile; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.Whitelist; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Whitelist.class) + +public class UserWhiteListMixin { + + /** + * Once an offline-player join the server, then the offline-uuid will be added to usercache.json. + * After that, the server will always use the player's offline-uuid in usercache.json to check whitelist (and other list, like ban list, op list). + *

+ * If you use white-list=true with online-mode=false, then the cases is: + * 1. for online-player, everything is ok. + * 2. for offline-player, the whitelist will always check the online-uuid, so you need to type /whitelist off to disable whitelist, + * and let the offline-player join the game, so that the usercache.json can be updated to the offline-uuid. + *

+ * This @Inject makes the whitelist.json only look-up for the player's name, not the uuid. + * + * @see Whitelist#isAllowed(GameProfile) + * @see Whitelist#toString(GameProfile) + * @see net.minecraft.server.PlayerManager#onPlayerConnect(ClientConnection, ServerPlayerEntity, ConnectedClientData) + * @see net.minecraft.util.UserCache#add(GameProfile) + **/ + @Inject(method = "toString*", at = @At("HEAD"), cancellable = true) + void $getKeyForUser(GameProfile gameProfile, CallbackInfoReturnable ci) { + String ret = gameProfile.getName(); + ci.setReturnValue(ret); + } +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/works/HopperBlockEntityMixin.java b/src/main/java/io/github/sakurawald/module/mixin/works/HopperBlockEntityMixin.java new file mode 100644 index 000000000..3807aec48 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/works/HopperBlockEntityMixin.java @@ -0,0 +1,76 @@ +package io.github.sakurawald.module.mixin.works; + +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.works.WorksCache; +import io.github.sakurawald.module.initializer.works.work_type.ProductionWork; +import io.github.sakurawald.module.initializer.works.work_type.Work; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.HashSet; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.block.entity.HopperBlockEntity; +import net.minecraft.block.entity.LootableContainerBlockEntity; +import net.minecraft.entity.vehicle.HopperMinecartEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; + +// the priority of carpet's is 1000 +@Mixin(value = HopperBlockEntity.class, priority = 999) + +public abstract class HopperBlockEntityMixin extends LootableContainerBlockEntity { + + protected HopperBlockEntityMixin(BlockEntityType blockEntityType, BlockPos blockPos, BlockState blockState) { + super(blockEntityType, blockPos, blockState); + } + + @Inject(method = "transfer(Lnet/minecraft/inventory/Inventory;Lnet/minecraft/inventory/Inventory;Lnet/minecraft/item/ItemStack;ILnet/minecraft/util/math/Direction;)Lnet/minecraft/item/ItemStack;", at = @At(value = "INVOKE", target = "Lnet/minecraft/inventory/Inventory;setStack(ILnet/minecraft/item/ItemStack;)V", shift = At.Shift.AFTER)) + private static void $ifHopperHasEmptySlot(Inventory container, Inventory container2, ItemStack itemStack, int i, Direction direction, CallbackInfoReturnable cir) { + count(container, container2, itemStack, direction, cir); + } + + + @Inject(method = "transfer(Lnet/minecraft/inventory/Inventory;Lnet/minecraft/inventory/Inventory;Lnet/minecraft/item/ItemStack;ILnet/minecraft/util/math/Direction;)Lnet/minecraft/item/ItemStack;", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;decrement(I)V", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD) + private static void $ifHopperHasMergableSlot(Inventory container, Inventory container2, ItemStack itemStack, int i, Direction direction, CallbackInfoReturnable cir, ItemStack itemStack2, boolean bl, boolean bl2, int j, int k) { + // Note: here we must copy the itemstack before ItemStack#shrink. + // If the count of itemStack is shark to 0, then it may become AIR, and then we can't count it any more. + ItemStack copy = itemStack.copy(); + copy.setCount(k); + + count(container, container2, copy, direction, cir); + } + + @SuppressWarnings("unused") + @Unique + private static void count(Inventory container, Inventory container2, ItemStack itemStack, Direction direction, CallbackInfoReturnable cir) { + // note: if the container == null, then means it's the source-hopper + if (container != null) return; + if (itemStack.isEmpty()) return; + + // If this hopper is a work's hopper, then we exclude it from carpet hoppers + HashSet works; + if (container2 instanceof HopperBlockEntity hb) { + works = WorksCache.getBlockpos2works().get(hb.getPos()); + } else if (container2 instanceof HopperMinecartEntity mh) { + works = WorksCache.getEntity2works().get(mh.getId()); + } else { + Fuji.LOGGER.warn("addItem() found an unknown container: {}", container2); + return; + } + if (works == null) return; + // count this itemstack for all works that contain this blockpos + works.forEach(work -> { + if (work instanceof ProductionWork pwork) { + pwork.addCounter(itemStack); + } + }); + } + +} diff --git a/src/main/java/io/github/sakurawald/module/mixin/zero_command_permission/CommandNodeAccessor.java b/src/main/java/io/github/sakurawald/module/mixin/zero_command_permission/CommandNodeAccessor.java new file mode 100644 index 000000000..4ca3ffa95 --- /dev/null +++ b/src/main/java/io/github/sakurawald/module/mixin/zero_command_permission/CommandNodeAccessor.java @@ -0,0 +1,17 @@ +package io.github.sakurawald.module.mixin.zero_command_permission; + +import com.mojang.brigadier.tree.CommandNode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.function.Predicate; + +@Mixin(value = CommandNode.class, remap = false) +public interface CommandNodeAccessor { + + @Accessor + @Mutable + void setRequirement(Predicate predicate); + +} diff --git a/src/main/java/io/github/sakurawald/util/CarpetUtil.java b/src/main/java/io/github/sakurawald/util/CarpetUtil.java new file mode 100644 index 000000000..7f1bc2005 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/CarpetUtil.java @@ -0,0 +1,11 @@ +package io.github.sakurawald.util; + +import lombok.experimental.UtilityClass; +import net.minecraft.server.network.ServerPlayerEntity; + +@UtilityClass +public class CarpetUtil { + public static boolean isFakePlayer(ServerPlayerEntity player) { + return player.getClass() != ServerPlayerEntity.class; + } +} diff --git a/src/main/java/io/github/sakurawald/util/CommandUtil.java b/src/main/java/io/github/sakurawald/util/CommandUtil.java new file mode 100644 index 000000000..c635950d0 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/CommandUtil.java @@ -0,0 +1,49 @@ +package io.github.sakurawald.util; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.module.initializer.seen.GameProfileCacheEx; +import lombok.experimental.UtilityClass; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.UserCache; + +import static net.minecraft.server.command.CommandManager.argument; + +@UtilityClass +public class CommandUtil { + public static RequiredArgumentBuilder offlinePlayerArgument(String argumentName) { + return argument(argumentName, StringArgumentType.string()) + .suggests((context, builder) -> { + UserCache gameProfileCache = Fuji.SERVER.getUserCache(); + if (gameProfileCache != null) { + ((GameProfileCacheEx) gameProfileCache).fuji$getNames().forEach(builder::suggest); + } + return builder.buildFuture(); + } + ); + } + + + public static RequiredArgumentBuilder offlinePlayerArgument() { + return offlinePlayerArgument("player"); + } + + public static int playerOnlyCommand(CommandContext ctx, PlayerOnlyCommandFunction function) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) { + MessageUtil.sendMessage(ctx.getSource(), "command.player_only"); + return Command.SINGLE_SUCCESS; + } + + return function.run(player); + } + + @FunctionalInterface + public interface PlayerOnlyCommandFunction { + int run(ServerPlayerEntity player); + } +} diff --git a/src/main/java/io/github/sakurawald/util/DateUtil.java b/src/main/java/io/github/sakurawald/util/DateUtil.java new file mode 100644 index 000000000..b6801a5f0 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/DateUtil.java @@ -0,0 +1,15 @@ +package io.github.sakurawald.util; + +import lombok.experimental.UtilityClass; + +import java.text.SimpleDateFormat; + +@UtilityClass +public class DateUtil { + + private static final SimpleDateFormat STANDARD_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static String toStandardDateFormat(long timeMillis) { + return STANDARD_DATE_FORMAT.format(timeMillis); + } +} diff --git a/src/main/java/io/github/sakurawald/util/GuiUtil.java b/src/main/java/io/github/sakurawald/util/GuiUtil.java new file mode 100644 index 000000000..7221486dc --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/GuiUtil.java @@ -0,0 +1,13 @@ +package io.github.sakurawald.util; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class GuiUtil { + public static final String PREVIOUS_PAGE_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzM3NjQ4YWU3YTU2NGE1Mjg3NzkyYjA1ZmFjNzljNmI2YmQ0N2Y2MTZhNTU5Y2U4YjU0M2U2OTQ3MjM1YmNlIn19fQ=="; + public static final String PLUS_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2VkZDIwYmU5MzUyMDk0OWU2Y2U3ODlkYzRmNDNlZmFlYjI4YzcxN2VlNmJmY2JiZTAyNzgwMTQyZjcxNiJ9fX0="; + public static final String HEART_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzM2ZmViZWNhN2M0ODhhNjY3MWRjMDcxNjU1ZGRlMmExYjY1YzNjY2IyMGI2ZThlYWY5YmZiMDhlNjRiODAifX19"; + public static final String A_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTY3ZDgxM2FlN2ZmZTViZTk1MWE0ZjQxZjJhYTYxOWE1ZTM4OTRlODVlYTVkNDk4NmY4NDk0OWM2M2Q3NjcyZSJ9fX0="; + public static final String QUESTION_MARK_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGNlYzg1YmM4MDYxYmRhM2UxZDQ5Zjc1NDQ2NDllNjVjODI3MmNhNTZmNzJkODM4Y2FmMmNjNDgxNmI2OSJ9fX0="; + public static final String NEXT_PAGE_ICON = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWE0ZjY4YzhmYjI3OWU1MGFiNzg2ZjlmYTU0Yzg4Y2E0ZWNmZTFlYjVmZDVmMGMzOGM1NGM5YjFjNzIwM2Q3YSJ9fX0="; +} diff --git a/src/main/java/io/github/sakurawald/util/HttpUtil.java b/src/main/java/io/github/sakurawald/util/HttpUtil.java new file mode 100644 index 000000000..e77a1e389 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/HttpUtil.java @@ -0,0 +1,39 @@ +package io.github.sakurawald.util; + +import lombok.experimental.UtilityClass; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import static io.github.sakurawald.Fuji.LOGGER; + +@UtilityClass +public class HttpUtil { + public static String post(URI uri, String param) throws IOException { + LOGGER.debug("post() -> uri = {}, param = {}", uri, param); + + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("User-Agent", "Fuji"); + connection.setDoOutput(true); + connection.setDoInput(true); + + IOUtils.write(param.getBytes(StandardCharsets.UTF_8), connection.getOutputStream()); + return IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8); + } + + public static String get(URI uri) throws IOException { + LOGGER.debug("get() -> uri = {}", uri); + + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setDoOutput(true); + + return IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/io/github/sakurawald/util/LogUtil.java b/src/main/java/io/github/sakurawald/util/LogUtil.java new file mode 100644 index 000000000..39645d979 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/LogUtil.java @@ -0,0 +1,22 @@ +package io.github.sakurawald.util; + +import io.github.sakurawald.Fuji; +import lombok.experimental.UtilityClass; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; + +@UtilityClass +public class LogUtil { + public static Logger createLogger(String name) { + Logger logger = LogManager.getLogger(name); + try { + String level = System.getProperty("%s.level".formatted(Fuji.MOD_ID)); + Configurator.setLevel(logger, Level.getLevel(level)); + } catch (Exception e) { + return logger; + } + return logger; + } +} diff --git a/src/main/java/io/github/sakurawald/util/MessageUtil.java b/src/main/java/io/github/sakurawald/util/MessageUtil.java new file mode 100644 index 000000000..48278f5fe --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/MessageUtil.java @@ -0,0 +1,152 @@ +package io.github.sakurawald.util; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.github.sakurawald.Fuji; +import io.github.sakurawald.config.handler.ResourceConfigHandler; +import lombok.Getter; +import lombok.experimental.UtilityClass; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.platform.fabric.FabricServerAudiences; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import org.apache.commons.io.FileUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@UtilityClass + +public class MessageUtil { + private static final FabricServerAudiences adventure = FabricServerAudiences.of(Fuji.SERVER); + @Getter + private static final Map player2lang = new HashMap<>(); + @Getter + private static final Map lang2json = new HashMap<>(); + private static final String DEFAULT_LANG = "en_us"; + private static final MiniMessage miniMessageParser = MiniMessage.builder().build(); + + static { + copyLanguageFiles(); + } + + public static void copyLanguageFiles() { + new ResourceConfigHandler("lang/en_us.json").loadFromDisk(); + new ResourceConfigHandler("lang/zh_cn.json").loadFromDisk(); + } + + public static void loadLanguageIfAbsent(String lang) { + if (lang2json.containsKey(lang)) return; + + InputStream is; + try { + is = FileUtils.openInputStream(Fuji.CONFIG_PATH.resolve("lang").resolve(lang + ".json").toFile()); + JsonObject jsonObject = JsonParser.parseReader(new InputStreamReader(is)).getAsJsonObject(); + lang2json.put(lang, jsonObject); + Fuji.LOGGER.info("Language {} loaded.", lang); + } catch (IOException e) { + Fuji.LOGGER.debug("One of your player is using a language '{}' that is missing -> fallback to default language for this player", lang); + } + + if (!lang2json.containsKey(DEFAULT_LANG)) loadLanguageIfAbsent(DEFAULT_LANG); + } + + + public static String ofString(Audience audience, String key, Object... args) { + + /* get player */ + ServerPlayerEntity player; + if (audience instanceof ServerPlayerEntity) player = (ServerPlayerEntity) audience; + else if (audience instanceof ServerCommandSource source && source.getPlayer() != null) + player = source.getPlayer(); + else player = null; + + /* get lang */ + String lang; + if (player != null) { + lang = player2lang.getOrDefault(player.getGameProfile().getName(), DEFAULT_LANG); + } else { + lang = DEFAULT_LANG; + } + + loadLanguageIfAbsent(lang); + + /* get json */ + JsonObject json; + json = lang2json.get(!lang2json.containsKey(lang) ? DEFAULT_LANG : lang); + if (!json.has(key)) { + Fuji.LOGGER.warn("Language {} miss key '{}' -> fallback to default language for this key", lang, key); + json = lang2json.get(DEFAULT_LANG); + } + + /* get value */ + String value; + value = json.get(key).getAsString(); + return formatString(value, args); + } + + public static String formatString(String string, Object... args) { + if (args.length > 0) { + return String.format(string, args); + } + return string; + } + + public static Component ofComponent(Audience audience, String key, Object... args) { + //note: if call ofString() directly with args, then we pass args to ofString(), + // or else we pass args to ofComponent() to avoid args being formatted twice + return ofComponent(ofString(audience, key), args); + } + + public static Component ofComponent(String str, Object... args) { + return miniMessageParser.deserialize(formatString(str, args)); + } + + public static net.minecraft.text.Text ofVomponent(String str, Object... args) { + return toVomponent(ofComponent(str, args)); + } + + public static net.minecraft.text.Text ofVomponent(Audience audience, String key, Object... args) { + return toVomponent(ofComponent(audience, key, args)); + } + + public static net.minecraft.text.Text toVomponent(Component component) { + return adventure.toNative(component); + } + + public static List ofVomponents(Audience audience, String key, Object... args) { + String lines = ofString(audience, key, args); + + List ret = new ArrayList<>(); + for (String line : lines.split("\n")) { + ret.add(ofVomponent(line)); + } + return ret; + } + + public static void sendMessage(Audience audience, String key, Object... args) { + audience.sendMessage(ofComponent(audience, key, args)); + } + + public static void sendActionBar(Audience audience, String key, Object... args) { + audience.sendActionBar(ofComponent(audience, key, args)); + } + + public static void sendBroadcast(String key, Object... args) { + // fix: log broadcast for console + Fuji.LOGGER.info(PlainTextComponentSerializer.plainText().serialize(ofComponent(null, key, args))); + + for (ServerPlayerEntity player : Fuji.SERVER.getPlayerManager().getPlayerList()) { + sendMessage(player, key, args); + } + } + +} diff --git a/src/main/java/io/github/sakurawald/util/ScheduleUtil.java b/src/main/java/io/github/sakurawald/util/ScheduleUtil.java new file mode 100644 index 000000000..17a919472 --- /dev/null +++ b/src/main/java/io/github/sakurawald/util/ScheduleUtil.java @@ -0,0 +1,129 @@ +package io.github.sakurawald.util; + + +import io.github.sakurawald.config.Configs; +import lombok.Getter; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Configurator; +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; +import org.quartz.impl.matchers.GroupMatcher; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +import static io.github.sakurawald.Fuji.LOGGER; + + +public class ScheduleUtil { + + public static final String CRON_EVERY_MINUTE = "0 * * ? * * *"; + @Getter + private static final Scheduler scheduler; + + static { + /* set logger level for quartz */ + Level level = Level.getLevel(Configs.configHandler.model().common.quartz.logger_level); + Configurator.setAllLevels("org.quartz", level); + + /* new scheduler */ + try { + scheduler = new StdSchedulerFactory().getScheduler(); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + } + + public static void addJob(Class jobClass, String jobName, String jobGroup, String cron, JobDataMap jobDataMap) { + if (jobName == null) { + jobName = UUID.randomUUID().toString(); + } + if (jobGroup == null) { + jobGroup = jobClass.getName(); + } + LOGGER.debug("addJob() -> jobClass: {}, jobName: {}, jobGroup: {}, cron: {}, jobDataMap: {}", jobClass, jobName, jobGroup, cron, jobDataMap); + + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).usingJobData(jobDataMap).build(); + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).withSchedule(CronScheduleBuilder.cronSchedule(cron)).build(); + try { + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.addJob", e); + } + } + + public static void addJob(Class jobClass, String jobName, String jobGroup, int intervalMs, int repeatCount, JobDataMap jobDataMap) { + if (jobName == null) { + jobName = UUID.randomUUID().toString(); + } + if (jobGroup == null) { + jobGroup = jobClass.getName(); + } + LOGGER.debug("addJob() -> jobClass: {}, jobName: {}, jobGroup: {}, intervalMs: {}, repeatCount: {}, jobDataMap: {}", jobClass, jobName, jobGroup, intervalMs, repeatCount, jobDataMap); + + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).usingJobData(jobDataMap).build(); + SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMilliseconds(intervalMs).withRepeatCount(repeatCount - 1)).build(); + try { + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.addJob", e); + } + } + + public static void removeJobs(String jobGroup, String jobName) { + LOGGER.debug("removeJobs() -> jobGroup: {}, jobName: {}", jobGroup, jobName); + + try { + scheduler.deleteJob(new JobKey(jobName, jobGroup)); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.removeJobs", e); + } + } + + public static void removeJobs(String jobGroup) { + LOGGER.debug("removeJobs() -> jobGroup: {}", jobGroup); + + try { + scheduler.deleteJobs(getJobKeys(jobGroup).stream().toList()); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.removeJobs", e); + } + } + + private static Set getJobKeys(String jobGroup) { + GroupMatcher groupMatcher = GroupMatcher.groupEquals(jobGroup); + try { + return scheduler.getJobKeys(groupMatcher); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.getJobKeys", e); + } + return Collections.emptySet(); + } + + public static void triggerJobs(String jobGroup) { + getJobKeys(jobGroup).forEach(jobKey -> { + try { + scheduler.triggerJob(jobKey); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.triggerJobs", e); + } + }); + } + + public static void startScheduler() { + try { + scheduler.start(); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.startScheduler", e); + } + } + + public static void shutdownScheduler() { + try { + scheduler.shutdown(); + } catch (SchedulerException e) { + LOGGER.error("Exception in ScheduleUtil.shutdownScheduler", e); + } + } +}