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/) | 서버 필수 기능 (좌표 관리, 닉네임 등) |
|
||||
| [ServerStatus](./ServerStatus/) | HTTP API로 서버 상태 제공 |
|
||||
| [DiscordBot](./DiscordBot/) | Discord 웹훅으로 서버 이벤트 전송 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ NeoForge 1.21.1 기반 마인크래프트 서버 모드 모음입니다.
|
|||
minecraft-mod/
|
||||
├── Essentials/ # 서버 필수 기능 모드
|
||||
├── ServerStatus/ # 서버 상태 API 모드
|
||||
├── DiscordBot/ # Discord 연동 모드
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue