Compare commits

..

No commits in common. "2d42669545df936579427410efc1651ea06b618a" and "695bc79ca43f9d2594d489b16b518f68fba2555e" have entirely different histories.

25 changed files with 527 additions and 1208 deletions

View file

@ -1,37 +0,0 @@
package com.beemer.essentials.mixin;
import com.beemer.essentials.event.MenuKeyHandler;
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
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;
/**
* 바꾸기 패킷 가로채기
* Shift + F 조합으로 메뉴 GUI 열기
*/
@Mixin(ServerGamePacketListenerImpl.class)
public class ServerGamePacketListenerImplMixin {
@Shadow
public ServerPlayer player;
@Inject(method = "handlePlayerAction", at = @At("HEAD"), cancellable = true)
private void onHandlePlayerAction(ServerboundPlayerActionPacket packet, CallbackInfo ci) {
// 바꾸기 액션인지 확인
if (packet.getAction() == ServerboundPlayerActionPacket.Action.SWAP_ITEM_WITH_OFFHAND) {
// Shift를 누르고 있으면 메뉴 열기
if (player.isShiftKeyDown()) {
// 패킷 처리 취소 (실제 바꾸기 방지)
ci.cancel();
// 메뉴 GUI 열기
MenuKeyHandler.INSTANCE.openMenuFromPacket(player);
}
}
}
}

View file

@ -33,7 +33,5 @@ class Essentials(modEventBus: IEventBus) {
NeoForge.EVENT_BUS.register(NicknameCommand)
NeoForge.EVENT_BUS.register(HeadCommand)
NeoForge.EVENT_BUS.register(HelpCommand)
NeoForge.EVENT_BUS.register(WorkbenchCommand)
NeoForge.EVENT_BUS.register(MenuCommand)
}
}

View file

