머리 명령어 추가

This commit is contained in:
Caadiq 2025-12-18 21:47:11 +09:00
parent 727769a170
commit e4337b9e12
7 changed files with 312 additions and 99 deletions

View file

@ -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)
}
}

View file

@ -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<String>()
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<UUID>(),
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
}
}

View file

@ -16,77 +16,104 @@ 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
}
)
}
}

View file

@ -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")

View file

@ -142,4 +142,19 @@ object PlayerConfig {
val uuid = player.uuid.toString()
return players[uuid]
}
/** 모든 저장된 플레이어 목록 (uuid -> name) */
fun getAllPlayers(): Map<String, String> {
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)
}
}

View file

@ -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<java.util.UUID>(),
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)) }
}
}

View file

@ -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) }
}
}