feat: Discord 연동 모드 추가 /n- 게임 내 채팅 Discord 전송/n- 접속/퇴장 알림/n- 사망 메시지 (한국어 번역)/n- 발전과제 알림 (한국어 번역)/n- 서버 시작/종료 알림/n- README 작성
This commit is contained in:
parent
ad253e8499
commit
f2fb0ad324
22 changed files with 2038 additions and 0 deletions
89
DiscordBot/README.md
Normal file
89
DiscordBot/README.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# 🤖 DiscordBot
|
||||||
|
|
||||||
|
마인크래프트 서버 이벤트를 Discord로 전송하는 NeoForge 모드입니다.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 주요 기능
|
||||||
|
|
||||||
|
- 💬 **채팅 연동** - 게임 내 채팅을 Discord로 전송
|
||||||
|
- 🚪 **접속/퇴장 알림** - 플레이어 접속/퇴장 알림
|
||||||
|
- ⚔️ **사망 메시지** - 플레이어 사망 시 알림 (한국어 번역)
|
||||||
|
- 🏆 **발전과제 알림** - 발전과제 달성 시 알림 (한국어 번역)
|
||||||
|
- 🖥️ **서버 시작/종료** - 서버 상태 변경 알림
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 설정
|
||||||
|
|
||||||
|
`config/discordbot.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[discord]
|
||||||
|
webhookUrl = "YOUR_DISCORD_WEBHOOK_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Discord 메시지 형식
|
||||||
|
|
||||||
|
### 채팅 메시지
|
||||||
|
|
||||||
|
> **플레이어명**: 메시지 내용
|
||||||
|
|
||||||
|
### 접속/퇴장
|
||||||
|
|
||||||
|
> 🟢 **플레이어명**님이 서버에 접속했습니다.
|
||||||
|
> 🔴 **플레이어명**님이 서버에서 퇴장했습니다.
|
||||||
|
|
||||||
|
### 사망 메시지
|
||||||
|
|
||||||
|
> ☠️ **플레이어명**님이 좀비에게 사망했습니다.
|
||||||
|
|
||||||
|
### 발전과제
|
||||||
|
|
||||||
|
> 🏆 **플레이어명**님이 발전과제 [돌 시대]를 달성했습니다!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 기술 스택
|
||||||
|
|
||||||
|
| 기술 | 설명 |
|
||||||
|
| -------------------- | --------------------- |
|
||||||
|
| **NeoForge** | Minecraft 모딩 플랫폼 |
|
||||||
|
| **Kotlin** | 주 개발 언어 |
|
||||||
|
| **Discord Webhooks** | Discord 연동 |
|
||||||
|
| **Gson** | JSON 직렬화 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
DiscordBot/
|
||||||
|
├── src/main/
|
||||||
|
│ ├── kotlin/com/beemer/discordbot/
|
||||||
|
│ │ ├── command/ # 명령어 처리
|
||||||
|
│ │ ├── config/ # 설정 관리
|
||||||
|
│ │ ├── discord/ # Discord 웹훅
|
||||||
|
│ │ ├── event/ # 이벤트 핸들러
|
||||||
|
│ │ ├── util/ # 유틸리티 (번역)
|
||||||
|
│ │ └── DiscordBot.kt # 메인 모드
|
||||||
|
│ └── resources/
|
||||||
|
│ └── META-INF/ # 모드 메타데이터
|
||||||
|
└── build.gradle
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 빌드
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
빌드된 JAR: `build/libs/discordbot-1.0.0.jar`
|
||||||
145
DiscordBot/build.gradle
Normal file
145
DiscordBot/build.gradle
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
id 'maven-publish'
|
||||||
|
id 'idea'
|
||||||
|
id 'net.neoforged.moddev' version '2.0.80'
|
||||||
|
id 'org.jetbrains.kotlin.jvm' version '2.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
version = mod_version
|
||||||
|
group = mod_group_id
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
maven {
|
||||||
|
name = 'Kotlin for Forge'
|
||||||
|
url = 'https://thedarkcolour.github.io/KotlinForForge/'
|
||||||
|
content { includeGroup "thedarkcolour" }
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName = mod_id
|
||||||
|
}
|
||||||
|
|
||||||
|
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
kotlin.jvmToolchain(21)
|
||||||
|
|
||||||
|
neoForge {
|
||||||
|
version = project.neo_version
|
||||||
|
|
||||||
|
parchment {
|
||||||
|
mappingsVersion = project.parchment_mappings_version
|
||||||
|
minecraftVersion = project.parchment_minecraft_version
|
||||||
|
}
|
||||||
|
|
||||||
|
runs {
|
||||||
|
client {
|
||||||
|
client()
|
||||||
|
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server()
|
||||||
|
programArgument '--nogui'
|
||||||
|
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||||
|
}
|
||||||
|
|
||||||
|
gameTestServer {
|
||||||
|
type = "gameTestServer"
|
||||||
|
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||||
|
}
|
||||||
|
|
||||||
|
data {
|
||||||
|
data()
|
||||||
|
programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
configureEach {
|
||||||
|
systemProperty 'forge.logging.markers', 'REGISTRIES'
|
||||||
|
logLevel = org.slf4j.event.Level.DEBUG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mods {
|
||||||
|
"${mod_id}" {
|
||||||
|
sourceSet(sourceSets.main)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.main.resources { srcDir 'src/generated/resources' }
|
||||||
|
|
||||||
|
// JDA 의존성 설정
|
||||||
|
configurations {
|
||||||
|
jdaLibs
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'thedarkcolour:kotlinforforge-neoforge:5.3.0'
|
||||||
|
|
||||||
|
// JDA (Java Discord API)
|
||||||
|
def jdaDeps = {
|
||||||
|
exclude module: 'opus-java'
|
||||||
|
exclude group: 'org.slf4j'
|
||||||
|
exclude group: 'com.google.errorprone'
|
||||||
|
exclude group: 'org.jetbrains'
|
||||||
|
exclude group: 'org.checkerframework'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation('net.dv8tion:JDA:5.2.1', jdaDeps)
|
||||||
|
jarJar('net.dv8tion:JDA:5.2.1', jdaDeps)
|
||||||
|
|
||||||
|
// JDA 필수 의존성들도 JarJar로 포함
|
||||||
|
jarJar('com.squareup.okhttp3:okhttp:4.12.0')
|
||||||
|
jarJar('com.squareup.okio:okio:3.6.0')
|
||||||
|
jarJar('com.squareup.okio:okio-jvm:3.6.0')
|
||||||
|
jarJar('com.neovisionaries:nv-websocket-client:2.14')
|
||||||
|
jarJar('net.sf.trove4j:trove4j:3.0.3')
|
||||||
|
jarJar('com.fasterxml.jackson.core:jackson-core:2.17.0')
|
||||||
|
jarJar('com.fasterxml.jackson.core:jackson-databind:2.17.0')
|
||||||
|
jarJar('com.fasterxml.jackson.core:jackson-annotations:2.17.0')
|
||||||
|
jarJar('org.apache.commons:commons-collections4:4.4')
|
||||||
|
}
|
||||||
|
|
||||||
|
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
|
||||||
|
var replaceProperties = [minecraft_version : minecraft_version,
|
||||||
|
minecraft_version_range: minecraft_version_range,
|
||||||
|
neo_version : neo_version,
|
||||||
|
neo_version_range : neo_version_range,
|
||||||
|
loader_version_range : loader_version_range,
|
||||||
|
mod_id : mod_id,
|
||||||
|
mod_name : mod_name,
|
||||||
|
mod_license : mod_license,
|
||||||
|
mod_version : mod_version,
|
||||||
|
mod_authors : mod_authors,
|
||||||
|
mod_description : mod_description]
|
||||||
|
inputs.properties replaceProperties
|
||||||
|
expand replaceProperties
|
||||||
|
from "src/main/templates"
|
||||||
|
into "build/generated/sources/modMetadata"
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.main.resources.srcDir generateModMetadata
|
||||||
|
neoForge.ideSyncTask generateModMetadata
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
register('mavenJava', MavenPublication) {
|
||||||
|
from components.java
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "file://${project.projectDir}/repo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
downloadSources = true
|
||||||
|
downloadJavadoc = true
|
||||||
|
}
|
||||||
|
}
|
||||||
24
DiscordBot/gradle.properties
Normal file
24
DiscordBot/gradle.properties
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
|
||||||
|
org.gradle.jvmargs=-Xmx2G
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
|
||||||
|
## Environment Properties
|
||||||
|
minecraft_version=1.21.1
|
||||||
|
minecraft_version_range=[1.21.1,1.22)
|
||||||
|
neo_version=21.1.194
|
||||||
|
neo_version_range=[21,)
|
||||||
|
loader_version_range=[5.3,)
|
||||||
|
parchment_minecraft_version=1.21.1
|
||||||
|
parchment_mappings_version=2024.11.17
|
||||||
|
|
||||||
|
## Mod Properties
|
||||||
|
mod_id=discordbot
|
||||||
|
mod_name=DiscordBot
|
||||||
|
mod_license=MIT
|
||||||
|
mod_version=1.0.0
|
||||||
|
mod_group_id=com.beemer
|
||||||
|
mod_authors=
|
||||||
|
mod_description=Discord integration mod for Minecraft
|
||||||
BIN
DiscordBot/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
DiscordBot/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
DiscordBot/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
DiscordBot/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
249
DiscordBot/gradlew
vendored
Executable file
249
DiscordBot/gradlew
vendored
Executable file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
92
DiscordBot/gradlew.bat
vendored
Normal file
92
DiscordBot/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
11
DiscordBot/settings.gradle
Normal file
11
DiscordBot/settings.gradle
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven { url = 'https://maven.neoforged.net/releases' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.beemer.discordbot
|
||||||
|
|
||||||
|
import com.beemer.discordbot.command.DiscordCommand
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.event.AdvancementEvents
|
||||||
|
import com.beemer.discordbot.event.ChatEvents
|
||||||
|
import com.beemer.discordbot.event.DeathEvents
|
||||||
|
import com.beemer.discordbot.event.PlayerEvents
|
||||||
|
import com.beemer.discordbot.event.ServerEvents
|
||||||
|
import net.neoforged.bus.api.IEventBus
|
||||||
|
import net.neoforged.fml.common.Mod
|
||||||
|
import net.neoforged.neoforge.common.NeoForge
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import org.apache.logging.log4j.Logger
|
||||||
|
|
||||||
|
@Mod(DiscordBot.ID)
|
||||||
|
class DiscordBot(modEventBus: IEventBus) {
|
||||||
|
companion object {
|
||||||
|
const val ID = "discordbot"
|
||||||
|
val LOGGER: Logger = LogManager.getLogger(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 설정 로드
|
||||||
|
DiscordConfig.load()
|
||||||
|
|
||||||
|
// 번역 파일 로드
|
||||||
|
com.beemer.discordbot.util.TranslationManager.load()
|
||||||
|
|
||||||
|
// 이벤트 등록
|
||||||
|
NeoForge.EVENT_BUS.register(ServerEvents)
|
||||||
|
NeoForge.EVENT_BUS.register(PlayerEvents)
|
||||||
|
NeoForge.EVENT_BUS.register(ChatEvents)
|
||||||
|
NeoForge.EVENT_BUS.register(DeathEvents)
|
||||||
|
NeoForge.EVENT_BUS.register(AdvancementEvents)
|
||||||
|
NeoForge.EVENT_BUS.register(DiscordCommand)
|
||||||
|
|
||||||
|
LOGGER.info("[DiscordBot] 모드 초기화 완료")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.beemer.discordbot.command
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
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
|
||||||
|
|
||||||
|
object DiscordCommand {
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onRegisterCommands(event: RegisterCommandsEvent) {
|
||||||
|
// /디스코드 새로고침
|
||||||
|
event.dispatcher.register(
|
||||||
|
Commands.literal("디스코드")
|
||||||
|
.requires { it.hasPermission(2) }
|
||||||
|
.then(
|
||||||
|
Commands.literal("새로고침").executes { context ->
|
||||||
|
val player = context.source.entity as? ServerPlayer
|
||||||
|
|
||||||
|
// 설정 다시 로드
|
||||||
|
DiscordConfig.reload()
|
||||||
|
|
||||||
|
// 봇 재시작
|
||||||
|
BotManager.restart()
|
||||||
|
|
||||||
|
val message =
|
||||||
|
Component.literal("디스코드 설정을 다시 불러왔습니다.").withStyle {
|
||||||
|
it.withColor(ChatFormatting.GOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
player?.sendSystemMessage(message)
|
||||||
|
?: context.source.sendSuccess({ message }, false)
|
||||||
|
|
||||||
|
1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// /discord reload (영문)
|
||||||
|
event.dispatcher.register(
|
||||||
|
Commands.literal("discord")
|
||||||
|
.requires { it.hasPermission(2) }
|
||||||
|
.then(
|
||||||
|
Commands.literal("reload").executes { context ->
|
||||||
|
val player = context.source.entity as? ServerPlayer
|
||||||
|
|
||||||
|
DiscordConfig.reload()
|
||||||
|
BotManager.restart()
|
||||||
|
|
||||||
|
val message =
|
||||||
|
Component.literal("Discord settings reloaded.")
|
||||||
|
.withStyle { it.withColor(ChatFormatting.GOLD) }
|
||||||
|
|
||||||
|
player?.sendSystemMessage(message)
|
||||||
|
?: context.source.sendSuccess({ message }, false)
|
||||||
|
|
||||||
|
1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.beemer.discordbot.config
|
||||||
|
|
||||||
|
import com.beemer.discordbot.DiscordBot
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import net.neoforged.fml.loading.FMLPaths
|
||||||
|
|
||||||
|
data class DiscordSettings(
|
||||||
|
var botToken: String = "",
|
||||||
|
var channelId: String = "",
|
||||||
|
var serverAddress: String = "",
|
||||||
|
var features: Features = Features(),
|
||||||
|
var messages: Messages = Messages()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Features(
|
||||||
|
var chatSync: Boolean = true,
|
||||||
|
var joinLeave: Boolean = true,
|
||||||
|
var serverStatus: Boolean = true,
|
||||||
|
var death: Boolean = true,
|
||||||
|
var advancement: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Messages(
|
||||||
|
var join: String = "**{player}**님이 서버에 접속했습니다.",
|
||||||
|
var leave: String = "**{player}**님이 서버에서 퇴장했습니다.",
|
||||||
|
var serverStart: String = "🟢 서버가 시작되었습니다.",
|
||||||
|
var serverStop: String = "🔴 서버가 종료되었습니다."
|
||||||
|
)
|
||||||
|
|
||||||
|
object DiscordConfig {
|
||||||
|
private val CONFIG_DIR: Path = FMLPaths.CONFIGDIR.get().resolve(DiscordBot.ID)
|
||||||
|
private val CONFIG_FILE: Path = CONFIG_DIR.resolve("config.json")
|
||||||
|
private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
|
|
||||||
|
var settings: DiscordSettings = DiscordSettings()
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(CONFIG_DIR)
|
||||||
|
|
||||||
|
if (Files.exists(CONFIG_FILE)) {
|
||||||
|
val json = Files.readString(CONFIG_FILE)
|
||||||
|
settings = gson.fromJson(json, DiscordSettings::class.java) ?: DiscordSettings()
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] 설정 파일 로드 완료")
|
||||||
|
} else {
|
||||||
|
// 기본 설정 파일 생성
|
||||||
|
save()
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] 기본 설정 파일 생성됨: $CONFIG_FILE")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DiscordBot.LOGGER.error("[DiscordBot] 설정 로드 실패", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(CONFIG_DIR)
|
||||||
|
val json = gson.toJson(settings)
|
||||||
|
Files.writeString(CONFIG_FILE, json)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DiscordBot.LOGGER.error("[DiscordBot] 설정 저장 실패", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
package com.beemer.discordbot.discord
|
||||||
|
|
||||||
|
import com.beemer.discordbot.DiscordBot
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import java.awt.Color
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder
|
||||||
|
import net.dv8tion.jda.api.JDA
|
||||||
|
import net.dv8tion.jda.api.JDABuilder
|
||||||
|
import net.dv8tion.jda.api.entities.Activity
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent
|
||||||
|
import net.minecraft.server.MinecraftServer
|
||||||
|
|
||||||
|
object BotManager {
|
||||||
|
private var jda: JDA? = null
|
||||||
|
private var server: MinecraftServer? = null
|
||||||
|
private var serverStartTime: Instant? = null
|
||||||
|
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
||||||
|
|
||||||
|
fun start(minecraftServer: MinecraftServer) {
|
||||||
|
server = minecraftServer
|
||||||
|
serverStartTime = Instant.now()
|
||||||
|
|
||||||
|
val token = DiscordConfig.settings.botToken
|
||||||
|
if (token.isBlank()) {
|
||||||
|
DiscordBot.LOGGER.warn(
|
||||||
|
"[DiscordBot] 봇 토큰이 설정되지 않았습니다. config/discordbot/config.json을 확인하세요."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jda =
|
||||||
|
JDABuilder.createDefault(token)
|
||||||
|
.enableIntents(
|
||||||
|
GatewayIntent.MESSAGE_CONTENT,
|
||||||
|
GatewayIntent.GUILD_MESSAGES
|
||||||
|
)
|
||||||
|
.addEventListeners(DiscordListener(minecraftServer))
|
||||||
|
.build()
|
||||||
|
.awaitReady()
|
||||||
|
|
||||||
|
// 봇 상태 업데이트 스케줄러 시작 (1분마다)
|
||||||
|
startStatusUpdater()
|
||||||
|
|
||||||
|
// 채널 토픽에 서버 주소 설정
|
||||||
|
updateChannelTopic()
|
||||||
|
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] 디스코드 봇 연결 성공!")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DiscordBot.LOGGER.error("[DiscordBot] 디스코드 봇 연결 실패", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 봇 상태(Activity)를 주기적으로 업데이트 */
|
||||||
|
private fun startStatusUpdater() {
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
updateBotStatus()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DiscordBot.LOGGER.error("[DiscordBot] 상태 업데이트 실패", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 봇 상태 업데이트 - 서버 구동 시간 표시 */
|
||||||
|
private fun updateBotStatus() {
|
||||||
|
val startTime = serverStartTime ?: return
|
||||||
|
val now = Instant.now()
|
||||||
|
val duration = java.time.Duration.between(startTime, now)
|
||||||
|
val hours = duration.toHours()
|
||||||
|
val minutes = duration.toMinutes() % 60
|
||||||
|
|
||||||
|
val statusText =
|
||||||
|
if (hours > 0) {
|
||||||
|
"⏱️ ${hours}시간 ${minutes}분 동안 서버 구동 중"
|
||||||
|
} else {
|
||||||
|
"⏱️ ${minutes}분 동안 서버 구동 중"
|
||||||
|
}
|
||||||
|
|
||||||
|
jda?.presence?.setPresence(Activity.playing(statusText), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 채널 토픽에 서버 주소 설정 */
|
||||||
|
private fun updateChannelTopic() {
|
||||||
|
val serverAddress = DiscordConfig.settings.serverAddress
|
||||||
|
if (serverAddress.isBlank()) return
|
||||||
|
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val topic = "🪧 서버 주소: $serverAddress"
|
||||||
|
channel.manager
|
||||||
|
.setTopic(topic)
|
||||||
|
.queue(
|
||||||
|
{ DiscordBot.LOGGER.info("[DiscordBot] 채널 토픽 업데이트 완료: $topic") },
|
||||||
|
{ error -> DiscordBot.LOGGER.error("[DiscordBot] 채널 토픽 업데이트 실패", error) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
scheduler.shutdown()
|
||||||
|
jda?.shutdown()
|
||||||
|
jda = null
|
||||||
|
server = null
|
||||||
|
serverStartTime = null
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] 디스코드 봇 연결 해제")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restart() {
|
||||||
|
val currentServer = server
|
||||||
|
stop()
|
||||||
|
if (currentServer != null) {
|
||||||
|
start(currentServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage(message: String) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
channel.sendMessage(message).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 마인크래프트 채팅 → 디스코드 전송 */
|
||||||
|
fun sendChatMessage(playerName: String, displayName: String, message: String) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(88, 101, 242)) // 디스코드 블루
|
||||||
|
.setAuthor(displayName, null, avatarUrl)
|
||||||
|
.setDescription(message)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 플레이어 접속 Embed 전송 */
|
||||||
|
fun sendPlayerJoinEmbed(playerName: String, displayName: String, uuid: UUID) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(87, 242, 135)) // 초록색
|
||||||
|
.setAuthor("${displayName}님이 서버에 접속했습니다", null, avatarUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 플레이어 첫 접속 Embed 전송 */
|
||||||
|
fun sendPlayerFirstJoinEmbed(playerName: String, displayName: String, uuid: UUID) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(254, 231, 92)) // 노란색
|
||||||
|
.setAuthor("🎉 ${displayName}님이 서버에 처음 접속했습니다!", null, avatarUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 플레이어 퇴장 Embed 전송 */
|
||||||
|
fun sendPlayerLeaveEmbed(playerName: String, displayName: String, uuid: UUID) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(237, 66, 69)) // 빨간색
|
||||||
|
.setAuthor("${displayName}님이 서버에서 퇴장했습니다", null, avatarUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 서버 상태 Embed 전송 */
|
||||||
|
fun sendServerStatusEmbed(isStart: Boolean) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val (title, color) =
|
||||||
|
if (isStart) {
|
||||||
|
"🟢 서버가 시작되었습니다" to Color(87, 242, 135)
|
||||||
|
} else {
|
||||||
|
"🔴 서버가 종료되었습니다" to Color(237, 66, 69)
|
||||||
|
}
|
||||||
|
|
||||||
|
val embed = EmbedBuilder().setColor(color).setDescription(title).build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean = jda != null
|
||||||
|
|
||||||
|
/** 플레이어 사망 Embed 전송 */
|
||||||
|
fun sendDeathEmbed(playerName: String, displayName: String, deathMessage: String) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(149, 100, 100)) // 밝은 회갈색
|
||||||
|
.setAuthor("💀 $deathMessage", null, avatarUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 발전 과제 달성 Embed 전송 */
|
||||||
|
fun sendAdvancementEmbed(
|
||||||
|
playerName: String,
|
||||||
|
displayName: String,
|
||||||
|
advancementTitle: String,
|
||||||
|
advancementDesc: String
|
||||||
|
) {
|
||||||
|
val channelId = DiscordConfig.settings.channelId
|
||||||
|
if (channelId.isBlank()) return
|
||||||
|
|
||||||
|
val channel = jda?.getTextChannelById(channelId) ?: return
|
||||||
|
|
||||||
|
val avatarUrl = "https://minotar.net/helm/$playerName/64.png"
|
||||||
|
|
||||||
|
val embed =
|
||||||
|
EmbedBuilder()
|
||||||
|
.setColor(Color(169, 112, 255)) // 보라색
|
||||||
|
.setAuthor("🏆 ${displayName}님이 발전 과제를 달성했습니다!", null, avatarUrl)
|
||||||
|
.setDescription("**$advancementTitle**\n> _${advancementDesc}_")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed).queue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.beemer.discordbot.discord
|
||||||
|
|
||||||
|
import com.beemer.discordbot.DiscordBot
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter
|
||||||
|
import net.minecraft.ChatFormatting
|
||||||
|
import net.minecraft.network.chat.Component
|
||||||
|
import net.minecraft.server.MinecraftServer
|
||||||
|
|
||||||
|
class DiscordListener(private val server: MinecraftServer) : ListenerAdapter() {
|
||||||
|
override fun onMessageReceived(event: MessageReceivedEvent) {
|
||||||
|
// 봇 메시지 무시
|
||||||
|
if (event.author.isBot) return
|
||||||
|
|
||||||
|
// 채팅 동기화 비활성화 시 무시
|
||||||
|
if (!DiscordConfig.settings.features.chatSync) return
|
||||||
|
|
||||||
|
// 설정된 채널이 아닌 경우 무시
|
||||||
|
if (event.channel.id != DiscordConfig.settings.channelId) return
|
||||||
|
|
||||||
|
val username = event.member?.effectiveName ?: event.author.name
|
||||||
|
val message = event.message.contentDisplay
|
||||||
|
|
||||||
|
// 빈 메시지 무시
|
||||||
|
if (message.isBlank()) return
|
||||||
|
|
||||||
|
// 마인크래프트 서버에 메시지 전송
|
||||||
|
// #7883f4 = 7898100 (10진수)
|
||||||
|
val discordColor = 0x7883f4
|
||||||
|
|
||||||
|
val chatMessage =
|
||||||
|
Component.literal("[디스코드] ")
|
||||||
|
.withStyle { it.withColor(discordColor) }
|
||||||
|
.append(
|
||||||
|
Component.literal(username).withStyle {
|
||||||
|
it.withColor(ChatFormatting.YELLOW)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
Component.literal(": ").withStyle {
|
||||||
|
it.withColor(ChatFormatting.GRAY)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
Component.literal(message).withStyle {
|
||||||
|
it.withColor(ChatFormatting.WHITE)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
server.playerList.players.forEach { player -> player.sendSystemMessage(chatMessage) }
|
||||||
|
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] Discord -> MC: <$username> $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.beemer.discordbot.event
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
import com.beemer.discordbot.util.TranslationManager
|
||||||
|
import java.util.UUID
|
||||||
|
import net.minecraft.server.level.ServerPlayer
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent
|
||||||
|
import net.neoforged.neoforge.event.entity.player.AdvancementEvent
|
||||||
|
|
||||||
|
object AdvancementEvents {
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onAdvancement(event: AdvancementEvent.AdvancementProgressEvent) {
|
||||||
|
if (!DiscordConfig.settings.features.advancement) return
|
||||||
|
|
||||||
|
// 업적 완료가 아닌 경우 무시
|
||||||
|
val advancementProgress = event.advancementProgress
|
||||||
|
if (!advancementProgress.isDone) return
|
||||||
|
|
||||||
|
val player = event.entity as? ServerPlayer ?: return
|
||||||
|
val advancement = event.advancement
|
||||||
|
|
||||||
|
// 레시피 업적은 무시
|
||||||
|
val advancementId = advancement.id.toString()
|
||||||
|
if (advancementId.contains("recipe")) return
|
||||||
|
|
||||||
|
// 표시되지 않는 업적은 무시
|
||||||
|
val display = advancement.value.display.orElse(null) ?: return
|
||||||
|
if (!display.shouldAnnounceChat()) return
|
||||||
|
|
||||||
|
val playerName = player.gameProfile.name
|
||||||
|
val uuid = player.uuid
|
||||||
|
|
||||||
|
// 번역된 업적 제목과 설명 가져오기
|
||||||
|
val advancementTitle =
|
||||||
|
TranslationManager.getAdvancementTitle(advancementId) ?: display.title.string
|
||||||
|
val advancementDesc =
|
||||||
|
TranslationManager.getAdvancementDescription(advancementId)
|
||||||
|
?: display.description.string
|
||||||
|
|
||||||
|
// Essentials 모드의 닉네임 가져오기 시도
|
||||||
|
val displayName = getNickname(uuid) ?: playerName
|
||||||
|
|
||||||
|
BotManager.sendAdvancementEmbed(playerName, displayName, advancementTitle, advancementDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Essentials 모드의 NicknameDataStore에서 닉네임 가져오기 */
|
||||||
|
private fun getNickname(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
val clazz = Class.forName("com.beemer.essentials.nickname.NicknameDataStore")
|
||||||
|
val instance = clazz.kotlin.objectInstance ?: return null
|
||||||
|
val method = clazz.getMethod("getNickname", UUID::class.java)
|
||||||
|
method.invoke(instance, uuid) as? String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.beemer.discordbot.event
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
import java.util.UUID
|
||||||
|
import net.neoforged.bus.api.EventPriority
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent
|
||||||
|
import net.neoforged.neoforge.event.ServerChatEvent
|
||||||
|
|
||||||
|
object ChatEvents {
|
||||||
|
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||||
|
fun onServerChat(event: ServerChatEvent) {
|
||||||
|
if (!DiscordConfig.settings.features.chatSync) return
|
||||||
|
|
||||||
|
val playerName = event.player.gameProfile.name
|
||||||
|
val uuid = event.player.uuid
|
||||||
|
val message = event.rawText
|
||||||
|
|
||||||
|
// 명령어 무시
|
||||||
|
if (message.startsWith("/")) return
|
||||||
|
|
||||||
|
// Essentials 모드의 닉네임 가져오기 시도
|
||||||
|
val displayName = getNickname(uuid) ?: playerName
|
||||||
|
|
||||||
|
BotManager.sendChatMessage(playerName, displayName, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Essentials 모드의 NicknameDataStore에서 닉네임 가져오기 */
|
||||||
|
private fun getNickname(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
val clazz = Class.forName("com.beemer.essentials.nickname.NicknameDataStore")
|
||||||
|
val instance = clazz.kotlin.objectInstance ?: return null
|
||||||
|
val method = clazz.getMethod("getNickname", UUID::class.java)
|
||||||
|
method.invoke(instance, uuid) as? String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.beemer.discordbot.event
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
import com.beemer.discordbot.util.TranslationManager
|
||||||
|
import java.util.UUID
|
||||||
|
import net.minecraft.server.level.ServerPlayer
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent
|
||||||
|
import net.neoforged.neoforge.event.entity.living.LivingDeathEvent
|
||||||
|
|
||||||
|
object DeathEvents {
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onPlayerDeath(event: LivingDeathEvent) {
|
||||||
|
if (!DiscordConfig.settings.features.death) return
|
||||||
|
|
||||||
|
val entity = event.entity
|
||||||
|
if (entity !is ServerPlayer) return
|
||||||
|
|
||||||
|
val playerName = entity.gameProfile.name
|
||||||
|
val uuid = entity.uuid
|
||||||
|
|
||||||
|
// Essentials 모드의 닉네임 가져오기
|
||||||
|
val displayName = getNickname(uuid) ?: playerName
|
||||||
|
|
||||||
|
// 사망 원인 ID
|
||||||
|
val msgId = event.source.type().msgId()
|
||||||
|
|
||||||
|
// 킬러 이름 (있는 경우)
|
||||||
|
val killerName =
|
||||||
|
event.source.entity?.let { killer ->
|
||||||
|
if (killer is ServerPlayer) {
|
||||||
|
getNickname(killer.uuid) ?: killer.gameProfile.name
|
||||||
|
} else {
|
||||||
|
// 몹 이름 번역 시도
|
||||||
|
val mobKey = killer.type.descriptionId
|
||||||
|
TranslationManager.translate(mobKey) ?: killer.name.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 번역된 사망 메시지 가져오기
|
||||||
|
val deathMessage =
|
||||||
|
TranslationManager.getDeathMessage(msgId, displayName, killerName)
|
||||||
|
?: event.source
|
||||||
|
.getLocalizedDeathMessage(entity)
|
||||||
|
.string
|
||||||
|
.replace(playerName, displayName)
|
||||||
|
|
||||||
|
BotManager.sendDeathEmbed(playerName, displayName, deathMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Essentials 모드의 NicknameDataStore에서 닉네임 가져오기 */
|
||||||
|
private fun getNickname(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
val clazz = Class.forName("com.beemer.essentials.nickname.NicknameDataStore")
|
||||||
|
val instance = clazz.kotlin.objectInstance ?: return null
|
||||||
|
val method = clazz.getMethod("getNickname", UUID::class.java)
|
||||||
|
method.invoke(instance, uuid) as? String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.beemer.discordbot.event
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
import java.util.UUID
|
||||||
|
import net.neoforged.bus.api.EventPriority
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent
|
||||||
|
import net.neoforged.neoforge.event.entity.player.PlayerEvent
|
||||||
|
|
||||||
|
object PlayerEvents {
|
||||||
|
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||||
|
fun onPlayerJoin(event: PlayerEvent.PlayerLoggedInEvent) {
|
||||||
|
if (!DiscordConfig.settings.features.joinLeave) return
|
||||||
|
|
||||||
|
val playerName = event.entity.gameProfile.name
|
||||||
|
val uuid = event.entity.uuid
|
||||||
|
|
||||||
|
// 첫 접속인지 확인 (Essentials보다 먼저 체크)
|
||||||
|
val isFirstJoin = !isKnownPlayer(uuid)
|
||||||
|
|
||||||
|
// Essentials 모드의 닉네임 가져오기 시도
|
||||||
|
val displayName = getNickname(uuid) ?: playerName
|
||||||
|
|
||||||
|
if (isFirstJoin) {
|
||||||
|
BotManager.sendPlayerFirstJoinEmbed(playerName, displayName, uuid)
|
||||||
|
} else {
|
||||||
|
BotManager.sendPlayerJoinEmbed(playerName, displayName, uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onPlayerLeave(event: PlayerEvent.PlayerLoggedOutEvent) {
|
||||||
|
if (!DiscordConfig.settings.features.joinLeave) return
|
||||||
|
|
||||||
|
val playerName = event.entity.gameProfile.name
|
||||||
|
val uuid = event.entity.uuid
|
||||||
|
|
||||||
|
// Essentials 모드의 닉네임 가져오기 시도
|
||||||
|
val displayName = getNickname(uuid) ?: playerName
|
||||||
|
|
||||||
|
BotManager.sendPlayerLeaveEmbed(playerName, displayName, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Essentials 모드의 NicknameDataStore에서 닉네임 가져오기 */
|
||||||
|
private fun getNickname(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
val clazz = Class.forName("com.beemer.essentials.nickname.NicknameDataStore")
|
||||||
|
val instance = clazz.kotlin.objectInstance ?: return null
|
||||||
|
val method = clazz.getMethod("getNickname", UUID::class.java)
|
||||||
|
method.invoke(instance, uuid) as? String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Essentials 모드의 PlayerConfig에서 플레이어가 등록되어 있는지 확인 */
|
||||||
|
private fun isKnownPlayer(uuid: UUID): Boolean {
|
||||||
|
return try {
|
||||||
|
val clazz = Class.forName("com.beemer.essentials.config.PlayerConfig")
|
||||||
|
val instance = clazz.kotlin.objectInstance ?: return false
|
||||||
|
val method = clazz.getMethod("isKnownPlayer", String::class.java)
|
||||||
|
method.invoke(instance, uuid.toString()) as? Boolean ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.beemer.discordbot.event
|
||||||
|
|
||||||
|
import com.beemer.discordbot.config.DiscordConfig
|
||||||
|
import com.beemer.discordbot.discord.BotManager
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStartedEvent
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent
|
||||||
|
|
||||||
|
object ServerEvents {
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onServerStarted(event: ServerStartedEvent) {
|
||||||
|
// 봇 시작 (서버 인스턴스 전달)
|
||||||
|
BotManager.start(event.server)
|
||||||
|
|
||||||
|
// 서버 시작 알림
|
||||||
|
if (DiscordConfig.settings.features.serverStatus) {
|
||||||
|
BotManager.sendServerStatusEmbed(isStart = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
fun onServerStopping(event: ServerStoppingEvent) {
|
||||||
|
// 서버 종료 알림
|
||||||
|
if (DiscordConfig.settings.features.serverStatus) {
|
||||||
|
BotManager.sendServerStatusEmbed(isStart = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 봇 종료
|
||||||
|
BotManager.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package com.beemer.discordbot.util
|
||||||
|
|
||||||
|
import com.beemer.discordbot.DiscordBot
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
object TranslationManager {
|
||||||
|
private val gson = Gson()
|
||||||
|
private var translations: Map<String, String> = emptyMap()
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
try {
|
||||||
|
// 모드 리소스에서 번역 파일 로드
|
||||||
|
val resourcePath = "/assets/discordbot/lang/ko_kr.json"
|
||||||
|
val inputStream = TranslationManager::class.java.getResourceAsStream(resourcePath)
|
||||||
|
|
||||||
|
if (inputStream != null) {
|
||||||
|
InputStreamReader(inputStream, Charsets.UTF_8).use { reader ->
|
||||||
|
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||||
|
translations = gson.fromJson(reader, type) ?: emptyMap()
|
||||||
|
DiscordBot.LOGGER.info("[DiscordBot] 번역 파일 로드 완료: ${translations.size}개")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DiscordBot.LOGGER.warn("[DiscordBot] 번역 파일을 찾을 수 없습니다: $resourcePath")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
DiscordBot.LOGGER.error("[DiscordBot] 번역 파일 로드 실패", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() = load()
|
||||||
|
|
||||||
|
/** 번역 키로 번역된 문자열 가져오기 */
|
||||||
|
fun translate(key: String): String? {
|
||||||
|
return translations[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사망 메시지 번역 - msgId로 찾기 */
|
||||||
|
fun getDeathMessage(msgId: String, playerName: String, killerName: String? = null): String? {
|
||||||
|
// msgId 형식: "inFire", "lava", "dragonBreath.player" 등
|
||||||
|
// 번역 키 형식: "death.attack.inFire", "death.attack.lava" 등
|
||||||
|
|
||||||
|
// 먼저 player 버전 시도 (킬러가 있는 경우)
|
||||||
|
if (killerName != null) {
|
||||||
|
val playerKey = "death.attack.$msgId.player"
|
||||||
|
val playerTemplate = translations[playerKey]
|
||||||
|
if (playerTemplate != null) {
|
||||||
|
return formatDeathMessage(playerTemplate, playerName, killerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 버전 시도
|
||||||
|
val key = "death.attack.$msgId"
|
||||||
|
val template = translations[key] ?: return null
|
||||||
|
|
||||||
|
return formatDeathMessage(template, playerName, killerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDeathMessage(
|
||||||
|
template: String,
|
||||||
|
playerName: String,
|
||||||
|
killerName: String?
|
||||||
|
): String {
|
||||||
|
var result = template
|
||||||
|
result = result.replace("%1\$s", playerName)
|
||||||
|
if (killerName != null) {
|
||||||
|
result = result.replace("%2\$s", killerName)
|
||||||
|
result = result.replace("%3\$s", "") // 무기 등 추가 인자
|
||||||
|
}
|
||||||
|
// 남은 플레이스홀더 제거
|
||||||
|
result = result.replace(Regex("%\\d+\\\$s"), "")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 업적 제목 번역 */
|
||||||
|
fun getAdvancementTitle(advancementId: String): String? {
|
||||||
|
// minecraft:nether/return_to_sender -> advancements.nether.return_to_sender.title
|
||||||
|
val key =
|
||||||
|
"advancements." +
|
||||||
|
advancementId.removePrefix("minecraft:").replace("/", ".") +
|
||||||
|
".title"
|
||||||
|
return translations[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 업적 설명 번역 */
|
||||||
|
fun getAdvancementDescription(advancementId: String): String? {
|
||||||
|
val key =
|
||||||
|
"advancements." +
|
||||||
|
advancementId.removePrefix("minecraft:").replace("/", ".") +
|
||||||
|
".description"
|
||||||
|
return translations[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
533
DiscordBot/src/main/resources/assets/discordbot/lang/ko_kr.json
Normal file
533
DiscordBot/src/main/resources/assets/discordbot/lang/ko_kr.json
Normal file
|
|
@ -0,0 +1,533 @@
|
||||||
|
{
|
||||||
|
"advancements.adventure.adventuring_time.description": "모든 생물 군계를 발견하세요",
|
||||||
|
"advancements.adventure.adventuring_time.title": "모험의 시간",
|
||||||
|
"advancements.adventure.arbalistic.description": "쇠뇌 한 발로 종류가 다른 몹 다섯 마리를 죽이세요",
|
||||||
|
"advancements.adventure.arbalistic.title": "명사수",
|
||||||
|
"advancements.adventure.avoid_vibration.description": "스컬크 감지체나 워든 근처에서 웅크린 채 움직여 당신을 감지하지 못하게 하세요",
|
||||||
|
"advancements.adventure.avoid_vibration.title": "은밀하게 위대하게",
|
||||||
|
"advancements.adventure.blowback.description": "브리즈가 쏜 돌풍구를 튕겨내 브리즈를 죽이세요",
|
||||||
|
"advancements.adventure.blowback.title": "역풍",
|
||||||
|
"advancements.adventure.brush_armadillo.description": "솔을 사용해 아르마딜로에게서 아르마딜로 인갑을 얻으세요",
|
||||||
|
"advancements.adventure.brush_armadillo.title": "이게 인갑인갑다",
|
||||||
|
"advancements.adventure.bullseye.description": "30미터 이상 떨어진 곳에서 과녁 블록의 정중앙을 맞추세요",
|
||||||
|
"advancements.adventure.bullseye.title": "명중",
|
||||||
|
"advancements.adventure.craft_decorated_pot_using_only_sherds.description": "도자기 조각 4개로 장식된 도자기를 만드세요",
|
||||||
|
"advancements.adventure.craft_decorated_pot_using_only_sherds.title": "세심한 복원",
|
||||||
|
"advancements.adventure.crafters_crafting_crafters.description": "제작기가 제작기를 제작할 때 근처에 있으세요",
|
||||||
|
"advancements.adventure.crafters_crafting_crafters.title": "제작기 제 자신 제작기",
|
||||||
|
"advancements.adventure.fall_from_world_height.description": "세계의 위쪽 끝(건축 제한)에서 아래쪽 끝까지 자유 낙하하고 살아남으세요",
|
||||||
|
"advancements.adventure.fall_from_world_height.title": "동굴과 절벽",
|
||||||
|
"advancements.adventure.hero_of_the_village.description": "습격으로부터 마을을 지켜내세요",
|
||||||
|
"advancements.adventure.hero_of_the_village.title": "마을의 영웅",
|
||||||
|
"advancements.adventure.honey_block_slide.description": "꿀 블록을 향해 점프해 낙하를 멈추세요",
|
||||||
|
"advancements.adventure.honey_block_slide.title": "달콤함에 몸을 맡기다",
|
||||||
|
"advancements.adventure.kill_a_mob.description": "적대적 몬스터를 죽이세요",
|
||||||
|
"advancements.adventure.kill_a_mob.title": "몬스터 사냥꾼",
|
||||||
|
"advancements.adventure.kill_all_mobs.description": "모든 적대적 몬스터를 하나 이상씩 죽이세요",
|
||||||
|
"advancements.adventure.kill_all_mobs.title": "몬스터 도감",
|
||||||
|
"advancements.adventure.kill_mob_near_sculk_catalyst.description": "스컬크 촉매 근처에서 몹을 죽이세요",
|
||||||
|
"advancements.adventure.kill_mob_near_sculk_catalyst.title": "퍼져간다",
|
||||||
|
"advancements.adventure.lighten_up.description": "구리 전구를 도끼로 긁어내 더 밝아지게 하세요",
|
||||||
|
"advancements.adventure.lighten_up.title": "불 좀 켜 줄래?",
|
||||||
|
"advancements.adventure.lightning_rod_with_villager_no_fire.description": "주민의 감전 사고를 화재 없이 막으세요",
|
||||||
|
"advancements.adventure.lightning_rod_with_villager_no_fire.title": "번개 멈춰!",
|
||||||
|
"advancements.adventure.minecraft_trials_edition.description": "시련의 회당에 발을 들여놓으세요",
|
||||||
|
"advancements.adventure.minecraft_trials_edition.title": "인생은 시련의 연속",
|
||||||
|
"advancements.adventure.ol_betsy.description": "쇠뇌를 쏘세요",
|
||||||
|
"advancements.adventure.ol_betsy.title": "부러진 화살",
|
||||||
|
"advancements.adventure.overoverkill.description": "철퇴를 사용해 단 한 번의 타격으로 피해를 하트 50개만큼 입히세요",
|
||||||
|
"advancements.adventure.overoverkill.title": "하늘에서 철퇴가 내린다면",
|
||||||
|
"advancements.adventure.play_jukebox_in_meadows.description": "주크박스로 음악을 틀어 목초지에 활기를 불어넣으세요",
|
||||||
|
"advancements.adventure.play_jukebox_in_meadows.title": "사운드 오브 뮤직",
|
||||||
|
"advancements.adventure.read_power_from_chiseled_bookshelf.description": "비교기를 사용하여 조각된 책장의 레드스톤 신호를 측정하세요",
|
||||||
|
"advancements.adventure.read_power_from_chiseled_bookshelf.title": "책의 힘",
|
||||||
|
"advancements.adventure.revaulting.description": "불길한 시련 열쇠로 불길한 금고의 잠금을 푸세요",
|
||||||
|
"advancements.adventure.revaulting.title": "불길에 뛰어드는 나",
|
||||||
|
"advancements.adventure.root.description": "모험, 탐사와 전투",
|
||||||
|
"advancements.adventure.root.title": "모험",
|
||||||
|
"advancements.adventure.salvage_sherd.description": "수상한 블록을 솔질해 도자기 조각을 얻으세요",
|
||||||
|
"advancements.adventure.salvage_sherd.title": "소중한 잔해",
|
||||||
|
"advancements.adventure.shoot_arrow.description": "화살로 무언가를 맞히세요",
|
||||||
|
"advancements.adventure.shoot_arrow.title": "정조준",
|
||||||
|
"advancements.adventure.sleep_in_bed.description": "침대에서 자서 리스폰 지점을 바꾸세요",
|
||||||
|
"advancements.adventure.sleep_in_bed.title": "달콤한 꿈",
|
||||||
|
"advancements.adventure.sniper_duel.description": "50미터 이상 떨어져 있는 스켈레톤을 죽이세요",
|
||||||
|
"advancements.adventure.sniper_duel.title": "저격 대결",
|
||||||
|
"advancements.adventure.spyglass_at_dragon.description": "망원경으로 엔더 드래곤을 바라보세요",
|
||||||
|
"advancements.adventure.spyglass_at_dragon.title": "비행기인가?",
|
||||||
|
"advancements.adventure.spyglass_at_ghast.description": "망원경으로 가스트를 바라보세요",
|
||||||
|
"advancements.adventure.spyglass_at_ghast.title": "풍선인가?",
|
||||||
|
"advancements.adventure.spyglass_at_parrot.description": "망원경으로 앵무새를 바라보세요",
|
||||||
|
"advancements.adventure.spyglass_at_parrot.title": "새인가?",
|
||||||
|
"advancements.adventure.summon_iron_golem.description": "마을 방어를 돕기 위해 철 골렘을 소환하세요",
|
||||||
|
"advancements.adventure.summon_iron_golem.title": "도우미 고용",
|
||||||
|
"advancements.adventure.throw_trident.description": "무언가를 향해 삼지창을 던지세요.\n참고: 가지고 있는 유일한 무기를 내던지는 것은 좋은 생각이 아닙니다.",
|
||||||
|
"advancements.adventure.throw_trident.title": "준비하시고... 쏘세요!",
|
||||||
|
"advancements.adventure.totem_of_undying.description": "불사의 토템으로 죽음을 면하세요",
|
||||||
|
"advancements.adventure.totem_of_undying.title": "죽음을 초월한 자",
|
||||||
|
"advancements.adventure.trade.description": "주민과 거래하세요",
|
||||||
|
"advancements.adventure.trade.title": "훌륭한 거래군요!",
|
||||||
|
"advancements.adventure.trade_at_world_height.description": "건축 높이 제한에서 주민과 거래하세요",
|
||||||
|
"advancements.adventure.trade_at_world_height.title": "최고의 거래",
|
||||||
|
"advancements.adventure.trim_with_all_exclusive_armor_patterns.description": "다음 대장장이 형판을 모두 사용해 보세요: 첨탑, 돼지 코, 갈비뼈, 파수, 고요, 벡스, 물결, 길잡이",
|
||||||
|
"advancements.adventure.trim_with_all_exclusive_armor_patterns.title": "형판 좋은 대장장이",
|
||||||
|
"advancements.adventure.trim_with_any_armor_pattern.description": "대장장이 작업대에서 장식된 갑옷을 제작하세요",
|
||||||
|
"advancements.adventure.trim_with_any_armor_pattern.title": "유행의 선도자",
|
||||||
|
"advancements.adventure.two_birds_one_arrow.description": "관통 화살 한 발로 팬텀 두 마리를 죽이세요",
|
||||||
|
"advancements.adventure.two_birds_one_arrow.title": "일전쌍조",
|
||||||
|
"advancements.adventure.under_lock_and_key.description": "시련 열쇠로 금고의 잠금을 푸세요",
|
||||||
|
"advancements.adventure.under_lock_and_key.title": "열쇠로 잠금 해제",
|
||||||
|
"advancements.adventure.very_very_frightening.description": "주민에게 벼락을 떨어뜨리세요",
|
||||||
|
"advancements.adventure.very_very_frightening.title": "동에 번쩍 서에 번쩍",
|
||||||
|
"advancements.adventure.voluntary_exile.description": "습격 대장을 죽이세요.\n당분간 마을에서 떨어져 있는 게 좋을지도 몰라요...",
|
||||||
|
"advancements.adventure.voluntary_exile.title": "자진 유배",
|
||||||
|
"advancements.adventure.walk_on_powder_snow_with_leather_boots.description": "가루눈 위를 걸으세요... 빠지지 않고요",
|
||||||
|
"advancements.adventure.walk_on_powder_snow_with_leather_boots.title": "토끼처럼 가볍게",
|
||||||
|
"advancements.adventure.who_needs_rockets.description": "돌풍구를 사용해 자신을 8블록만큼 쏘아 올리세요",
|
||||||
|
"advancements.adventure.who_needs_rockets.title": "누가 로켓이 필요하대?",
|
||||||
|
"advancements.adventure.whos_the_pillager_now.description": "약탈자에게 똑같은 무기로 앙갚음해 주세요",
|
||||||
|
"advancements.adventure.whos_the_pillager_now.title": "이제 누가 약탈자지?",
|
||||||
|
"advancements.empty": "아무것도 없어 보이네요...",
|
||||||
|
"advancements.end.dragon_breath.description": "드래곤의 숨결을 유리병에 담으세요",
|
||||||
|
"advancements.end.dragon_breath.title": "양치질이 필요해 보이는걸",
|
||||||
|
"advancements.end.dragon_egg.description": "드래곤 알을 들어올리세요",
|
||||||
|
"advancements.end.dragon_egg.title": "그다음 세대",
|
||||||
|
"advancements.end.elytra.description": "겉날개를 찾으세요",
|
||||||
|
"advancements.end.elytra.title": "불가능은 없다",
|
||||||
|
"advancements.end.enter_end_gateway.description": "섬에서 탈출하세요",
|
||||||
|
"advancements.end.enter_end_gateway.title": "머나먼 휴양지",
|
||||||
|
"advancements.end.find_end_city.description": "들어가 보세요, 무슨 일 있겠어요?",
|
||||||
|
"advancements.end.find_end_city.title": "게임의 끝에서 만난 도시",
|
||||||
|
"advancements.end.kill_dragon.description": "행운을 빌어요",
|
||||||
|
"advancements.end.kill_dragon.title": "엔드 해방",
|
||||||
|
"advancements.end.levitate.description": "셜커의 공격을 맞고 50블록만큼 공중 부양하세요",
|
||||||
|
"advancements.end.levitate.title": "위쪽 공기 좋은데?",
|
||||||
|
"advancements.end.respawn_dragon.description": "엔더 드래곤을 다시 소환하세요",
|
||||||
|
"advancements.end.respawn_dragon.title": "끝 아녔어?",
|
||||||
|
"advancements.end.root.description": "끝일까요, 아니면 시작일까요?",
|
||||||
|
"advancements.end.root.title": "엔드",
|
||||||
|
"advancements.husbandry.allay_deliver_cake_to_note_block.description": "알레이가 소리 블록에 케이크를 떨구게 하세요",
|
||||||
|
"advancements.husbandry.allay_deliver_cake_to_note_block.title": "생일 축하 노래",
|
||||||
|
"advancements.husbandry.allay_deliver_item_to_player.description": "알레이에게 아이템을 건네받으세요",
|
||||||
|
"advancements.husbandry.allay_deliver_item_to_player.title": "난 너의 친구야",
|
||||||
|
"advancements.husbandry.axolotl_in_a_bucket.description": "양동이로 아홀로틀을 잡으세요",
|
||||||
|
"advancements.husbandry.axolotl_in_a_bucket.title": "귀여운 포식자",
|
||||||
|
"advancements.husbandry.balanced_diet.description": "먹을 수 있다면 모두 먹으세요. 그것이 건강에 좋지 않더라도 말이죠",
|
||||||
|
"advancements.husbandry.balanced_diet.title": "균형 잡힌 식단",
|
||||||
|
"advancements.husbandry.breed_all_animals.description": "모든 동물을 교배시키세요!",
|
||||||
|
"advancements.husbandry.breed_all_animals.title": "짝지어주기",
|
||||||
|
"advancements.husbandry.breed_an_animal.description": "동물 두 마리를 교배시키세요",
|
||||||
|
"advancements.husbandry.breed_an_animal.title": "아기는 어떻게 태어나?",
|
||||||
|
"advancements.husbandry.complete_catalogue.description": "모든 종류의 고양이를 길들이세요!",
|
||||||
|
"advancements.husbandry.complete_catalogue.title": "집사 그 자체",
|
||||||
|
"advancements.husbandry.feed_snifflet.description": "아기 스니퍼에게 먹이를 주세요",
|
||||||
|
"advancements.husbandry.feed_snifflet.title": "조그만 킁킁이",
|
||||||
|
"advancements.husbandry.fishy_business.description": "물고기를 잡으세요",
|
||||||
|
"advancements.husbandry.fishy_business.title": "강태공이 세월을 낚듯",
|
||||||
|
"advancements.husbandry.froglights.description": "보관함에 모든 개구리불을 가지세요",
|
||||||
|
"advancements.husbandry.froglights.title": "우리의 힘을 합친다면!",
|
||||||
|
"advancements.husbandry.kill_axolotl_target.description": "아홀로틀과 협력해 싸워 이기세요",
|
||||||
|
"advancements.husbandry.kill_axolotl_target.title": "우정의 치유력!",
|
||||||
|
"advancements.husbandry.leash_all_frog_variants.description": "모든 종류의 개구리를 끈으로 묶으세요",
|
||||||
|
"advancements.husbandry.leash_all_frog_variants.title": "개구리 삼총사 출동!",
|
||||||
|
"advancements.husbandry.make_a_sign_glow.description": "어떤 종류든 표지판의 글자를 빛나게 만드세요",
|
||||||
|
"advancements.husbandry.make_a_sign_glow.title": "밝은 말 고운 말",
|
||||||
|
"advancements.husbandry.netherite_hoe.description": "네더라이트 주괴로 괭이를 강화한 후, 삶의 선택들을 돌이켜 보세요",
|
||||||
|
"advancements.husbandry.netherite_hoe.title": "도를 넘은 전념",
|
||||||
|
"advancements.husbandry.obtain_sniffer_egg.description": "스니퍼 알을 얻으세요",
|
||||||
|
"advancements.husbandry.obtain_sniffer_egg.title": "흥미로운 냄새",
|
||||||
|
"advancements.husbandry.plant_any_sniffer_seed.description": "아무 스니퍼 씨앗이나 심으세요",
|
||||||
|
"advancements.husbandry.plant_any_sniffer_seed.title": "과거를 심다",
|
||||||
|
"advancements.husbandry.plant_seed.description": "씨앗을 심고 자라나는 것을 지켜보세요",
|
||||||
|
"advancements.husbandry.plant_seed.title": "씨앗이 자라나는 곳",
|
||||||
|
"advancements.husbandry.remove_wolf_armor.description": "가위를 사용해 늑대에게서 늑대 갑옷을 벗기세요",
|
||||||
|
"advancements.husbandry.remove_wolf_armor.title": "탁월한 가위질",
|
||||||
|
"advancements.husbandry.repair_wolf_armor.description": "아르마딜로 인갑을 사용해 손상된 늑대 갑옷을 수리하세요",
|
||||||
|
"advancements.husbandry.repair_wolf_armor.title": "새것처럼",
|
||||||
|
"advancements.husbandry.ride_a_boat_with_a_goat.description": "염소와 함께 보트에 타세요",
|
||||||
|
"advancements.husbandry.ride_a_boat_with_a_goat.title": "염소 떴소",
|
||||||
|
"advancements.husbandry.root.description": "세상은 친구들과 음식으로 가득 차 있어요",
|
||||||
|
"advancements.husbandry.root.title": "농사",
|
||||||
|
"advancements.husbandry.safely_harvest_honey.description": "꿀벌을 자극하지 않도록 모닥불을 사용해 벌통에 든 꿀을 유리병에 담으세요",
|
||||||
|
"advancements.husbandry.safely_harvest_honey.title": "벌집을 내 집처럼",
|
||||||
|
"advancements.husbandry.silk_touch_nest.description": "벌 세 마리가 들어 있는 벌집을 섬세한 손길을 사용해 옮기세요",
|
||||||
|
"advancements.husbandry.silk_touch_nest.title": "한 벌 한 벌 정성껏 모시겠습니다",
|
||||||
|
"advancements.husbandry.tactical_fishing.description": "물고기를 잡으세요... 낚싯대 없이요!",
|
||||||
|
"advancements.husbandry.tactical_fishing.title": "이 대신 잇몸으로",
|
||||||
|
"advancements.husbandry.tadpole_in_a_bucket.description": "양동이로 올챙이를 잡으세요",
|
||||||
|
"advancements.husbandry.tadpole_in_a_bucket.title": "양동이에 올챙이 한 마리",
|
||||||
|
"advancements.husbandry.tame_an_animal.description": "동물을 길들이세요",
|
||||||
|
"advancements.husbandry.tame_an_animal.title": "인생의 동반자",
|
||||||
|
"advancements.husbandry.wax_off.description": "구리 블록의 밀랍칠을 벗기세요!",
|
||||||
|
"advancements.husbandry.wax_off.title": "밀랍을 벗기자",
|
||||||
|
"advancements.husbandry.wax_on.description": "구리 블록에 벌집 조각을 사용하세요!",
|
||||||
|
"advancements.husbandry.wax_on.title": "밀랍을 칠하자",
|
||||||
|
"advancements.husbandry.whole_pack.description": "모든 종류의 늑대를 한 마리씩 길들이세요",
|
||||||
|
"advancements.husbandry.whole_pack.title": "외롭지 못한 늑대",
|
||||||
|
"advancements.nether.all_effects.description": "모든 효과를 동시에 가지세요",
|
||||||
|
"advancements.nether.all_effects.title": "어쩌다 이 지경까지",
|
||||||
|
"advancements.nether.all_potions.description": "모든 물약 효과를 동시에 가지세요",
|
||||||
|
"advancements.nether.all_potions.title": "뿅가는 폭탄주",
|
||||||
|
"advancements.nether.brew_potion.description": "물약을 만드세요",
|
||||||
|
"advancements.nether.brew_potion.title": "물약 양조장",
|
||||||
|
"advancements.nether.charge_respawn_anchor.description": "리스폰 정박기를 최대로 충전하세요",
|
||||||
|
"advancements.nether.charge_respawn_anchor.title": "목숨 충전",
|
||||||
|
"advancements.nether.create_beacon.description": "신호기를 제작하고 설치하세요",
|
||||||
|
"advancements.nether.create_beacon.title": "신호기 꾸리기",
|
||||||
|
"advancements.nether.create_full_beacon.description": "신호기의 출력을 최대로 만드세요",
|
||||||
|
"advancements.nether.create_full_beacon.title": "신호자",
|
||||||
|
"advancements.nether.distract_piglin.description": "금으로 피글린의 주의를 돌리세요",
|
||||||
|
"advancements.nether.distract_piglin.title": "반짝반짝 눈이 부셔",
|
||||||
|
"advancements.nether.explore_nether.description": "모든 네더 생물 군계를 탐험하세요",
|
||||||
|
"advancements.nether.explore_nether.title": "화끈한 관광 명소",
|
||||||
|
"advancements.nether.fast_travel.description": "네더를 이용해 오버월드의 7 km를 이동하세요",
|
||||||
|
"advancements.nether.fast_travel.title": "천 리 길도 한 걸음",
|
||||||
|
"advancements.nether.find_bastion.description": "보루 잔해에 진입하세요",
|
||||||
|
"advancements.nether.find_bastion.title": "그때가 좋았지",
|
||||||
|
"advancements.nether.find_fortress.description": "네더 요새 안으로 들어가세요",
|
||||||
|
"advancements.nether.find_fortress.title": "끔찍한 요새",
|
||||||
|
"advancements.nether.get_wither_skull.description": "위더 스켈레톤의 해골을 얻으세요",
|
||||||
|
"advancements.nether.get_wither_skull.title": "으스스한 스켈레톤",
|
||||||
|
"advancements.nether.loot_bastion.description": "보루 잔해에 있는 상자에서 노획물을 얻으세요",
|
||||||
|
"advancements.nether.loot_bastion.title": "돼지와 전쟁",
|
||||||
|
"advancements.nether.netherite_armor.description": "네더라이트 갑옷을 모두 얻으세요",
|
||||||
|
"advancements.nether.netherite_armor.title": "잔해로 날 감싸줘",
|
||||||
|
"advancements.nether.obtain_ancient_debris.description": "고대 잔해를 얻으세요",
|
||||||
|
"advancements.nether.obtain_ancient_debris.title": "깊이 파묻힌 잔해",
|
||||||
|
"advancements.nether.obtain_blaze_rod.description": "블레이즈의 막대기를 얻으세요",
|
||||||
|
"advancements.nether.obtain_blaze_rod.title": "포화 속으로",
|
||||||
|
"advancements.nether.obtain_crying_obsidian.description": "우는 흑요석을 얻으세요",
|
||||||
|
"advancements.nether.obtain_crying_obsidian.title": "누가 양파를 써나?",
|
||||||
|
"advancements.nether.return_to_sender.description": "화염구로 가스트를 죽이세요",
|
||||||
|
"advancements.nether.return_to_sender.title": "전해지지 않은 러브레터",
|
||||||
|
"advancements.nether.ride_strider.description": "뒤틀린 균 낚싯대를 들고 스트라이더 위에 타세요",
|
||||||
|
"advancements.nether.ride_strider.title": "두 발 달린 보트",
|
||||||
|
"advancements.nether.ride_strider_in_overworld_lava.description": "오버월드의 용암 호수에서 스트라이더를 타고 머어어얼리 이동하세요",
|
||||||
|
"advancements.nether.ride_strider_in_overworld_lava.title": "고향 같은 편안함",
|
||||||
|
"advancements.nether.root.description": "여름옷을 가져오세요",
|
||||||
|
"advancements.nether.root.title": "네더",
|
||||||
|
"advancements.nether.summon_wither.description": "위더를 소환하세요",
|
||||||
|
"advancements.nether.summon_wither.title": "시들어 버린 언덕",
|
||||||
|
"advancements.nether.uneasy_alliance.description": "네더에서 가스트를 구출해 오버월드로 안전하게 데려온 다음... 죽이세요",
|
||||||
|
"advancements.nether.uneasy_alliance.title": "쉽지 않은 동행",
|
||||||
|
"advancements.nether.use_lodestone.description": "자석석에 나침반을 사용하세요",
|
||||||
|
"advancements.nether.use_lodestone.title": "집으로 이끌려가네",
|
||||||
|
"advancements.progress": "%s/%s",
|
||||||
|
"advancements.sad_label": "ㅠㅠ",
|
||||||
|
"advancements.story.cure_zombie_villager.description": "좀비 주민을 약화시킨 후 치료하세요",
|
||||||
|
"advancements.story.cure_zombie_villager.title": "좀비 의사",
|
||||||
|
"advancements.story.deflect_arrow.description": "방패로 발사체를 튕겨내세요",
|
||||||
|
"advancements.story.deflect_arrow.title": "저희는 그런 것 받지 않습니다",
|
||||||
|
"advancements.story.enchant_item.description": "마법 부여대로 아이템에 마법을 부여하세요",
|
||||||
|
"advancements.story.enchant_item.title": "마법 부여자",
|
||||||
|
"advancements.story.enter_the_end.description": "엔드 차원문에 진입하세요",
|
||||||
|
"advancements.story.enter_the_end.title": "이걸로 끝이야?",
|
||||||
|
"advancements.story.enter_the_nether.description": "네더 차원문을 짓고, 불을 붙여 들어가세요",
|
||||||
|
"advancements.story.enter_the_nether.title": "더 깊은 곳으로",
|
||||||
|
"advancements.story.follow_ender_eye.description": "엔더의 눈을 따라가세요",
|
||||||
|
"advancements.story.follow_ender_eye.title": "스무고개",
|
||||||
|
"advancements.story.form_obsidian.description": "흑요석을 얻으세요",
|
||||||
|
"advancements.story.form_obsidian.title": "아이스 버킷 챌린지",
|
||||||
|
"advancements.story.iron_tools.description": "곡괭이를 개선하세요",
|
||||||
|
"advancements.story.iron_tools.title": "이젠 철 좀 들어라",
|
||||||
|
"advancements.story.lava_bucket.description": "양동이에 용암을 채우세요",
|
||||||
|
"advancements.story.lava_bucket.title": "화끈한 화제",
|
||||||
|
"advancements.story.mine_diamond.description": "다이아몬드를 얻으세요",
|
||||||
|
"advancements.story.mine_diamond.title": "다이아몬드다!",
|
||||||
|
"advancements.story.mine_stone.description": "새 곡괭이로 돌을 채굴하세요",
|
||||||
|
"advancements.story.mine_stone.title": "석기 시대",
|
||||||
|
"advancements.story.obtain_armor.description": "철 갑옷으로 스스로를 보호하세요",
|
||||||
|
"advancements.story.obtain_armor.title": "차려입기",
|
||||||
|
"advancements.story.root.description": "게임의 핵심과 이야기",
|
||||||
|
"advancements.story.root.title": "Minecraft",
|
||||||
|
"advancements.story.shiny_gear.description": "다이아몬드 갑옷은 생명을 구합니다",
|
||||||
|
"advancements.story.shiny_gear.title": "다이아몬드로 날 감싸줘",
|
||||||
|
"advancements.story.smelt_iron.description": "철 주괴를 제련하세요",
|
||||||
|
"advancements.story.smelt_iron.title": "철이 철철 넘쳐",
|
||||||
|
"advancements.story.upgrade_tools.description": "더 좋은 곡괭이를 만드세요",
|
||||||
|
"advancements.story.upgrade_tools.title": "더욱더 좋게",
|
||||||
|
"advancements.toast.challenge": "도전 완료!",
|
||||||
|
"advancements.toast.goal": "목표 달성!",
|
||||||
|
"advancements.toast.task": "발전 과제 달성!",
|
||||||
|
"death.attack.anvil": "%1$s님이 떨어지는 모루에 짓눌렸습니다",
|
||||||
|
"death.attack.anvil.player": "%1$s님이 %2$s과(와) 싸우다가 떨어지는 모루에 짓눌렸습니다",
|
||||||
|
"death.attack.arrow": "%1$s님이 %2$s에게 저격당했습니다",
|
||||||
|
"death.attack.arrow.item": "%1$s님이 %2$s에게 %3$s(으)로 저격당했습니다",
|
||||||
|
"death.attack.badRespawnPoint.link": "의도적 게임 설계",
|
||||||
|
"death.attack.badRespawnPoint.message": "%1$s님이 %2$s 때문에 죽었습니다",
|
||||||
|
"death.attack.cactus": "%1$s님이 찔려 죽었습니다",
|
||||||
|
"death.attack.cactus.player": "%1$s님이 %2$s에게서 도망치려다 선인장에 찔렸습니다",
|
||||||
|
"death.attack.cramming": "%1$s님이 으깨져버렸습니다",
|
||||||
|
"death.attack.cramming.player": "%1$s님이 %2$s에게 짓눌렸습니다",
|
||||||
|
"death.attack.dragonBreath": "%1$s님이 드래곤의 숨결에 구워졌습니다",
|
||||||
|
"death.attack.dragonBreath.player": "%1$s님이 %2$s 때문에 드래곤의 숨결에 구워졌습니다",
|
||||||
|
"death.attack.drown": "%1$s님이 익사했습니다",
|
||||||
|
"death.attack.drown.player": "%1$s님이 %2$s에게서 도망치려다 익사했습니다",
|
||||||
|
"death.attack.dryout": "%1$s님이 탈수로 죽었습니다",
|
||||||
|
"death.attack.dryout.player": "%1$s님이 %2$s에게서 도망치려다 탈수로 죽었습니다",
|
||||||
|
"death.attack.even_more_magic": "%1$s님이 더욱 심오한 마법에 당했습니다",
|
||||||
|
"death.attack.explosion": "%1$s님이 폭파당했습니다",
|
||||||
|
"death.attack.explosion.player": "%1$s님이 %2$s 때문에 폭사했습니다",
|
||||||
|
"death.attack.explosion.player.item": "%1$s님이 %3$s을(를) 사용한 %2$s 때문에 폭사했습니다",
|
||||||
|
"death.attack.fall": "%1$s님이 땅바닥으로 곤두박질쳤습니다",
|
||||||
|
"death.attack.fall.player": "%1$s님이 %2$s에게서 도망치려다 땅바닥으로 곤두박질쳤습니다",
|
||||||
|
"death.attack.fallingBlock": "%1$s님이 떨어지는 블록에 짓눌렸습니다",
|
||||||
|
"death.attack.fallingBlock.player": "%1$s님이 %2$s과(와) 싸우다가 떨어지는 블록에 짓눌렸습니다",
|
||||||
|
"death.attack.fallingStalactite": "%1$s님이 떨어지는 종유석에 찔렸습니다",
|
||||||
|
"death.attack.fallingStalactite.player": "%1$s님이 %2$s과(와) 싸우다가 떨어지는 종유석에 찔렸습니다",
|
||||||
|
"death.attack.fireball": "%1$s님이 %2$s님이 던진 화염구에 맞았습니다",
|
||||||
|
"death.attack.fireball.item": "%1$s님이 %3$s을(를) 사용한 %2$s님이 던진 화염구에 맞았습니다",
|
||||||
|
"death.attack.fireworks": "%1$s님이 굉음과 함께 폭사했습니다",
|
||||||
|
"death.attack.fireworks.item": "%1$s님이 %2$s님이 %3$s(으)로 쏜 폭죽 때문에 굉음과 함께 폭사했습니다",
|
||||||
|
"death.attack.fireworks.player": "%1$s님이 %2$s과(와) 싸우다가 굉음과 함께 폭사했습니다",
|
||||||
|
"death.attack.flyIntoWall": "%1$s님이 운동 에너지를 경험했습니다",
|
||||||
|
"death.attack.flyIntoWall.player": "%1$s님이 %2$s에게서 도망치려다 운동 에너지를 경험했습니다",
|
||||||
|
"death.attack.freeze": "%1$s님이 얼어 죽었습니다",
|
||||||
|
"death.attack.freeze.player": "%1$s님이 %2$s 때문에 얼어 죽었습니다",
|
||||||
|
"death.attack.generic": "%1$s님이 죽었습니다",
|
||||||
|
"death.attack.generic.player": "%1$s님이 %2$s 때문에 죽었습니다",
|
||||||
|
"death.attack.genericKill": "%1$s님이 죽었습니다",
|
||||||
|
"death.attack.genericKill.player": "%1$s님이 %2$s과(와) 싸우는 도중 죽었습니다",
|
||||||
|
"death.attack.hotFloor": "%1$s님이 바닥이 용암인 것을 알아챘습니다",
|
||||||
|
"death.attack.hotFloor.player": "%1$s님이 %2$s 때문에 위험 지대에 빠졌습니다",
|
||||||
|
"death.attack.inFire": "%1$s님이 화염에 휩싸였습니다",
|
||||||
|
"death.attack.inFire.player": "%1$s님이 %2$s과(와) 싸우다가 불에 빠졌습니다",
|
||||||
|
"death.attack.inWall": "%1$s님이 벽 속에서 질식했습니다",
|
||||||
|
"death.attack.inWall.player": "%1$s님이 %2$s과(와) 싸우다가 벽 속에서 질식했습니다",
|
||||||
|
"death.attack.indirectMagic": "%1$s님이 %2$s의 마법에 당했습니다",
|
||||||
|
"death.attack.indirectMagic.item": "%1$s님이 %2$s의 %3$s에 당했습니다",
|
||||||
|
"death.attack.lava": "%1$s님이 용암에 빠졌습니다",
|
||||||
|
"death.attack.lava.player": "%1$s님이 %2$s에게서 도망치려다 용암에 빠졌습니다",
|
||||||
|
"death.attack.lightningBolt": "%1$s님이 벼락을 맞았습니다",
|
||||||
|
"death.attack.lightningBolt.player": "%1$s님이 %2$s과(와) 싸우다가 벼락을 맞았습니다",
|
||||||
|
"death.attack.magic": "%1$s님이 마법에 당했습니다",
|
||||||
|
"death.attack.magic.player": "%1$s님이 %2$s에게서 도망치려다 마법에 당했습니다",
|
||||||
|
"death.attack.message_too_long": "완전히 전달하기에는 메시지가 너무 길었습니다. 죄송합니다! 잘라낸 메시지입니다: %s",
|
||||||
|
"death.attack.mob": "%1$s님이 %2$s에게 살해당했습니다",
|
||||||
|
"death.attack.mob.item": "%1$s님이 %2$s에게 %3$s(으)로 살해당했습니다",
|
||||||
|
"death.attack.onFire": "%1$s님이 불타 죽었습니다",
|
||||||
|
"death.attack.onFire.item": "%1$s님이 %3$s을(를) 든 %2$s과(와) 싸우다가 바삭하게 구워졌습니다",
|
||||||
|
"death.attack.onFire.player": "%1$s님이 %2$s과(와) 싸우다가 바삭하게 구워졌습니다",
|
||||||
|
"death.attack.outOfWorld": "%1$s님이 세계 밖으로 떨어졌습니다",
|
||||||
|
"death.attack.outOfWorld.player": "%1$s은(는) %2$s과(와)는 도저히 한 하늘을 같이 이고 살 수 없었습니다",
|
||||||
|
"death.attack.outsideBorder": "%1$s님이 세계의 한계를 벗어났습니다",
|
||||||
|
"death.attack.outsideBorder.player": "%1$s님이 %2$s과(와) 싸우다가 세계의 한계를 벗어났습니다",
|
||||||
|
"death.attack.player": "%1$s님이 %2$s에게 살해당했습니다",
|
||||||
|
"death.attack.player.item": "%1$s님이 %2$s에게 %3$s(으)로 살해당했습니다",
|
||||||
|
"death.attack.sonic_boom": "%1$s은(는) 강한 음파의 비명에 말살됐습니다",
|
||||||
|
"death.attack.sonic_boom.item": "%1$s님이 %3$s을(를) 든 %2$s에게서 도망치려다 강한 음파의 비명에 말살됐습니다",
|
||||||
|
"death.attack.sonic_boom.player": "%1$s님이 %2$s에게서 도망치려다 강한 음파의 비명에 말살됐습니다",
|
||||||
|
"death.attack.stalagmite": "%1$s님이 석순에 찔렸습니다",
|
||||||
|
"death.attack.stalagmite.player": "%1$s님이 %2$s과(와) 싸우다가 석순에 찔렸습니다",
|
||||||
|
"death.attack.starve": "%1$s님이 굶어 죽었습니다",
|
||||||
|
"death.attack.starve.player": "%1$s님이 %2$s과(와) 싸우다가 굶어 죽었습니다",
|
||||||
|
"death.attack.sting": "%1$s님이 쏘여 죽었습니다",
|
||||||
|
"death.attack.sting.item": "%1$s님이 %3$s을(를) 사용한 %2$s에게 쏘여 죽었습니다",
|
||||||
|
"death.attack.sting.player": "%1$s님이 %2$s에게 쏘여 죽었습니다",
|
||||||
|
"death.attack.sweetBerryBush": "%1$s님이 달콤한 열매 덤불에 찔려 죽었습니다",
|
||||||
|
"death.attack.sweetBerryBush.player": "%1$s님이 %2$s에게서 도망치려다 달콤한 열매 덤불에 찔려 죽었습니다",
|
||||||
|
"death.attack.thorns": "%1$s님이 %2$s을(를) 해치려다 죽었습니다",
|
||||||
|
"death.attack.thorns.item": "%1$s님이 %2$s을(를) 해치려다 %3$s 때문에 죽었습니다",
|
||||||
|
"death.attack.thrown": "%1$s님이 %2$s에게 구타당했습니다",
|
||||||
|
"death.attack.thrown.item": "%1$s님이 %2$s에게 %3$s(으)로 구타당했습니다",
|
||||||
|
"death.attack.trident": "%1$s님이 %2$s에게 찔렸습니다",
|
||||||
|
"death.attack.trident.item": "%1$s님이 %3$s을(를) 사용한 %2$s에게 찔렸습니다",
|
||||||
|
"death.attack.wither": "%1$s님이 사그라졌습니다",
|
||||||
|
"death.attack.wither.player": "%1$s님이 %2$s과(와) 싸우다가 사그라졌습니다",
|
||||||
|
"death.attack.witherSkull": "%1$s님이 %2$s에게 해골로 저격당했습니다",
|
||||||
|
"death.attack.witherSkull.item": "%1$s님이 %3$s을(를) 사용한 %2$s에게 해골로 저격당했습니다",
|
||||||
|
"death.fell.accident.generic": "%1$s님이 높은 곳에서 떨어졌습니다",
|
||||||
|
"death.fell.accident.ladder": "%1$s님이 사다리에서 떨어졌습니다",
|
||||||
|
"death.fell.accident.other_climbable": "%1$s님이 무언가를 타고 오르던 중 떨어졌습니다",
|
||||||
|
"death.fell.accident.scaffolding": "%1$s님이 비계에서 떨어졌습니다",
|
||||||
|
"death.fell.accident.twisting_vines": "%1$s님이 휘어진 덩굴에서 떨어졌습니다",
|
||||||
|
"death.fell.accident.vines": "%1$s님이 덩굴에서 떨어졌습니다",
|
||||||
|
"death.fell.accident.weeping_vines": "%1$s님이 늘어진 덩굴에서 떨어졌습니다",
|
||||||
|
"death.fell.assist": "%1$s은(는) %2$s 때문에 추락을 피하지 못했습니다",
|
||||||
|
"death.fell.assist.item": "%1$s은(는) %3$s을(를) 사용한 %2$s 때문에 추락을 피하지 못했습니다",
|
||||||
|
"death.fell.finish": "%1$s님이 너무 높은 곳에서 떨어진 후, %2$s에게 최후를 맞이했습니다",
|
||||||
|
"death.fell.finish.item": "%1$s님이 너무 높은 곳에서 떨어진 후, %3$s을(를) 사용한 %2$s에게 최후를 맞이했습니다",
|
||||||
|
"death.fell.killer": "%1$s은(는) 추락을 피하지 못했습니다",
|
||||||
|
"entity.minecraft.allay": "알레이",
|
||||||
|
"entity.minecraft.area_effect_cloud": "광역 효과 구름",
|
||||||
|
"entity.minecraft.armadillo": "아르마딜로",
|
||||||
|
"entity.minecraft.armor_stand": "갑옷 거치대",
|
||||||
|
"entity.minecraft.arrow": "화살",
|
||||||
|
"entity.minecraft.axolotl": "아홀로틀",
|
||||||
|
"entity.minecraft.bat": "박쥐",
|
||||||
|
"entity.minecraft.bee": "꿀벌",
|
||||||
|
"entity.minecraft.blaze": "블레이즈",
|
||||||
|
"entity.minecraft.block_display": "블록 표시",
|
||||||
|
"entity.minecraft.boat": "보트",
|
||||||
|
"entity.minecraft.bogged": "보그드",
|
||||||
|
"entity.minecraft.breeze": "브리즈",
|
||||||
|
"entity.minecraft.breeze_wind_charge": "돌풍구",
|
||||||
|
"entity.minecraft.camel": "낙타",
|
||||||
|
"entity.minecraft.cat": "고양이",
|
||||||
|
"entity.minecraft.cave_spider": "동굴 거미",
|
||||||
|
"entity.minecraft.chest_boat": "상자가 실린 보트",
|
||||||
|
"entity.minecraft.chest_minecart": "상자가 실린 광산 수레",
|
||||||
|
"entity.minecraft.chicken": "닭",
|
||||||
|
"entity.minecraft.cod": "대구",
|
||||||
|
"entity.minecraft.command_block_minecart": "명령 블록이 실린 광산 수레",
|
||||||
|
"entity.minecraft.cow": "소",
|
||||||
|
"entity.minecraft.creeper": "크리퍼",
|
||||||
|
"entity.minecraft.dolphin": "돌고래",
|
||||||
|
"entity.minecraft.donkey": "당나귀",
|
||||||
|
"entity.minecraft.dragon_fireball": "드래곤 화염구",
|
||||||
|
"entity.minecraft.drowned": "드라운드",
|
||||||
|
"entity.minecraft.egg": "던져진 달걀",
|
||||||
|
"entity.minecraft.elder_guardian": "엘더 가디언",
|
||||||
|
"entity.minecraft.end_crystal": "엔드 수정",
|
||||||
|
"entity.minecraft.ender_dragon": "엔더 드래곤",
|
||||||
|
"entity.minecraft.ender_pearl": "던져진 엔더 진주",
|
||||||
|
"entity.minecraft.enderman": "엔더맨",
|
||||||
|
"entity.minecraft.endermite": "엔더마이트",
|
||||||
|
"entity.minecraft.evoker": "소환사",
|
||||||
|
"entity.minecraft.evoker_fangs": "소환사 송곳니",
|
||||||
|
"entity.minecraft.experience_bottle": "던져진 경험치 병",
|
||||||
|
"entity.minecraft.experience_orb": "경험 구슬",
|
||||||
|
"entity.minecraft.eye_of_ender": "엔더의 눈",
|
||||||
|
"entity.minecraft.falling_block": "떨어지는 블록",
|
||||||
|
"entity.minecraft.falling_block_type": "떨어지는 %s",
|
||||||
|
"entity.minecraft.fireball": "화염구",
|
||||||
|
"entity.minecraft.firework_rocket": "폭죽 로켓",
|
||||||
|
"entity.minecraft.fishing_bobber": "낚시찌",
|
||||||
|
"entity.minecraft.fox": "여우",
|
||||||
|
"entity.minecraft.frog": "개구리",
|
||||||
|
"entity.minecraft.furnace_minecart": "화로가 실린 광산 수레",
|
||||||
|
"entity.minecraft.ghast": "가스트",
|
||||||
|
"entity.minecraft.giant": "거인",
|
||||||
|
"entity.minecraft.glow_item_frame": "발광 아이템 액자",
|
||||||
|
"entity.minecraft.glow_squid": "발광 오징어",
|
||||||
|
"entity.minecraft.goat": "염소",
|
||||||
|
"entity.minecraft.guardian": "가디언",
|
||||||
|
"entity.minecraft.hoglin": "호글린",
|
||||||
|
"entity.minecraft.hopper_minecart": "호퍼가 실린 광산 수레",
|
||||||
|
"entity.minecraft.horse": "말",
|
||||||
|
"entity.minecraft.husk": "허스크",
|
||||||
|
"entity.minecraft.illusioner": "환술사",
|
||||||
|
"entity.minecraft.interaction": "상호 작용",
|
||||||
|
"entity.minecraft.iron_golem": "철 골렘",
|
||||||
|
"entity.minecraft.item": "아이템",
|
||||||
|
"entity.minecraft.item_display": "아이템 표시",
|
||||||
|
"entity.minecraft.item_frame": "아이템 액자",
|
||||||
|
"entity.minecraft.killer_bunny": "살인 토끼",
|
||||||
|
"entity.minecraft.leash_knot": "끈 매듭",
|
||||||
|
"entity.minecraft.lightning_bolt": "벼락",
|
||||||
|
"entity.minecraft.llama": "라마",
|
||||||
|
"entity.minecraft.llama_spit": "라마 침",
|
||||||
|
"entity.minecraft.magma_cube": "마그마 큐브",
|
||||||
|
"entity.minecraft.marker": "표지",
|
||||||
|
"entity.minecraft.minecart": "광산 수레",
|
||||||
|
"entity.minecraft.mooshroom": "무시룸",
|
||||||
|
"entity.minecraft.mule": "노새",
|
||||||
|
"entity.minecraft.ocelot": "오실롯",
|
||||||
|
"entity.minecraft.ominous_item_spawner": "불길한 아이템 생성기",
|
||||||
|
"entity.minecraft.painting": "그림",
|
||||||
|
"entity.minecraft.panda": "판다",
|
||||||
|
"entity.minecraft.parrot": "앵무새",
|
||||||
|
"entity.minecraft.phantom": "팬텀",
|
||||||
|
"entity.minecraft.pig": "돼지",
|
||||||
|
"entity.minecraft.piglin": "피글린",
|
||||||
|
"entity.minecraft.piglin_brute": "난폭한 피글린",
|
||||||
|
"entity.minecraft.pillager": "약탈자",
|
||||||
|
"entity.minecraft.player": "플레이어",
|
||||||
|
"entity.minecraft.polar_bear": "북극곰",
|
||||||
|
"entity.minecraft.potion": "물약",
|
||||||
|
"entity.minecraft.pufferfish": "복어",
|
||||||
|
"entity.minecraft.rabbit": "토끼",
|
||||||
|
"entity.minecraft.ravager": "파괴수",
|
||||||
|
"entity.minecraft.salmon": "연어",
|
||||||
|
"entity.minecraft.sheep": "양",
|
||||||
|
"entity.minecraft.shulker": "셜커",
|
||||||
|
"entity.minecraft.shulker_bullet": "셜커 탄환",
|
||||||
|
"entity.minecraft.silverfish": "좀벌레",
|
||||||
|
"entity.minecraft.skeleton": "스켈레톤",
|
||||||
|
"entity.minecraft.skeleton_horse": "스켈레톤 말",
|
||||||
|
"entity.minecraft.slime": "슬라임",
|
||||||
|
"entity.minecraft.small_fireball": "작은 화염구",
|
||||||
|
"entity.minecraft.sniffer": "스니퍼",
|
||||||
|
"entity.minecraft.snow_golem": "눈 골렘",
|
||||||
|
"entity.minecraft.snowball": "눈덩이",
|
||||||
|
"entity.minecraft.spawner_minecart": "몬스터 생성기가 실린 광산 수레",
|
||||||
|
"entity.minecraft.spectral_arrow": "분광 화살",
|
||||||
|
"entity.minecraft.spider": "거미",
|
||||||
|
"entity.minecraft.squid": "오징어",
|
||||||
|
"entity.minecraft.stray": "스트레이",
|
||||||
|
"entity.minecraft.strider": "스트라이더",
|
||||||
|
"entity.minecraft.tadpole": "올챙이",
|
||||||
|
"entity.minecraft.text_display": "문자 표시",
|
||||||
|
"entity.minecraft.tnt": "점화된 TNT",
|
||||||
|
"entity.minecraft.tnt_minecart": "TNT가 실린 광산 수레",
|
||||||
|
"entity.minecraft.trader_llama": "상인 라마",
|
||||||
|
"entity.minecraft.trident": "삼지창",
|
||||||
|
"entity.minecraft.tropical_fish": "열대어",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.0": "아네모네",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.1": "긴코양쥐돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.10": "깃대돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.11": "오네이트 나비고기",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.12": "비늘돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.13": "여왕 신선돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.14": "빨간 시클리드",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.15": "빨간 입술 베도라치",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.16": "빨간퉁돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.17": "날가지숭어",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.18": "토마토 흰동가리",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.19": "쥐치복",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.2": "남양쥐돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.20": "노랑꼬리비늘돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.21": "노랑양쥐돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.3": "나비고기",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.4": "시클리드",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.5": "흰동가리",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.6": "솜사탕 베타",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.7": "도티백",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.8": "황적퉁돔",
|
||||||
|
"entity.minecraft.tropical_fish.predefined.9": "촉수",
|
||||||
|
"entity.minecraft.tropical_fish.type.betty": "싸움고기",
|
||||||
|
"entity.minecraft.tropical_fish.type.blockfish": "사각고기",
|
||||||
|
"entity.minecraft.tropical_fish.type.brinely": "소금치",
|
||||||
|
"entity.minecraft.tropical_fish.type.clayfish": "점토고기",
|
||||||
|
"entity.minecraft.tropical_fish.type.dasher": "날쌘돌이",
|
||||||
|
"entity.minecraft.tropical_fish.type.flopper": "퍼덕이",
|
||||||
|
"entity.minecraft.tropical_fish.type.glitter": "반짝이",
|
||||||
|
"entity.minecraft.tropical_fish.type.kob": "보구치",
|
||||||
|
"entity.minecraft.tropical_fish.type.snooper": "서성이",
|
||||||
|
"entity.minecraft.tropical_fish.type.spotty": "점박이",
|
||||||
|
"entity.minecraft.tropical_fish.type.stripey": "줄무늬",
|
||||||
|
"entity.minecraft.tropical_fish.type.sunstreak": "볕금고기",
|
||||||
|
"entity.minecraft.turtle": "거북",
|
||||||
|
"entity.minecraft.vex": "벡스",
|
||||||
|
"entity.minecraft.villager": "주민",
|
||||||
|
"entity.minecraft.villager.armorer": "갑옷 제조인",
|
||||||
|
"entity.minecraft.villager.butcher": "도살업자",
|
||||||
|
"entity.minecraft.villager.cartographer": "지도 제작자",
|
||||||
|
"entity.minecraft.villager.cleric": "성직자",
|
||||||
|
"entity.minecraft.villager.farmer": "농부",
|
||||||
|
"entity.minecraft.villager.fisherman": "어부",
|
||||||
|
"entity.minecraft.villager.fletcher": "화살 제조인",
|
||||||
|
"entity.minecraft.villager.leatherworker": "가죽 세공인",
|
||||||
|
"entity.minecraft.villager.librarian": "사서",
|
||||||
|
"entity.minecraft.villager.mason": "석공",
|
||||||
|
"entity.minecraft.villager.nitwit": "멍청이",
|
||||||
|
"entity.minecraft.villager.none": "주민",
|
||||||
|
"entity.minecraft.villager.shepherd": "양치기",
|
||||||
|
"entity.minecraft.villager.toolsmith": "도구 대장장이",
|
||||||
|
"entity.minecraft.villager.weaponsmith": "무기 대장장이",
|
||||||
|
"entity.minecraft.vindicator": "변명자",
|
||||||
|
"entity.minecraft.wandering_trader": "떠돌이 상인",
|
||||||
|
"entity.minecraft.warden": "워든",
|
||||||
|
"entity.minecraft.wind_charge": "돌풍구",
|
||||||
|
"entity.minecraft.witch": "마녀",
|
||||||
|
"entity.minecraft.wither": "위더",
|
||||||
|
"entity.minecraft.wither_skeleton": "위더 스켈레톤",
|
||||||
|
"entity.minecraft.wither_skull": "위더 해골",
|
||||||
|
"entity.minecraft.wolf": "늑대",
|
||||||
|
"entity.minecraft.zoglin": "조글린",
|
||||||
|
"entity.minecraft.zombie": "좀비",
|
||||||
|
"entity.minecraft.zombie_horse": "좀비 말",
|
||||||
|
"entity.minecraft.zombie_villager": "좀비 주민",
|
||||||
|
"entity.minecraft.zombified_piglin": "좀비화 피글린",
|
||||||
|
"entity.not_summonable": "%s 유형의 개체를 소환할 수 없습니다"
|
||||||
|
}
|
||||||
32
DiscordBot/src/main/templates/META-INF/neoforge.mods.toml
Normal file
32
DiscordBot/src/main/templates/META-INF/neoforge.mods.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
modLoader = "kotlinforforge"
|
||||||
|
loaderVersion = "${loader_version_range}"
|
||||||
|
license = "${mod_license}"
|
||||||
|
issueTrackerURL = ""
|
||||||
|
|
||||||
|
[[mods]]
|
||||||
|
modId = "${mod_id}"
|
||||||
|
version = "${mod_version}"
|
||||||
|
displayName = "${mod_name}"
|
||||||
|
authors = "${mod_authors}"
|
||||||
|
description = '''${mod_description}'''
|
||||||
|
|
||||||
|
[[dependencies.${mod_id}]]
|
||||||
|
modId = "neoforge"
|
||||||
|
type = "required"
|
||||||
|
versionRange = "${neo_version_range}"
|
||||||
|
ordering = "NONE"
|
||||||
|
side = "BOTH"
|
||||||
|
|
||||||
|
[[dependencies.${mod_id}]]
|
||||||
|
modId = "minecraft"
|
||||||
|
type = "required"
|
||||||
|
versionRange = "${minecraft_version_range}"
|
||||||
|
ordering = "NONE"
|
||||||
|
side = "BOTH"
|
||||||
|
|
||||||
|
[[dependencies.${mod_id}]]
|
||||||
|
modId = "kotlinforforge"
|
||||||
|
type = "required"
|
||||||
|
versionRange = "[5.3.0,)"
|
||||||
|
ordering = "NONE"
|
||||||
|
side = "BOTH"
|
||||||
|
|
@ -14,6 +14,7 @@ NeoForge 1.21.1 기반 마인크래프트 서버 모드 모음입니다.
|
||||||
| ------------------------------- | ------------------------------------- |
|
| ------------------------------- | ------------------------------------- |
|
||||||
| [Essentials](./Essentials/) | 서버 필수 기능 (좌표 관리, 닉네임 등) |
|
| [Essentials](./Essentials/) | 서버 필수 기능 (좌표 관리, 닉네임 등) |
|
||||||
| [ServerStatus](./ServerStatus/) | HTTP API로 서버 상태 제공 |
|
| [ServerStatus](./ServerStatus/) | HTTP API로 서버 상태 제공 |
|
||||||
|
| [DiscordBot](./DiscordBot/) | Discord 웹훅으로 서버 이벤트 전송 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -45,6 +46,7 @@ NeoForge 1.21.1 기반 마인크래프트 서버 모드 모음입니다.
|
||||||
minecraft-mod/
|
minecraft-mod/
|
||||||
├── Essentials/ # 서버 필수 기능 모드
|
├── Essentials/ # 서버 필수 기능 모드
|
||||||
├── ServerStatus/ # 서버 상태 API 모드
|
├── ServerStatus/ # 서버 상태 API 모드
|
||||||
|
├── DiscordBot/ # Discord 연동 모드
|
||||||
└── .gitignore
|
└── .gitignore
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue