From e4337b9e125f16a3ef96a27d12e25a69aeaca2ce Mon Sep 17 00:00:00 2001 From: Caadiq Date: Thu, 18 Dec 2025 21:47:11 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A8=B8=EB=A6=AC=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/beemer/essentials/Essentials.kt | 1 + .../beemer/essentials/command/HeadCommand.kt | 153 ++++++++++++++++++ .../beemer/essentials/command/SpawnCommand.kt | 127 +++++++++------ .../essentials/config/CoordinateConfig.kt | 13 +- .../beemer/essentials/config/PlayerConfig.kt | 15 ++ .../com/beemer/essentials/gui/AntimobGui.kt | 94 ++++++----- .../essentials/nickname/NicknameDataStore.kt | 8 + 7 files changed, 312 insertions(+), 99 deletions(-) create mode 100644 Essentials/src/main/kotlin/com/beemer/essentials/command/HeadCommand.kt diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/Essentials.kt b/Essentials/src/main/kotlin/com/beemer/essentials/Essentials.kt index bf620ca..a2a8df5 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/Essentials.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/Essentials.kt @@ -31,5 +31,6 @@ class Essentials(modEventBus: IEventBus) { NeoForge.EVENT_BUS.register(AntimobCommand) NeoForge.EVENT_BUS.register(CoordinateCommand) NeoForge.EVENT_BUS.register(NicknameCommand) + NeoForge.EVENT_BUS.register(HeadCommand) } } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/HeadCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/HeadCommand.kt new file mode 100644 index 0000000..c5181fc --- /dev/null +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/HeadCommand.kt @@ -0,0 +1,153 @@ +package com.beemer.essentials.command + +import com.beemer.essentials.config.PlayerConfig +import com.beemer.essentials.nickname.NicknameDataStore +import com.mojang.brigadier.arguments.StringArgumentType +import java.util.Optional +import java.util.UUID +import net.minecraft.ChatFormatting +import net.minecraft.commands.Commands +import net.minecraft.commands.SharedSuggestionProvider +import net.minecraft.core.component.DataComponents +import net.minecraft.network.chat.Component +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.item.component.ResolvableProfile +import net.neoforged.bus.api.SubscribeEvent +import net.neoforged.neoforge.event.RegisterCommandsEvent + +object HeadCommand { + @SubscribeEvent + fun onRegisterCommands(event: RegisterCommandsEvent) { + // /머리 <플레이어> - 해당 플레이어 머리 아이템 지급 + listOf("머리", "head").forEach { command -> + event.dispatcher.register( + Commands.literal(command) + .then( + Commands.argument( + "플레이어", + StringArgumentType.greedyString() + ) + .suggests { _, builder -> + // 서버에 접속한 적 있는 플레이어만 제안 + // 닉네임이 있으면 닉네임, 없으면 원래 이름 + val suggestions = + mutableListOf() + PlayerConfig.getAllPlayers() + .forEach { (uuid, name) -> + val nickname = + NicknameDataStore + .getNickname( + UUID.fromString( + uuid + ) + ) + suggestions.add( + nickname + ?: name + ) + } + SharedSuggestionProvider.suggest( + suggestions, + builder + ) + } + .executes { context -> + val player = + context.source.entity as? + ServerPlayer + ?: return@executes 0 + val targetName = + StringArgumentType + .getString( + context, + "플레이어" + ) + givePlayerHead(player, targetName) + } + ) + .executes { context -> + // 인자 없이 실행 시 자신의 머리 + val player = + context.source.entity as? ServerPlayer + ?: return@executes 0 + givePlayerHead(player, player.gameProfile.name) + } + ) + } + } + + private fun givePlayerHead(player: ServerPlayer, targetName: String): Int { + // 1. 원래 이름으로 UUID 찾기 + var targetUuid = PlayerConfig.getUuidByName(targetName) + var realName = targetName + + // 2. 못 찾으면 닉네임으로 UUID 찾기 + if (targetUuid == null) { + val uuidByNickname = NicknameDataStore.getUuidByNickname(targetName) + if (uuidByNickname != null) { + targetUuid = uuidByNickname.toString() + } + } + + // 3. 서버에 접속한 적 없는 플레이어면 거부 + if (targetUuid == null || !PlayerConfig.isKnownPlayer(targetUuid)) { + player.sendSystemMessage( + Component.literal("해당 플레이어는 서버에 접속한 적이 없습니다.").withStyle { + it.withColor(ChatFormatting.RED) + } + ) + return 0 + } + + // 실제 마인크래프트 이름 가져오기 + val allPlayers = PlayerConfig.getAllPlayers() + realName = allPlayers[targetUuid] ?: targetName + + // 표시용 이름 (닉네임 우선) + val displayName = + NicknameDataStore.getNickname(UUID.fromString(targetUuid)) ?: realName + + // 머리 아이템 생성 + val headItem = ItemStack(Items.PLAYER_HEAD) + val resolvableProfile = + ResolvableProfile( + Optional.of(realName), + Optional.empty(), + com.mojang.authlib.properties.PropertyMap() + ) + + headItem.set(DataComponents.PROFILE, resolvableProfile) + headItem.set( + DataComponents.CUSTOM_NAME, + Component.literal("${displayName}의 머리").withStyle { + it.withColor(ChatFormatting.GOLD) + } + ) + + // 인벤토리에 추가 + val added = player.inventory.add(headItem) + + if (added) { + player.sendSystemMessage( + Component.literal(displayName) + .withStyle { it.withColor(ChatFormatting.GREEN) } + .append( + Component.literal("의 머리가 지급되었습니다.").withStyle { + it.withColor(ChatFormatting.GOLD) + } + ) + ) + } else { + // 인벤토리가 가득 찬 경우 바닥에 드랍 + player.drop(headItem, false) + player.sendSystemMessage( + Component.literal("인벤토리가 가득 차서 ${displayName}의 머리를 바닥에 떨어뜨렸습니다.") + .withStyle { it.withColor(ChatFormatting.YELLOW) } + ) + } + + return 1 + } +} diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt index 6c9af03..5cf5517 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt @@ -16,78 +16,105 @@ object SpawnCommand { fun onRegisterCommands(event: RegisterCommandsEvent) { listOf("setspawn", "스폰설정").forEach { command -> event.dispatcher.register( - Commands.literal(command).executes { context -> - val player = CommandUtils.getPlayerOrSendFailure(context.source) ?: return@executes 0 + Commands.literal(command).executes { context -> + val player = + CommandUtils.getPlayerOrSendFailure(context.source) + ?: return@executes 0 - val playerLocation = player.blockPosition() - val dimensionId = player.level().dimension().location().toString() - val biomeId = player.level().getBiome(playerLocation) - .unwrapKey() - .map { it.location().toString() } - .orElse("minecraft:plains") + val exactPos = player.position() + val blockPos = player.blockPosition() + val dimensionId = player.level().dimension().location().toString() + val biomeId = + player.level() + .getBiome(blockPos) + .unwrapKey() + .map { it.location().toString() } + .orElse("minecraft:plains") - val location = Location( - dimension = dimensionId, - biome = biomeId, - x = playerLocation.x.toDouble(), - y = playerLocation.y.toDouble(), - z = playerLocation.z.toDouble() - ) + val location = + Location( + dimension = dimensionId, + biome = biomeId, + x = exactPos.x, + y = exactPos.y, + z = exactPos.z + ) - SpawnConfig.setCustomSpawn(location) + SpawnConfig.setCustomSpawn(location) - player.sendSystemMessage(Component.literal("스폰 지점이 현재 위치로 설정되었습니다.").withStyle { it.withColor(ChatFormatting.GOLD) }) - 1 - } + player.sendSystemMessage( + Component.literal("스폰 지점이 현재 위치로 설정되었습니다.").withStyle { + it.withColor(ChatFormatting.GOLD) + } + ) + 1 + } ) } listOf("spawn", "스폰", "넴주").forEach { command -> event.dispatcher.register( - Commands.literal(command).executes { context -> - val player = CommandUtils.getPlayerOrSendFailure(context.source) ?: return@executes 0 + Commands.literal(command).executes { context -> + val player = + CommandUtils.getPlayerOrSendFailure(context.source) + ?: return@executes 0 - val target = SpawnConfig.getCustomSpawn() ?: SpawnConfig.getDefaultSpawn() + val target = SpawnConfig.getCustomSpawn() ?: SpawnConfig.getDefaultSpawn() - target?.let { t -> - val level = DimensionUtils.getLevelById(player.server, t.dimension) + target?.let { t -> + val level = DimensionUtils.getLevelById(player.server, t.dimension) - level?.let { l -> - val currentPos = player.blockPosition() - val currentDimension = player.level().dimension().location().toString() - val currentBiome = player.level().getBiome(currentPos) - .unwrapKey() - .map { it.location().toString() } - .orElse("minecraft:plains") + level?.let { l -> + val exactPos = player.position() + val blockPos = player.blockPosition() + val currentDimension = + player.level().dimension().location().toString() + val currentBiome = + player.level() + .getBiome(blockPos) + .unwrapKey() + .map { it.location().toString() } + .orElse("minecraft:plains") - val currentLocation = Location( - dimension = currentDimension, - biome = currentBiome, - x = currentPos.x.toDouble(), - y = currentPos.y.toDouble(), - z = currentPos.z.toDouble() - ) + val currentLocation = + Location( + dimension = currentDimension, + biome = currentBiome, + x = exactPos.x, + y = exactPos.y, + z = exactPos.z + ) - PlayerConfig.recordLastLocation(player, currentLocation) - player.teleportTo(l, t.x, t.y, t.z, player.yRot, player.xRot) - player.sendSystemMessage(Component.literal("스폰으로 이동했습니다.").withStyle { it.withColor(ChatFormatting.GOLD) }) + PlayerConfig.recordLastLocation(player, currentLocation) + player.teleportTo(l, t.x, t.y, t.z, player.yRot, player.xRot) + player.sendSystemMessage( + Component.literal("스폰으로 이동했습니다.").withStyle { + it.withColor(ChatFormatting.GOLD) + } + ) + } } + 1 } - 1 - } ) } listOf("delspawn", "스폰삭제").forEach { command -> event.dispatcher.register( - Commands.literal(command).executes { context -> - val player = CommandUtils.getPlayerOrSendFailure(context.source) ?: return@executes 0 + Commands.literal(command).executes { context -> + val player = + CommandUtils.getPlayerOrSendFailure(context.source) + ?: return@executes 0 - SpawnConfig.removeCustomSpawn() - player.sendSystemMessage(Component.literal("스폰 지점이 기본 스폰 지점으로 변경되었습니다.").withStyle { it.withColor(ChatFormatting.GOLD) }) - 1 - } + SpawnConfig.removeCustomSpawn() + player.sendSystemMessage( + Component.literal("스폰 지점이 기본 스폰 지점으로 변경되었습니다.").withStyle { + it.withColor(ChatFormatting.GOLD) + } + ) + 1 + } ) } } -} \ No newline at end of file +} diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/config/CoordinateConfig.kt b/Essentials/src/main/kotlin/com/beemer/essentials/config/CoordinateConfig.kt index fe2187a..c7d01e4 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/config/CoordinateConfig.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/config/CoordinateConfig.kt @@ -71,16 +71,9 @@ object CoordinateConfig { val dimension = tag.getString("dimension") val biome = tag.getString("biome") - // 하위 호환: Int로 저장된 경우 Double로 변환 - val x = - if (tag.contains("x", 6)) tag.getDouble("x") - else tag.getInt("x").toDouble() + 0.5 - val y = - if (tag.contains("y", 6)) tag.getDouble("y") - else tag.getInt("y").toDouble() - val z = - if (tag.contains("z", 6)) tag.getDouble("z") - else tag.getInt("z").toDouble() + 0.5 + val x = tag.getDouble("x") + val y = tag.getDouble("y") + val z = tag.getDouble("z") val creatorUuid = if (tag.contains("creatorUuid")) tag.getString("creatorUuid") diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/config/PlayerConfig.kt b/Essentials/src/main/kotlin/com/beemer/essentials/config/PlayerConfig.kt index d4a335e..d2cab6d 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/config/PlayerConfig.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/config/PlayerConfig.kt @@ -142,4 +142,19 @@ object PlayerConfig { val uuid = player.uuid.toString() return players[uuid] } + + /** 모든 저장된 플레이어 목록 (uuid -> name) */ + fun getAllPlayers(): Map { + return players.mapValues { it.value.name } + } + + /** 이름으로 UUID 찾기 */ + fun getUuidByName(name: String): String? { + return players.entries.find { it.value.name.equals(name, ignoreCase = true) }?.key + } + + /** UUID가 저장된 플레이어인지 확인 */ + fun isKnownPlayer(uuid: String): Boolean { + return players.containsKey(uuid) + } } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt b/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt index 4c3f2a7..9adf8ea 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt @@ -1,7 +1,6 @@ package com.beemer.essentials.gui import com.beemer.essentials.config.AntimobConfig -import com.mojang.authlib.GameProfile import net.minecraft.ChatFormatting import net.minecraft.core.component.DataComponents import net.minecraft.network.chat.Component @@ -15,9 +14,13 @@ import net.minecraft.world.inventory.Slot import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import net.minecraft.world.item.component.ResolvableProfile -import java.util.* -class AntimobGui(syncId: Int, playerInv: Inventory, val container: Container, private val viewer: ServerPlayer) : AbstractContainerMenu(MenuType.GENERIC_9x2, syncId) { +class AntimobGui( + syncId: Int, + playerInv: Inventory, + val container: Container, + viewer: ServerPlayer +) : AbstractContainerMenu(MenuType.GENERIC_9x2, syncId) { companion object { const val CONTAINER_COLUMNS = 9 const val CONTAINER_ROWS = 2 @@ -38,10 +41,14 @@ class AntimobGui(syncId: Int, playerInv: Inventory, val container: Container, pr for (i in 0 until CONTAINER_SIZE) { val x = LEFT_PADDING + (i % CONTAINER_COLUMNS) * SLOT_SIZE val y = TOP_PADDING + (i / CONTAINER_COLUMNS) * SLOT_SIZE - addSlot(object : Slot(container, i, x, y) { - override fun mayPickup(player: net.minecraft.world.entity.player.Player): Boolean = false - override fun mayPlace(stack: ItemStack): Boolean = false - }) + addSlot( + object : Slot(container, i, x, y) { + override fun mayPickup( + player: net.minecraft.world.entity.player.Player + ): Boolean = false + override fun mayPlace(stack: ItemStack): Boolean = false + } + ) } for (row in 0 until 3) { @@ -62,7 +69,12 @@ class AntimobGui(syncId: Int, playerInv: Inventory, val container: Container, pr refreshContainerContents() } - override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: net.minecraft.world.entity.player.Player) { + override fun clicked( + slotId: Int, + button: Int, + clickType: ClickType, + player: net.minecraft.world.entity.player.Player + ) { if (player is ServerPlayer && slotId in 0 until CONTAINER_SIZE) { if (glassSlotToMob.containsKey(slotId)) { val mob = glassSlotToMob[slotId] ?: return @@ -77,51 +89,55 @@ class AntimobGui(syncId: Int, playerInv: Inventory, val container: Container, pr override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true - override fun quickMoveStack(player: net.minecraft.world.entity.player.Player, index: Int): ItemStack = ItemStack.EMPTY + override fun quickMoveStack( + player: net.minecraft.world.entity.player.Player, + index: Int + ): ItemStack = ItemStack.EMPTY - private fun makeMobHead(mob: String, player: ServerPlayer): ItemStack { + private fun makeMobHead(mob: String): ItemStack { val headItem = ItemStack(Items.PLAYER_HEAD) - val uuid = when (mob) { - "크리퍼" -> UUID.fromString("057b1c47-1321-4863-a6fe-8887f9ec265f") - "가스트" -> UUID.fromString("063085a6-797f-4785-be1a-21cd7580f752") - "엔더맨" -> UUID.fromString("40ffb372-12f6-4678-b3f2-2176bf56dd4b") - else -> UUID.fromString("c06f8906-4c8a-4911-9c29-ea1dbd1aab82") - } + // MHF_ 스킨 이름 사용 (네트워크 요청 없음) + val skinName = + when (mob) { + "크리퍼" -> "MHF_Creeper" + "가스트" -> "MHF_Ghast" + "엔더맨" -> "MHF_Enderman" + else -> "MHF_Steve" + } - val filledProfile: GameProfile = try { - player.server.sessionService.fetchProfile(uuid, true)?.profile ?: player.gameProfile - } catch (_: Exception) { - player.gameProfile - } + val resolvableProfile = + ResolvableProfile( + java.util.Optional.of(skinName), + java.util.Optional.empty(), + com.mojang.authlib.properties.PropertyMap() + ) - val resolvableProfile = try { - ResolvableProfile(filledProfile) - } catch (_: NoSuchMethodError) { - val ctor = ResolvableProfile::class.java.getDeclaredConstructor(GameProfile::class.java) - ctor.isAccessible = true - ctor.newInstance(filledProfile) - } - - headItem.set(DataComponents.CUSTOM_NAME, Component.literal(mob).withStyle { it.withColor(ChatFormatting.YELLOW) }) + headItem.set( + DataComponents.CUSTOM_NAME, + Component.literal(mob).withStyle { it.withColor(ChatFormatting.YELLOW) } + ) headItem.set(DataComponents.PROFILE, resolvableProfile) return headItem } private fun makeToggleGlass(mob: String): ItemStack { val enabled = AntimobConfig.get(mob) - val glass = if (enabled) ItemStack(Items.GREEN_STAINED_GLASS_PANE) else ItemStack(Items.RED_STAINED_GLASS_PANE) + val glass = + if (enabled) ItemStack(Items.GREEN_STAINED_GLASS_PANE) + else ItemStack(Items.RED_STAINED_GLASS_PANE) val statusText = if (enabled) "활성화" else "비활성화" - glass.set(DataComponents.CUSTOM_NAME, Component.literal(statusText).withStyle { it.withColor(if (enabled) ChatFormatting.GREEN else ChatFormatting.RED) }) + glass.set( + DataComponents.CUSTOM_NAME, + Component.literal(statusText).withStyle { + it.withColor(if (enabled) ChatFormatting.GREEN else ChatFormatting.RED) + } + ) return glass } private fun refreshContainerContents() { - headSlotToMob.forEach { (slot, mob) -> - container.setItem(slot, makeMobHead(mob, viewer)) - } - glassSlotToMob.forEach { (slot, mob) -> - container.setItem(slot, makeToggleGlass(mob)) - } + headSlotToMob.forEach { (slot, mob) -> container.setItem(slot, makeMobHead(mob)) } + glassSlotToMob.forEach { (slot, mob) -> container.setItem(slot, makeToggleGlass(mob)) } } -} \ No newline at end of file +} diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameDataStore.kt b/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameDataStore.kt index 6d03143..755deb2 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameDataStore.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameDataStore.kt @@ -86,4 +86,12 @@ object NicknameDataStore { fun hasNickname(uuid: UUID): Boolean { return nicknames.containsKey(uuid.toString()) } + + /** 닉네임으로 UUID 찾기 */ + fun getUuidByNickname(nickname: String): UUID? { + val target = nickname.trim() + if (target.isEmpty()) return null + val entry = nicknames.entries.find { it.value.equals(target, ignoreCase = true) } + return entry?.key?.let { UUID.fromString(it) } + } }