@ -2,10 +2,11 @@ package com.beemer.essentials.command
import com.beemer.essentials.config.ChatConfig
import com.beemer.essentials.util.ChatUtils
import com.beemer.essentials.util.MessageUtils
import com.mojang.brigadier.exceptions.CommandSyntaxException
import net.minecraft.ChatFormatting
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
@ -30,7 +31,11 @@ object ChatCommand {
private fun hasPermissionOrSend(player: ServerPlayer?, requiredLevel: Int = 2): Boolean {
if (player != null && !player.hasPermissions(requiredLevel)) {
MessageUtils.sendError(player, "해당 명령어를 실행할 권한이 없습니다.")
player.sendSystemMessage(
Component.literal("해당 명령어를 실행할 권한이 없습니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return false
}
return true
@ -46,15 +51,15 @@ object ChatCommand {
ChatConfig.loadConfig()
if (player != null) {
MessageUtils.sendSuccess(player, "채팅 형식을 새로고침했습니다.")
} else {
context.source.sendSuccess(
{ MessageUtils.success("채팅 형식을 새로고침했습니다.") },
false
)
val success =
Component.literal("채팅 형식을 새로고침했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
if (player == null)
context.source.sendSuccess({ success }, false)
else player.sendSystemMessage(success)
1
}
)
@ -76,7 +81,9 @@ object ChatCommand {
ChatUtils.clearChatForAll(allPlayers)
server.playerList.broadcastSystemMessage(
MessageUtils.info("채팅창을 비웠습니다."),
Component.literal("\u00A0\u00A0채팅창을 비웠습니다.").withStyle {
it.withColor(ChatFormatting.AQUA)
},
false
)

View file

@ -7,9 +7,8 @@ import com.beemer.essentials.data.Location
import com.beemer.essentials.gui.Menu
import com.beemer.essentials.gui.createPageContainer
import com.beemer.essentials.util.DimensionUtils
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import com.mojang.brigadier.arguments.StringArgumentType
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
@ -20,7 +19,7 @@ import net.neoforged.neoforge.event.RegisterCommandsEvent
object CoordinateCommand {
@SubscribeEvent
fun onRegisterCommands(event: RegisterCommandsEvent) {
// /좌표 - GUI 열기
// /좌표 - GUI 열기 (페이지 0부터 시작)
event.dispatcher.register(
Commands.literal("좌표").executes { context ->
val player = context.source.playerOrException as ServerPlayer
@ -29,7 +28,7 @@ object CoordinateCommand {
}
)
// /좌표이동 <장소>
// /좌표이동 <장소> - 바로 이동
event.dispatcher.register(
Commands.literal("좌표이동")
.then(
@ -71,9 +70,16 @@ object CoordinateCommand {
)
if (CoordinateConfig.isExist(name)) {
MessageUtils.sendError(
player,
"{$name} 좌표가 이미 존재합니다."
player.sendSystemMessage(
Component.literal(
"$name 좌표가 이미 존재합니다."
)
.withStyle {
it.withColor(
ChatFormatting
.RED
)
}
)
return@executes 0
}
@ -110,9 +116,16 @@ object CoordinateCommand {
)
CoordinateConfig.addCoordinate(coordinate)
MessageUtils.sendSuccess(
player,
"{$name} 좌표를 추가했습니다."
player.sendSystemMessage(
Component.literal(
"$name 좌표를 추가했습니다."
)
.withStyle {
it.withColor(
ChatFormatting
.GOLD
)
}
)
1
}
@ -142,17 +155,31 @@ object CoordinateCommand {
)
if (!CoordinateConfig.isExist(name)) {
MessageUtils.sendError(
player,
"{$name} 좌표가 존재하지 않습니다."
player.sendSystemMessage(
Component.literal(
"$name 좌표가 존재하지 않습니다."
)
.withStyle {
it.withColor(
ChatFormatting
.RED
)
}
)
return@executes 0
}
CoordinateConfig.removeCoordinate(name)
MessageUtils.sendSuccess(
player,
"{$name} 좌표를 제거했습니다."
player.sendSystemMessage(
Component.literal(
"$name 좌표를 제거했습니다."
)
.withStyle {
it.withColor(
ChatFormatting
.GOLD
)
}
)
1
}
@ -167,22 +194,12 @@ object CoordinateCommand {
else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE
val container = createPageContainer(player, page)
// 버튼 상태에 따른 이미지 선택
val buttonImage =
when {
page == 0 && page >= totalPages - 1 -> "\uE010" // 둘 다 비활성
page == 0 -> "\uE012" // 이전만 비활성
page >= totalPages - 1 -> "\uE011" // 다음만 비활성
else -> "\uE013" // 둘 다 활성
}
// 커스텀 버튼 이미지만 타이틀에 표시 (타이틀 텍스트는 이미지에 포함)
val customTitle = "\uF808$buttonImage"
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ -> Menu(windowId, inv, container, page) },
Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) }
Component.literal("저장된 좌표 (${page + 1}/$totalPages)").withStyle {
it.withBold(true)
}
)
)
}
@ -190,13 +207,21 @@ object CoordinateCommand {
private fun teleportToCoordinate(player: ServerPlayer, name: String): Int {
val coord = CoordinateConfig.getCoordinate(name)
if (coord == null) {
MessageUtils.sendError(player, "{$name} 좌표가 존재하지 않습니다.")
player.sendSystemMessage(
Component.literal("$name 좌표가 존재하지 않습니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return 0
}
val level = DimensionUtils.getLevelById(player.server, coord.dimension)
if (level == null) {
MessageUtils.sendError(player, "존재하지 않는 차원입니다.")
player.sendSystemMessage(
Component.literal("존재하지 않는 차원입니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return 0
}
@ -219,9 +244,11 @@ object CoordinateCommand {
)
player.teleportTo(level, coord.x, coord.y, coord.z, player.yRot, player.xRot)
// 텔레포트 사운드 재생
SoundUtils.playTeleport(player)
MessageUtils.sendSuccess(player, "{$name}(으)로 이동했습니다.")
player.sendSystemMessage(
Component.literal("$name (으)로 이동했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
return 1
}
}

View file

@ -2,7 +2,6 @@ package com.beemer.essentials.command
import com.beemer.essentials.config.PlayerConfig
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.MessageUtils
import com.mojang.brigadier.arguments.StringArgumentType
import java.util.Optional
import java.util.UUID
@ -31,6 +30,8 @@ object HeadCommand {
StringArgumentType.greedyString()
)
.suggests { _, builder ->
// 서버에 접속한 적 있는 플레이어만 제안
// 닉네임이 있으면 닉네임, 없으면 원래 이름
val suggestions =
mutableListOf<String>()
PlayerConfig.getAllPlayers()
@ -92,7 +93,11 @@ object HeadCommand {
// 3. 서버에 접속한 적 없는 플레이어면 거부
if (targetUuid == null || !PlayerConfig.isKnownPlayer(targetUuid)) {
MessageUtils.sendError(player, "해당 플레이어는 서버에 접속한 적이 없습니다.")
player.sendSystemMessage(
Component.literal("해당 플레이어는 서버에 접속한 적이 없습니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return 0
}
@ -125,13 +130,21 @@ object HeadCommand {
val added = player.inventory.add(headItem)
if (added) {
MessageUtils.sendSuccess(player, "{$displayName}의 머리가 지급되었습니다.")
player.sendSystemMessage(
Component.literal(displayName)
.withStyle { it.withColor(ChatFormatting.GREEN) }
.append(
Component.literal("의 머리가 지급되었습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
} else {
// 인벤토리가 가득 찬 경우 바닥에 드랍
player.drop(headItem, false)
MessageUtils.sendWarning(
player,
"인벤토리가 가득 차서 {$displayName}의 머리를 바닥에 떨어뜨렸습니다."
player.sendSystemMessage(
Component.literal("인벤토리가 가득 차서 ${displayName}의 머리를 바닥에 떨어뜨렸습니다.")
.withStyle { it.withColor(ChatFormatting.YELLOW) }
)
}

View file

@ -1,29 +1,13 @@
package com.beemer.essentials.command
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.TextColor
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
object HelpCommand {
// 커스텀 색상 정의
private val COLOR_HEADER = TextColor.fromRgb(0xFFD700) // 골드
private val COLOR_SEPARATOR = TextColor.fromRgb(0x555555) // 어두운 회색
private val COLOR_COMMAND = TextColor.fromRgb(0xFFFFFF) // 흰색
private val COLOR_DESC = TextColor.fromRgb(0xAAAAAA) // 밝은 회색
private val COLOR_DASH = TextColor.fromRgb(0x666666) // 회색
// 카테고리별 색상
private val COLOR_COORDINATE = TextColor.fromRgb(0xFFE066) // 밝은 노랑
private val COLOR_SPAWN = TextColor.fromRgb(0x7FD88C) // 연두색
private val COLOR_TELEPORT = TextColor.fromRgb(0x7DD3FC) // 하늘색
private val COLOR_NICKNAME = TextColor.fromRgb(0xE879F9) // 보라색
private val COLOR_HEAD = TextColor.fromRgb(0xFBBF24) // 주황색
private val COLOR_WORKBENCH = TextColor.fromRgb(0xA3E635) // 라임색
private val COLOR_MENU = TextColor.fromRgb(0xF472B6) // 핑크색
@SubscribeEvent
fun onRegisterCommands(event: RegisterCommandsEvent) {
listOf("도움말", "help", "essentials").forEach { command ->
@ -40,11 +24,15 @@ object HelpCommand {
private fun showHelp(player: ServerPlayer) {
val header =
Component.literal("══════════ ")
.withStyle { it.withColor(COLOR_SEPARATOR) }
.append(Component.literal("도움말").withStyle { it.withColor(COLOR_HEADER) })
.withStyle { it.withColor(ChatFormatting.DARK_GRAY) }
.append(
Component.literal("도움말").withStyle {
it.withColor(ChatFormatting.GOLD).withBold(true)
}
)
.append(
Component.literal(" ══════════").withStyle {
it.withColor(COLOR_SEPARATOR)
it.withColor(ChatFormatting.DARK_GRAY)
}
)
@ -52,7 +40,7 @@ object HelpCommand {
player.sendSystemMessage(Component.empty())
// 좌표 관리
sendCategory(player, "좌표 관리", COLOR_COORDINATE)
sendCategory(player, "좌표 관리", ChatFormatting.YELLOW)
sendCommand(player, "/좌표", "저장된 좌표 목록 (GUI)")
sendCommand(player, "/좌표추가 <이름>", "현재 위치 저장")
sendCommand(player, "/좌표이동 <이름>", "해당 좌표로 이동")
@ -60,61 +48,66 @@ object HelpCommand {
player.sendSystemMessage(Component.empty())
// 스폰
sendCategory(player, "스폰", COLOR_SPAWN)
sendCategory(player, "스폰", ChatFormatting.GREEN)
sendCommand(player, "/스폰", "스폰으로 이동")
sendCommand(player, "/스폰설정", "현재 위치를 스폰으로 설정")
sendCommand(player, "/스폰삭제", "커스텀 스폰 삭제")
player.sendSystemMessage(Component.empty())
// 텔레포트
sendCategory(player, "텔레포트", COLOR_TELEPORT)
sendCategory(player, "텔레포트", ChatFormatting.AQUA)
sendCommand(player, "/tpa", "플레이어 선택 (GUI)")
sendCommand(player, "/back", "이전 위치로 이동")
player.sendSystemMessage(Component.empty())
// 닉네임
sendCategory(player, "닉네임", COLOR_NICKNAME)
sendCategory(player, "닉네임", ChatFormatting.LIGHT_PURPLE)
sendCommand(player, "/닉네임 변경 <닉네임>", "닉네임 설정")
sendCommand(player, "/닉네임 초기화", "닉네임 초기화")
player.sendSystemMessage(Component.empty())
// 머리
sendCategory(player, "머리", COLOR_HEAD)
sendCategory(player, "머리", ChatFormatting.GOLD)
sendCommand(player, "/머리", "내 머리 아이템 받기")
sendCommand(player, "/머리 <닉네임>", "해당 플레이어 머리 받기")
player.sendSystemMessage(Component.empty())
// 제작대
sendCategory(player, "제작대", COLOR_WORKBENCH)
sendCommand(player, "/제작대", "제작대 열기")
player.sendSystemMessage(Component.empty())
// 메뉴
sendCategory(player, "메뉴", COLOR_MENU)
sendCommand(player, "/메뉴", "메뉴 GUI 열기")
sendCommand(player, "Shift + F", "메뉴 GUI 열기 (단축키)")
val footer =
Component.literal("═══════════════════════════════").withStyle {
it.withColor(COLOR_SEPARATOR)
it.withColor(ChatFormatting.DARK_GRAY)
}
player.sendSystemMessage(footer)
}
private fun sendCategory(player: ServerPlayer, name: String, color: TextColor) {
private fun sendCategory(player: ServerPlayer, name: String, color: ChatFormatting) {
val message =
Component.literal("")
.withStyle { it.withColor(color) }
.append(Component.literal(name).withStyle { it.withColor(color) })
.append(
Component.literal(name).withStyle {
it.withColor(color).withBold(true)
}
)
player.sendSystemMessage(message)
}
private fun sendCommand(player: ServerPlayer, cmd: String, desc: String) {
val message =
Component.literal(" ")
.append(Component.literal(cmd).withStyle { it.withColor(COLOR_COMMAND) })
.append(Component.literal(" - ").withStyle { it.withColor(COLOR_DASH) })
.append(Component.literal(desc).withStyle { it.withColor(COLOR_DESC) })
.append(
Component.literal(cmd).withStyle {
it.withColor(ChatFormatting.WHITE)
}
)
.append(
Component.literal(" - ").withStyle {
it.withColor(ChatFormatting.DARK_GRAY)
}
)
.append(
Component.literal(desc).withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
player.sendSystemMessage(message)
}
}

View file

@ -1,23 +0,0 @@
package com.beemer.essentials.command
import com.beemer.essentials.gui.openEssentialsMenu
import net.minecraft.commands.Commands
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
object MenuCommand {
@SubscribeEvent
fun onRegisterCommands(event: RegisterCommandsEvent) {
// /메뉴, /menu - 메뉴 GUI 열기
listOf("메뉴", "menu").forEach { command ->
event.dispatcher.register(
Commands.literal(command).executes { context ->
val player = context.source.entity as? ServerPlayer ?: return@executes 0
openEssentialsMenu(player)
1
}
)
}
}
}

View file

@ -3,9 +3,9 @@ package com.beemer.essentials.command
import com.beemer.essentials.config.PlayerConfig
import com.beemer.essentials.util.CommandUtils
import com.beemer.essentials.util.DimensionUtils
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
@ -23,10 +23,17 @@ object PlayerCommand {
val lastLocation =
info?.lastLocation
?: run {
MessageUtils.sendError(
player,
player.sendSystemMessage(
Component.literal(
"이전 위치가 존재하지 않습니다."
)
.withStyle {
it.withColor(
ChatFormatting
.RED
)
}
)
return@executes 0
}
@ -36,10 +43,17 @@ object PlayerCommand {
lastLocation.dimension
)
?: run {
MessageUtils.sendError(
player,
player.sendSystemMessage(
Component.literal(
"존재하지 않는 차원입니다."
)
.withStyle {
it.withColor(
ChatFormatting
.RED
)
}
)
return@executes 0
}
@ -51,9 +65,11 @@ object PlayerCommand {
player.yRot,
player.xRot
)
// 텔레포트 사운드 재생
SoundUtils.playTeleport(player)
MessageUtils.sendSuccess(player, "이전 위치로 이동했습니다.")
player.sendSystemMessage(
Component.literal("이전 위치로 이동했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
1
}
)

View file

@ -2,8 +2,9 @@ package com.beemer.essentials.command
import com.beemer.essentials.config.ProtectFarmlandConfig
import com.beemer.essentials.util.CommandUtils
import com.beemer.essentials.util.MessageUtils
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
@ -18,9 +19,22 @@ object ProtectFarmlandCommand {
?: return@executes 0
val enabled = ProtectFarmlandConfig.toggle()
val status = if (enabled) "활성화" else "비활성화"
MessageUtils.sendSuccess(player, "밭 보호를 {$status}했습니다.")
player.sendSystemMessage(
Component.literal("밭 보호를 ")
.withStyle { it.withColor(ChatFormatting.GOLD) }
.append(
Component.literal(if (enabled) "활성화" else "비활성화")
.withStyle {
it.withColor(ChatFormatting.DARK_GREEN)
}
)
.append(
Component.literal("했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
1
}
)

View file

@ -5,9 +5,9 @@ import com.beemer.essentials.config.SpawnConfig
import com.beemer.essentials.data.Location
import com.beemer.essentials.util.CommandUtils
import com.beemer.essentials.util.DimensionUtils
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
@ -23,9 +23,8 @@ object SpawnCommand {
val exactPos = player.position()
val blockPos = player.blockPosition()
val dimension =
player.level().dimension().location().toString()
val biome =
val dimensionId = player.level().dimension().location().toString()
val biomeId =
player.level()
.getBiome(blockPos)
.unwrapKey()
@ -34,15 +33,20 @@ object SpawnCommand {
val location =
Location(
dimension = dimension,
biome = biome,
dimension = dimensionId,
biome = biomeId,
x = exactPos.x,
y = exactPos.y,
z = exactPos.z
)
SpawnConfig.setCustomSpawn(location)
MessageUtils.sendSuccess(player, "스폰 지점이 현재 위치로 설정되었습니다.")
player.sendSystemMessage(
Component.literal("스폰 지점이 현재 위치로 설정되었습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
1
}
)
@ -55,62 +59,38 @@ object SpawnCommand {
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
)
val level = DimensionUtils.getLevelById(player.server, t.dimension)
level?.let { l ->
val exactPos = player.position()
val blockPos = player.blockPosition()
val currentDimension =
player.level()
.dimension()
.location()
.toString()
player.level().dimension().location().toString()
val currentBiome =
player.level()
.getBiome(blockPos)
.unwrapKey()
.map {
it.location()
.toString()
}
.map { it.location().toString() }
.orElse("minecraft:plains")
val currentLocation =
Location(
dimension =
currentDimension,
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
)
// 텔레포트 사운드 재생
SoundUtils.playTeleport(player)
MessageUtils.sendSuccess(
player,
"스폰으로 이동했습니다."
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)
}
)
}
}
@ -127,7 +107,11 @@ object SpawnCommand {
?: return@executes 0
SpawnConfig.removeCustomSpawn()
MessageUtils.sendInfo(player, "스폰 지점이 기본 스폰 지점으로 변경되었습니다.")
player.sendSystemMessage(
Component.literal("스폰 지점이 기본 스폰 지점으로 변경되었습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
1
}
)

View file

@ -4,8 +4,6 @@ import com.beemer.essentials.gui.TeleportGui
import com.beemer.essentials.gui.TeleportGui.Companion.CONTAINER_SIZE
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.CommandUtils
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import com.beemer.essentials.util.TranslationUtils
import com.mojang.authlib.GameProfile
import net.minecraft.ChatFormatting
@ -34,22 +32,13 @@ object TeleportCommand {
CommandUtils.getPlayerOrSendFailure(context.source)
?: return@executes 0
val container = SimpleContainer(9 * 3)
val targetPlayers =
player.server.playerList.players.filter {
it != player
}
// 다른 플레이어가 없으면 에러 메시지 표시
if (targetPlayers.isEmpty()) {
MessageUtils.sendError(
player,
"현재 접속 중인 다른 플레이어가 없습니다."
)
return@executes 0
}
val container = SimpleContainer(9 * 3)
targetPlayers.take(CONTAINER_SIZE).forEachIndexed {
idx,
target ->
@ -57,9 +46,6 @@ object TeleportCommand {
container.setItem(idx, head)
}
// 플레이어가 있을 때만 클릭 사운드 재생
SoundUtils.playClick(player)
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ ->

View file

@ -1,66 +0,0 @@
package com.beemer.essentials.command
import java.util.Optional
import java.util.function.BiFunction
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.SimpleMenuProvider
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.ContainerLevelAccess
import net.minecraft.world.inventory.CraftingMenu
import net.minecraft.world.level.Level
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
object WorkbenchCommand {
@SubscribeEvent
fun onRegisterCommands(event: RegisterCommandsEvent) {
// /제작대, /workbench - 제작대 GUI 열기
listOf("제작대", "workbench").forEach { command ->
event.dispatcher.register(
Commands.literal(command).executes { context ->
val player = context.source.entity as? ServerPlayer
if (player == null) {
context.source.sendFailure(
Component.literal("플레이어만 사용할 수 있는 명령어입니다.")
.withStyle(ChatFormatting.RED)
)
return@executes 0
}
openWorkbench(player)
1
}
)
}
}
/** 항상 유효한 ContainerLevelAccess 생성 */
private fun createAlwaysValidAccess(level: Level, pos: BlockPos): ContainerLevelAccess {
return object : ContainerLevelAccess {
override fun <T : Any> evaluate(function: BiFunction<Level, BlockPos, T>): Optional<T> {
return Optional.ofNullable(function.apply(level, pos))
}
}
}
/** 플레이어에게 제작대 GUI 열기 */
private fun openWorkbench(player: ServerPlayer) {
val access = createAlwaysValidAccess(player.level(), player.blockPosition())
player.openMenu(
SimpleMenuProvider(
{ containerId, inventory, _ ->
object : CraftingMenu(containerId, inventory, access) {
// stillValid를 오버라이드하여 항상 true 반환 (거리 제한 없음)
override fun stillValid(player: Player): Boolean = true
}
},
Component.translatable("container.crafting")
)
)
}
}

View file

@ -1,7 +1,7 @@
package com.beemer.essentials.event
import com.beemer.essentials.util.ChatUtils
import com.beemer.essentials.util.MessageUtils
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.ServerChatEvent
@ -12,11 +12,13 @@ object ChatEvents {
val player = event.player as ServerPlayer
val raw = event.rawText
if (raw.startsWith("/")) return
if (raw.startsWith("/"))
return
if (ChatUtils.hasIllegalCharacter(raw)) {
event.isCanceled = true
MessageUtils.sendError(player, "채팅에 허용되지 않는 문자가 포함되어 있습니다.")
player.sendSystemMessage(Component.literal("채팅에 허용되지 않는 문자가 포함되어 있습니다.")
.withStyle { it.withColor(net.minecraft.ChatFormatting.RED) })
return
}

View file

@ -1,44 +0,0 @@
package com.beemer.essentials.event
import com.beemer.essentials.gui.openEssentialsMenu
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import net.minecraft.server.level.ServerPlayer
/**
* Shift + F ( 바꾸기) 패킷 감지하여 메뉴 GUI 열기
*
* 동작 방식:
* 1. 플레이어가 Shift를 누르고 있는 상태에서
* 2. F키( 바꾸기) 누르면
* 3. 바꾸기 패킷을 취소하고 메뉴 GUI를 엽니다
*
* Mixin으로 패킷을 가로채기 때문에 손에서도 작동합니다.
*/
object MenuKeyHandler {
// 메뉴 열기 쿨다운 (밀리초 단위, 더블 감지 방지)
private val cooldowns = ConcurrentHashMap<UUID, Long>()
private const val COOLDOWN_MS = 500L
/** Mixin에서 호출 - 패킷 감지 시 메뉴 열기 */
fun openMenuFromPacket(player: ServerPlayer) {
if (isOnCooldown(player.uuid)) return
setCooldown(player.uuid)
openEssentialsMenu(player)
}
private fun isOnCooldown(uuid: UUID): Boolean {
val lastTime = cooldowns[uuid] ?: return false
return System.currentTimeMillis() - lastTime < COOLDOWN_MS
}
private fun setCooldown(uuid: UUID) {
cooldowns[uuid] = System.currentTimeMillis()
}
/** 플레이어 로그아웃 시 상태 정리 */
fun cleanup(uuid: UUID) {
cooldowns.remove(uuid)
}
}

View file

@ -5,8 +5,6 @@ import com.beemer.essentials.config.SpawnConfig
import com.beemer.essentials.data.Location
import com.beemer.essentials.util.ChatUtils
import com.beemer.essentials.util.DimensionUtils
import com.beemer.essentials.util.SoundUtils
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.entity.player.PlayerEvent
@ -37,69 +35,8 @@ object PlayerEvents {
val level = DimensionUtils.getLevelById(player.server, spawn.dimension) ?: return
player.teleportTo(level, spawn.x, spawn.y, spawn.z, player.yRot, player.xRot)
// 신규 플레이어 도움말 (약간의 딜레이 후 표시)
player.server.execute {
sendWelcomeGuide(player)
// 알림 사운드 재생
SoundUtils.playNotification(player)
}
}
}
/** 신규 플레이어 도움말 */
private fun sendWelcomeGuide(player: ServerPlayer) {
// 커스텀 색상 정의
val colorSeparator = net.minecraft.network.chat.TextColor.fromRgb(0x555555)
val colorWelcome = net.minecraft.network.chat.TextColor.fromRgb(0x7DD3FC)
val colorCategory = net.minecraft.network.chat.TextColor.fromRgb(0xFFE066)
val colorCommand = net.minecraft.network.chat.TextColor.fromRgb(0xFFFFFF)
val colorDesc = net.minecraft.network.chat.TextColor.fromRgb(0xAAAAAA)
val separator =
Component.literal("═══════════════════════════════").withStyle {
it.withColor(colorSeparator)
}
player.sendSystemMessage(Component.empty())
player.sendSystemMessage(separator)
player.sendSystemMessage(
Component.literal("🎉 서버에 오신 것을 환영합니다!").withStyle { it.withColor(colorWelcome) }
)
player.sendSystemMessage(Component.empty())
// 기본 명령어 안내
player.sendSystemMessage(
Component.literal("▸ 기본 명령어").withStyle { it.withColor(colorCategory) }
)
sendGuideItem(player, "/도움말", "명령어 목록 보기", colorCommand, colorDesc)
sendGuideItem(player, "/메뉴", "메뉴 GUI 열기", colorCommand, colorDesc)
sendGuideItem(player, "/스폰", "스폰으로 이동", colorCommand, colorDesc)
player.sendSystemMessage(Component.empty())
// 단축키 안내
player.sendSystemMessage(
Component.literal("▸ 단축키").withStyle { it.withColor(colorCategory) }
)
sendGuideItem(player, "Shift + F", "메뉴 GUI 열기", colorCommand, colorDesc)
player.sendSystemMessage(separator)
player.sendSystemMessage(Component.empty())
}
private fun sendGuideItem(
player: ServerPlayer,
key: String,
desc: String,
colorKey: net.minecraft.network.chat.TextColor,
colorDesc: net.minecraft.network.chat.TextColor
) {
player.sendSystemMessage(
Component.literal(" ")
.append(Component.literal(key).withStyle { it.withColor(colorKey) })
.append(Component.literal(" - $desc").withStyle { it.withColor(colorDesc) })
)
}
@SubscribeEvent
fun onPlayerLoggedOut(event: PlayerEvent.PlayerLoggedOutEvent) {
@ -117,16 +54,12 @@ object PlayerEvents {
val pos = oldPlayer.blockPosition()
val dimension = oldPlayer.level().dimension().location().toString()
val biome =
oldPlayer
.level()
.getBiome(pos)
val biome = oldPlayer.level().getBiome(pos)
.unwrapKey()
.map { it.location().toString() }
.orElse("minecraft:plains")
val lastLoc =
Location(
val lastLoc = Location(
dimension = dimension,
biome = biome,
x = pos.x.toDouble(),

View file

@ -1,7 +1,6 @@
package com.beemer.essentials.gui
import com.beemer.essentials.config.AntimobConfig
import com.beemer.essentials.util.SoundUtils
import net.minecraft.ChatFormatting
import net.minecraft.core.component.DataComponents
import net.minecraft.network.chat.Component
@ -78,7 +77,6 @@ class AntimobGui(
) {
if (player is ServerPlayer && slotId in 0 until CONTAINER_SIZE) {
if (glassSlotToMob.containsKey(slotId)) {
playClickSound(player)
val mob = glassSlotToMob[slotId] ?: return
AntimobConfig.toggle(mob)
container.setItem(slotId, makeToggleGlass(mob))
@ -96,10 +94,6 @@ class AntimobGui(
index: Int
): ItemStack = ItemStack.EMPTY
private fun playClickSound(player: ServerPlayer) {
SoundUtils.playClick(player)
}
private fun makeMobHead(mob: String): ItemStack {
val headItem = ItemStack(Items.PLAYER_HEAD)

View file

@ -4,8 +4,6 @@ import com.beemer.essentials.config.CoordinateConfig
import com.beemer.essentials.config.PlayerConfig
import com.beemer.essentials.data.Location
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import com.beemer.essentials.util.TranslationUtils.translateBiome
import com.beemer.essentials.util.TranslationUtils.translateDimension
import java.util.UUID
@ -31,7 +29,7 @@ import net.minecraft.world.item.Items
import net.minecraft.world.item.component.CustomData
import net.minecraft.world.item.component.ItemLore
/** 좌표 GUI - 페이지 기능 포함 4줄(36개) 표시 + 6번째 줄에 이전/다음 버튼 */
/** 좌표 GUI - 페이지 기능 포함 5줄(45개) 표시 + 6번째 줄에 이전/다음 버튼 */
class Menu(
syncId: Int,
playerInv: Inventory,
@ -40,9 +38,9 @@ class Menu(
) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) {
companion object {
const val ITEMS_PER_PAGE = 36 // 4줄 * 9
val PREV_BUTTON_SLOTS = listOf(45, 46) // 6번째 줄 이전 버튼 2칸
val NEXT_BUTTON_SLOTS = listOf(52, 53) // 6번째 줄 다음 버튼 2칸
const val ITEMS_PER_PAGE = 45 // 5줄 * 9
const val PREV_BUTTON_SLOT = 45 // 6번째 줄 첫 번째
const val NEXT_BUTTON_SLOT = 53 // 6번째 줄 마지막
}
init {
@ -83,26 +81,24 @@ class Menu(
val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
// 이전 페이지 버튼 (슬롯 45, 46)
if (slotId in PREV_BUTTON_SLOTS &&
// 이전 페이지 버튼
if (slotId == PREV_BUTTON_SLOT &&
tag.contains("action") &&
tag.getString("action") == "prev"
) {
if (currentPage > 0) {
playClickSound(player)
openPage(player, currentPage - 1)
}
return
}
// 다음 페이지 버튼 (슬롯 52, 53)
if (slotId in NEXT_BUTTON_SLOTS &&
// 다음 페이지 버튼
if (slotId == NEXT_BUTTON_SLOT &&
tag.contains("action") &&
tag.getString("action") == "next"
) {
val totalPages = getTotalPages()
if (currentPage < totalPages - 1) {
playClickSound(player)
openPage(player, currentPage + 1)
}
return
@ -113,7 +109,6 @@ class Menu(
val x = tag.getDouble("x")
val y = tag.getDouble("y")
val z = tag.getDouble("z")
val coordName = tag.getString("name")
val dimension =
ResourceLocation.tryParse(tag.getString("dimension"))?.let {
ResourceKey.create(Registries.DIMENSION, it)
@ -121,6 +116,7 @@ class Menu(
val level = dimension?.let { player.server.getLevel(it) }
if (level != null) {
// 이전 위치 저장
val prevPos = player.blockPosition()
PlayerConfig.recordLastLocation(
player,
@ -139,18 +135,20 @@ class Menu(
)
player.teleportTo(level, x, y, z, player.yRot, player.xRot)
// 텔레포트 사운드 재생
SoundUtils.playTeleport(player)
MessageUtils.sendSuccess(player, "{$coordName}(으)로 이동했습니다.")
player.sendSystemMessage(
Component.literal(tag.getString("name"))
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) }
.append(
Component.literal("(으)로 이동했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
}
player.closeContainer()
}
}
private fun playClickSound(player: ServerPlayer) {
SoundUtils.playClick(player)
}
private fun getTotalPages(): Int {
val totalItems = CoordinateConfig.getCoordinates().size
return if (totalItems == 0) 1 else (totalItems + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE
@ -158,24 +156,12 @@ class Menu(
private fun openPage(player: ServerPlayer, page: Int) {
val container = createPageContainer(player, page)
val totalPages = getTotalPages()
// 버튼 상태에 따른 이미지 선택
val buttonImage =
when {
page == 0 && page >= totalPages - 1 -> "\uE010" // 둘 다 비활성
page == 0 -> "\uE012" // 이전만 비활성
page >= totalPages - 1 -> "\uE011" // 다음만 비활성
else -> "\uE013" // 둘 다 활성
}
// 커스텀 버튼 이미지만 타이틀에 표시 (타이틀 텍스트는 이미지에 포함)
val customTitle = "\uF808$buttonImage"
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ -> Menu(windowId, inv, container, page) },
Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) }
Component.literal("저장된 좌표 (${page + 1}/${getTotalPages()})").withStyle {
it.withBold(true)
}
)
)
}
@ -194,10 +180,10 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer {
if (coordinates.isEmpty()) 1
else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE
// 좌표 아이템 추가 (2~5번째 줄, 슬롯 9-44)
// 좌표 아이템 추가 (5줄, 45개)
for (i in startIdx until endIdx) {
val coord = coordinates[i]
val item = ItemStack(Items.FILLED_MAP)
val item = ItemStack(Items.PAPER)
val tag =
CompoundTag().apply {
putString("name", coord.name)
@ -221,131 +207,96 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer {
val displayName =
if (creatorName != null) {
Component.literal(coord.name)
.withStyle {
it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.GOLD).withBold(true) }
.append(
Component.literal(" ($creatorName)").withStyle {
it.withColor(ChatFormatting.AQUA)
.withBold(false)
.withItalic(false)
it.withColor(ChatFormatting.AQUA).withBold(false)
}
)
} else {
Component.literal(coord.name).withStyle {
it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false)
it.withColor(ChatFormatting.GOLD).withBold(true)
}
}
val loreList: List<Component> =
listOf(
Component.literal("디멘션: ")
.withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) }
.append(
Component.literal(translateDimension(coord.dimension))
.withStyle {
it.withColor(ChatFormatting.GRAY)
.withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.GRAY) }
),
Component.literal("바이옴: ")
.withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) }
.append(
Component.literal(translateBiome(coord.biome)).withStyle {
it.withColor(ChatFormatting.GRAY).withItalic(false)
it.withColor(ChatFormatting.GRAY)
}
),
Component.literal("좌표: ")
.withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) }
.append(
Component.literal(
"${coord.x.toInt()}, ${coord.y.toInt()}, ${coord.z.toInt()}"
)
.withStyle {
it.withColor(ChatFormatting.GRAY)
.withItalic(false)
}
.withStyle { it.withColor(ChatFormatting.GRAY) }
)
)
item.set(DataComponents.CUSTOM_NAME, displayName)
item.set(DataComponents.LORE, ItemLore(loreList))
item.set(DataComponents.CUSTOM_DATA, CustomData.of(tag))
item.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
)
container.setItem(9 + (i - startIdx), item) // 슬롯 9부터 시작 (1줄 건너뛰기)
container.setItem(i - startIdx, item)
}
// 1번째 줄 (슬롯 0-8) - 타이틀 영역 투명 처리
val fillerItem = ItemStack(Items.GLASS_PANE)
fillerItem.set(DataComponents.CUSTOM_NAME, Component.empty())
fillerItem.set(DataComponents.LORE, ItemLore(listOf()))
// 맨 아래줄 빈 공간을 회색 유리판으로 채우기 (슬롯 46-52)
val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE)
fillerItem.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
DataComponents.CUSTOM_NAME,
Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) }
)
fillerItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE)
for (slot in 0..8) {
for (slot in 46..52) {
container.setItem(slot, fillerItem.copy())
}
// 6번째 줄 가운데 슬롯 (47-51) - 투명 처리
for (slot in 47..51) {
container.setItem(slot, fillerItem.copy())
}
// 이전 페이지 버튼 (슬롯 45, 46) - 페이지 정보 툴팁
val prevItem = ItemStack(Items.GLASS_PANE)
val prevTag = CompoundTag().apply { putString("action", if (page > 0) "prev" else "") }
// 이전 페이지 버튼 (슬롯 45)
if (page > 0) {
val prevItem = ItemStack(Items.LIME_STAINED_GLASS_PANE)
val prevTag = CompoundTag().apply { putString("action", "prev") }
prevItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("이전 페이지 [${page + 1}/$totalPages]").withStyle {
it.withColor(0xA8D8D8).withItalic(false)
}
Component.literal("이전 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) }
)
} else {
prevItem.set(DataComponents.CUSTOM_NAME, Component.empty())
prevItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE)
}
prevItem.set(DataComponents.LORE, ItemLore(listOf()))
prevItem.set(DataComponents.CUSTOM_DATA, CustomData.of(prevTag))
prevItem.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
container.setItem(Menu.PREV_BUTTON_SLOT, prevItem)
} else {
val disabledItem = ItemStack(Items.RED_STAINED_GLASS_PANE)
disabledItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("이전 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) }
)
Menu.PREV_BUTTON_SLOTS.forEach { container.setItem(it, prevItem.copy()) }
container.setItem(Menu.PREV_BUTTON_SLOT, disabledItem)
}
// 다음 페이지 버튼 (슬롯 52, 53) - 페이지 정보 툴팁
val nextItem = ItemStack(Items.GLASS_PANE)
val nextTag =
CompoundTag().apply { putString("action", if (page < totalPages - 1) "next" else "") }
// 다음 페이지 버튼 (슬롯 53)
if (page < totalPages - 1) {
val nextItem = ItemStack(Items.LIME_STAINED_GLASS_PANE)
val nextTag = CompoundTag().apply { putString("action", "next") }
nextItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("다음 페이지 [${page + 1}/$totalPages]").withStyle {
it.withColor(0xA8D8D8).withItalic(false)
}
Component.literal("다음 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) }
)
} else {
nextItem.set(DataComponents.CUSTOM_NAME, Component.empty())
nextItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE)
}
nextItem.set(DataComponents.LORE, ItemLore(listOf()))
nextItem.set(DataComponents.CUSTOM_DATA, CustomData.of(nextTag))
nextItem.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
container.setItem(Menu.NEXT_BUTTON_SLOT, nextItem)
} else {
val disabledItem = ItemStack(Items.RED_STAINED_GLASS_PANE)
disabledItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("다음 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) }
)
Menu.NEXT_BUTTON_SLOTS.forEach { container.setItem(it, nextItem.copy()) }
container.setItem(Menu.NEXT_BUTTON_SLOT, disabledItem)
}
return container
}

View file

@ -1,268 +0,0 @@
package com.beemer.essentials.gui
import com.beemer.essentials.util.SoundUtils
import net.minecraft.ChatFormatting
import net.minecraft.core.component.DataComponents
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.SimpleMenuProvider
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.ClickType
import net.minecraft.world.inventory.ContainerLevelAccess
import net.minecraft.world.inventory.CraftingMenu
import net.minecraft.world.inventory.MenuType
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.component.CustomData
import net.minecraft.world.item.component.ItemLore
/** Essentials 메뉴 GUI - Shift+F로 열기 스폰, 좌표, 제작대 등 주요 기능 바로가기 */
class EssentialsMenuGui(
syncId: Int,
playerInv: Inventory,
val container: Container,
val viewer: ServerPlayer
) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) {
companion object {
const val CONTAINER_SIZE = 54 // 9x6 = 54
// 메뉴 아이템 슬롯 위치 (9x6 기준)
const val SLOT_SPAWN = 19 // 스폰
const val SLOT_COORDINATES = 21 // 좌표
const val SLOT_WORKBENCH = 23 // 제작대
const val SLOT_TPA = 25 // TPA
}
init {
// 컨테이너 슬롯 추가
for (i in 0 until CONTAINER_SIZE) {
val x = 8 + (i % 9) * 18
val y = 18 + (i / 9) * 18
addSlot(
object : Slot(container, i, x, y) {
override fun mayPickup(player: Player): Boolean = false
override fun mayPlace(stack: ItemStack): Boolean = false
}
)
}
// 플레이어 인벤토리 슬롯
for (row in 0 until 3) {
for (col in 0 until 9) {
val index = col + row * 9 + 9
val x = 8 + col * 18
val y = 86 + row * 18
addSlot(Slot(playerInv, index, x, y))
}
}
for (col in 0 until 9) {
val x = 8 + col * 18
val y = 144
addSlot(Slot(playerInv, col, x, y))
}
}
override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) {
// 허용된 버튼 슬롯 목록
val buttonSlots =
setOf(
// 스폰
18,
19,
20,
27,
28,
29,
// 제작대 (순서 변경)
21,
22,
23,
30,
31,
32,
// 좌표 (순서 변경)
24,
25,
26,
33,
34,
35,
// TPA
36,
37,
38,
45,
46,
47
)
// 버튼 슬롯이 아니면 기본 처리
if (slotId !in buttonSlots || player !is ServerPlayer) {
super.clicked(slotId, button, clickType, player)
return
}
val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
val action = tag.getString("action")
if (action.isEmpty()) return
when (action) {
"spawn" -> {
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"스폰"
)
// 스폰 명령어에서 이미 사운드 재생하므로 여기서는 제거
}
"coordinates" -> {
playClickSound(player)
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"좌표"
)
}
"workbench" -> {
player.closeContainer()
openWorkbench(player)
playClickSound(player)
}
"tpa" -> {
// TPA 명령어에서 사운드 재생하므로 여기서는 제거
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"tpa"
)
}
}
}
private fun playClickSound(player: ServerPlayer) {
SoundUtils.playClick(player)
}
private fun openWorkbench(player: ServerPlayer) {
val access =
object : ContainerLevelAccess {
override fun <T : Any> evaluate(
function:
java.util.function.BiFunction<
net.minecraft.world.level.Level,
net.minecraft.core.BlockPos,
T>
): java.util.Optional<T> {
return java.util.Optional.ofNullable(
function.apply(
player.level(),
player.blockPosition()
)
)
}
}
player.openMenu(
SimpleMenuProvider(
{ containerId, inventory, _ ->
object : CraftingMenu(containerId, inventory, access) {
override fun stillValid(player: Player): Boolean =
true
}
},
Component.translatable("container.crafting")
)
)
}
override fun stillValid(player: Player): Boolean = true
override fun quickMoveStack(player: Player, index: Int): ItemStack = ItemStack.EMPTY
}
/** 메뉴 컨테이너 생성 */
fun createEssentialsMenuContainer(): SimpleContainer {
val container = SimpleContainer(54) // 9x6 = 54
// 버튼 생성 헬퍼 함수 (툴팁 포함)
fun createButton(action: String, name: String, description: String): ItemStack {
val item = ItemStack(Items.GLASS_PANE)
// 이름 설정 (하늘색)
item.set(
DataComponents.CUSTOM_NAME,
Component.literal(name).withStyle {
it.withColor(0xA8D8D8).withItalic(false)
}
)
// 설명 설정 (회색)
item.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal(description).withStyle {
it.withColor(ChatFormatting.GRAY).withItalic(false)
}
)
)
)
item.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", action) })
)
// CustomModelData를 1로 설정하여 텍스처 제거
item.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
)
return item
}
// 스폰 버튼 (3x2 영역: 18,19,20,27,28,29)
val spawnSlots = listOf(18, 19, 20, 27, 28, 29)
val spawnItem = createButton("spawn", "스폰", "스폰 지점으로 이동합니다")
spawnSlots.forEach { container.setItem(it, spawnItem.copy()) }
// 제작대 버튼 (3x2 영역: 21,22,23,30,31,32)
val workbenchSlots = listOf(21, 22, 23, 30, 31, 32)
val workbenchItem = createButton("workbench", "제작대", "제작대를 엽니다")
workbenchSlots.forEach { container.setItem(it, workbenchItem.copy()) }
// 좌표 버튼 (3x2 영역: 24,25,26,33,34,35)
val coordSlots = listOf(24, 25, 26, 33, 34, 35)
val coordItem = createButton("coordinates", "좌표", "저장된 좌표 목록을 엽니다")
coordSlots.forEach { container.setItem(it, coordItem.copy()) }
// TPA 버튼 (3x2 영역: 36,37,38,45,46,47)
val tpaSlots = listOf(36, 37, 38, 45, 46, 47)
val tpaItem = createButton("tpa", "텔레포트", "다른 플레이어에게 이동합니다")
tpaSlots.forEach { container.setItem(it, tpaItem.copy()) }
return container
}
/** 메뉴 GUI 열기 */
fun openEssentialsMenu(player: ServerPlayer) {
val container = createEssentialsMenuContainer()
// 메뉴 오픈 사운드 재생
SoundUtils.playMenu(player)
// 커스텀 GUI 이미지를 위한 유니코드 문자
// \uF808 = -8 (왼쪽으로 8px 이동)
// \uE000 = 메뉴 GUI 이미지
val customGuiPrefix = "\uF808\uE000"
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) },
Component.literal(customGuiPrefix).withStyle { it.withColor(0xFFFFFF) }
)
)
}

View file

@ -2,9 +2,10 @@ package com.beemer.essentials.gui
import com.beemer.essentials.config.PlayerConfig
import com.beemer.essentials.data.Location
import com.beemer.essentials.data.Player
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.SoundUtils
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory
@ -78,17 +79,10 @@ class TeleportGui(
player: net.minecraft.world.entity.player.Player
) {
if (slotId in 0 until CONTAINER_SIZE && player is ServerPlayer) {
// 해당 슬롯에 플레이어가 있는지 확인
val currentTargetPlayer =
targetPlayers.getOrNull(slotId)?.let {
player.server.playerList.getPlayer(it)
}
// 플레이어가 없으면 무시 (빈 공간 클릭)
if (currentTargetPlayer == null) {
return
}
targetPlayers.getOrNull(slotId)?.let { player.server.playerList.getPlayer(it) }
if (currentTargetPlayer != null && player.uuid != currentTargetPlayer.uuid) {
val prevPos = player.blockPosition()
val prevDimension = player.level().dimension().location().toString()
val prevBiome =
@ -118,22 +112,38 @@ class TeleportGui(
currentTargetPlayer.xRot
)
// 소리 재생 (텔레포트 후)
playClickSound(player)
// 닉네임이 있으면 닉네임 사용
val targetName =
NicknameDataStore.getNickname(currentTargetPlayer.uuid)
?: currentTargetPlayer.gameProfile.name
val playerName =
NicknameDataStore.getNickname(player.uuid)
?: player.gameProfile.name
NicknameDataStore.getNickname(player.uuid) ?: player.gameProfile.name
MessageUtils.sendSuccess(player, "{$targetName}님에게 텔레포트 했습니다.")
MessageUtils.sendInfo(
currentTargetPlayer,
"{$playerName}님이 당신에게 텔레포트 했습니다."
player.sendSystemMessage(
Component.literal(targetName)
.withStyle { it.withColor(ChatFormatting.AQUA) }
.append(
Component.literal("님에게 텔레포트 했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
currentTargetPlayer.sendSystemMessage(
Component.literal(playerName)
.withStyle { it.withColor(ChatFormatting.AQUA) }
.append(
Component.literal("님이 당신에게 텔레포트 했습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
} else {
player.sendSystemMessage(
Component.literal("해당 플레이어는 현재 오프라인입니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
}
player.closeContainer()
return
@ -141,10 +151,6 @@ class TeleportGui(
super.clicked(slotId, button, clickType, player)
}
private fun playClickSound(player: ServerPlayer) {
SoundUtils.playTeleport(player)
}
override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true
override fun quickMoveStack(

View file

@ -1,8 +1,9 @@
package com.beemer.essentials.nickname
import com.beemer.essentials.util.MessageUtils
import com.mojang.brigadier.arguments.StringArgumentType
import net.minecraft.ChatFormatting
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.neoforge.event.RegisterCommandsEvent
@ -28,6 +29,7 @@ object NicknameCommand {
.entity as?
ServerPlayer
?: return@executes 0
val nickname =
StringArgumentType
.getString(
@ -44,6 +46,7 @@ object NicknameCommand {
val player =
context.source.entity as? ServerPlayer
?: return@executes 0
executeReset(player)
}
)
@ -66,6 +69,7 @@ object NicknameCommand {
.entity as?
ServerPlayer
?: return@executes 0
val nickname =
StringArgumentType
.getString(
@ -82,6 +86,7 @@ object NicknameCommand {
val player =
context.source.entity as? ServerPlayer
?: return@executes 0
executeReset(player)
}
)
@ -91,41 +96,65 @@ object NicknameCommand {
private fun executeSet(player: ServerPlayer, nickname: String): Int {
// 유효성 검사: 길이
if (nickname.length < 2 || nickname.length > 16) {
MessageUtils.sendError(player, "닉네임은 2~16자 사이여야 합니다.")
return 0
player.sendSystemMessage(
Component.literal("닉네임은 2~16자 사이여야 합니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
// 유효성 검사: 현재 닉네임과 동일
val currentNickname = NicknameDataStore.getNickname(player.uuid)
if (currentNickname == nickname) {
MessageUtils.sendError(player, "현재 사용 중인 닉네임과 동일합니다.")
)
return 0
}
// 유효성 검사: 중복
if (NicknameDataStore.isNicknameTaken(nickname, player.uuid)) {
MessageUtils.sendError(player, "이미 사용 중인 닉네임입니다.")
player.sendSystemMessage(
Component.literal("이미 사용 중인 닉네임입니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return 0
}
// 닉네임 저장 및 적용
// 닉네임 저장 및 적용 (gameProfile.name = 실제 마인크래프트 이름)
NicknameDataStore.setNickname(player.uuid, player.gameProfile.name, nickname)
NicknameManager.applyNickname(player, nickname)
MessageUtils.sendSuccess(player, "닉네임이 {$nickname}(으)로 변경되었습니다.")
player.sendSystemMessage(
Component.literal("닉네임이 ")
.withStyle { it.withColor(ChatFormatting.GOLD) }
.append(
Component.literal(nickname).withStyle {
it.withColor(ChatFormatting.AQUA)
}
)
.append(
Component.literal("(으)로 변경되었습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
)
return 1
}
private fun executeReset(player: ServerPlayer): Int {
if (!NicknameDataStore.hasNickname(player.uuid)) {
MessageUtils.sendError(player, "설정된 닉네임이 없습니다.")
player.sendSystemMessage(
Component.literal("설정된 닉네임이 없습니다.").withStyle {
it.withColor(ChatFormatting.RED)
}
)
return 0
}
NicknameDataStore.removeNickname(player.uuid)
NicknameManager.removeNickname(player)
MessageUtils.sendSuccess(player, "닉네임이 초기화되었습니다.")
player.sendSystemMessage(
Component.literal("닉네임이 초기화되었습니다.").withStyle {
it.withColor(ChatFormatting.GOLD)
}
)
return 1
}
}

View file

@ -1,23 +0,0 @@
package com.beemer.essentials.util
import com.beemer.essentials.data.Location
import net.minecraft.server.level.ServerPlayer
/** 위치 관련 유틸리티 플레이어 위치를 Location 객체로 변환하는 공통 로직 */
object LocationUtils {
/** 플레이어의 현재 위치를 Location 객체로 변환 */
fun fromPlayer(player: ServerPlayer): Location {
val pos = player.position()
val blockPos = player.blockPosition()
val level = player.level()
val dimension = level.dimension().location().toString()
val biome =
level.getBiome(blockPos)
.unwrapKey()
.map { it.location().toString() }
.orElse("minecraft:plains")
return Location(dimension = dimension, biome = biome, x = pos.x, y = pos.y, z = pos.z)
}
}

View file

@ -1,125 +0,0 @@
package com.beemer.essentials.util
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Style
import net.minecraft.network.chat.TextColor
import net.minecraft.server.level.ServerPlayer
/** 메시지 스타일 유틸리티 기본 텍스트: 밝은 청록색 (#A8D8D8) 강조 텍스트: 흰색 (#FFFFFF) */
object MessageUtils {
// 기본 색상 (더 밝게)
private const val BASE_COLOR = 0xA8D8D8 // 밝은 청록색 (기본 텍스트)
private const val ACCENT_COLOR = 0xFFFFFF // 흰색 (강조)
private const val ERROR_COLOR = 0xFF8888 // 밝은 빨간색 (오류)
private const val WARNING_COLOR = 0xFFE066 // 밝은 노란색 (경고)
/**
* 기본 스타일 메시지 생성
* @param text 텍스트 (강조 부분은 {중괄호} 감싸기) : "스폰으로 이동했습니다." 또는 "{비머}님에게 텔레포트 요청을 보냈습니다."
*/
fun styled(text: String): MutableComponent {
return parseStyledText(text, BASE_COLOR, ACCENT_COLOR)
}
/** 성공 메시지 (아이콘 없이 기본 스타일) */
fun success(text: String): MutableComponent {
return styled(text)
}
/** 정보 메시지 (아이콘 없이 기본 스타일) */
fun info(text: String): MutableComponent {
return styled(text)
}
/** 오류 메시지 (빨간색 스타일) */
fun error(text: String): MutableComponent {
return parseStyledText(text, ERROR_COLOR, ACCENT_COLOR)
}
/** 경고 메시지 (노란색 스타일) */
fun warning(text: String): MutableComponent {
return parseStyledText(text, WARNING_COLOR, ACCENT_COLOR)
}
// === 플레이어에게 직접 전송 ===
fun sendSuccess(player: ServerPlayer, text: String) {
player.sendSystemMessage(success(text))
}
fun sendInfo(player: ServerPlayer, text: String) {
player.sendSystemMessage(info(text))
}
fun sendError(player: ServerPlayer, text: String) {
player.sendSystemMessage(error(text))
SoundUtils.playError(player)
}
fun sendWarning(player: ServerPlayer, text: String) {
player.sendSystemMessage(warning(text))
}
/** 알림 메시지 (사운드 포함) - 서버 접속 시 등 */
fun sendNotification(player: ServerPlayer, text: String) {
player.sendSystemMessage(info(text))
SoundUtils.playNotification(player)
}
/** 스타일 텍스트 파싱 {중괄호} 안의 텍스트는 강조 색상으로 표시 */
private fun parseStyledText(text: String, baseColor: Int, accentColor: Int): MutableComponent {
val result: MutableComponent = Component.empty()
var i = 0
val sb = StringBuilder()
while (i < text.length) {
when {
text[i] == '{' -> {
// 이전 텍스트 추가 (기본 색상)
if (sb.isNotEmpty()) {
result.append(
Component.literal(sb.toString())
.withStyle(
Style.EMPTY.withColor(TextColor.fromRgb(baseColor))
)
)
sb.clear()
}
// 강조 텍스트 찾기
val endIdx = text.indexOf('}', i)
if (endIdx != -1) {
val accentText = text.substring(i + 1, endIdx)
result.append(
Component.literal(accentText)
.withStyle(
Style.EMPTY.withColor(
TextColor.fromRgb(accentColor)
)
)
)
i = endIdx + 1
} else {
sb.append(text[i])
i++
}
}
else -> {
sb.append(text[i])
i++
}
}
}
// 남은 텍스트 추가
if (sb.isNotEmpty()) {
result.append(
Component.literal(sb.toString())
.withStyle(Style.EMPTY.withColor(TextColor.fromRgb(baseColor)))
)
}
return result
}
}

View file

@ -1,44 +0,0 @@
package com.beemer.essentials.util
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundSource
/** 사운드 재생 유틸리티 모든 커스텀 사운드 재생 로직을 중앙화 */
object SoundUtils {
// 기본 볼륨 (20%)
private const val DEFAULT_VOLUME = 0.2f
private const val DEFAULT_PITCH = 1.0f
/** 클릭 사운드 - GUI 버튼 클릭 시 */
fun playClick(player: ServerPlayer) {
playSound(player, "minecraft:custom.click")
}
/** 텔레포트 사운드 - 스폰, 좌표이동, TPA, back 시 */
fun playTeleport(player: ServerPlayer) {
playSound(player, "minecraft:custom.teleport")
}
/** 에러 사운드 - 오류 메시지 시 */
fun playError(player: ServerPlayer) {
playSound(player, "minecraft:custom.error")
}
/** 알림 사운드 - 서버 접속 시 */
fun playNotification(player: ServerPlayer) {
playSound(player, "minecraft:custom.notification")
}
/** 메뉴 오픈 사운드 - 메뉴 GUI 열기 시 */
fun playMenu(player: ServerPlayer) {
playSound(player, "minecraft:custom.menu")
}
/** 공통 사운드 재생 */
private fun playSound(player: ServerPlayer, soundId: String) {
val sound = SoundEvent.createVariableRangeEvent(ResourceLocation.parse(soundId))
player.playNotifySound(sound, SoundSource.MASTER, DEFAULT_VOLUME, DEFAULT_PITCH)
}
}

View file

@ -1,3 +0,0 @@
# Essentials Access Transformer
# ServerGamePacketListenerImpl의 player 필드에 접근하기 위한 설정은 필요 없음
# NeoForge에서 ServerPlayer 접근 가능

View file

@ -10,7 +10,6 @@
"PlayerInfoPacketMixin",
"PlayerListMixin",
"PlayerMixin",
"ServerGamePacketListenerImplMixin",
"ServerPlayerMixin"
],
"client": [],