Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tmvkrpxl0 committed Jun 7, 2024
1 parent 792baac commit d8e6857
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 76 deletions.
38 changes: 19 additions & 19 deletions docs/concepts/sides.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,37 @@ sidebar_position: 2
---
# 사이드

마인크래프트도 다른 프로그램들처럼 클라이언트-서버 구조를 따릅니다, 클라이언트는 사용자에게 데이터를 표시하고, 서버는 데이터를 처리합니다. 이들을 사이드라 칭합니다. 컴퓨터를 자주 사용하시거나, 특히 게임좀 해보신 분들이라면 이 둘이 뭔지 잘 알겁니다, 그렇죠?
마인크래프트도 다른 프로그램들처럼 클라이언트-서버 구조를 따릅니다, 클라이언트는 사용자에게 데이터를 표시하고, 서버는 데이터를 처리합니다. 이 둘을 사이드라 칭합니다. 컴퓨터를 자주 사용하시거나, 특히 게임좀 해보신 분들이라면 이 둘이 뭔지 잘 알겁니다, 간단하죠?

사실 아닙니다, 마인크래프트는 사이드를 구분하는 방법이 두 가지라 모드 개발에 많은 혼란을 유발합니다. 사이드를 나누는 기준은 논리, 그리고 물리가 있습니다.
사실 아닙니다, 마인크래프트는 사이드를 구분하는 방법이 두 가지라 모드 개발에 많은 혼란을 유발합니다. 사이드를 나누는 기준은 논리, 그리고 물리가 있습니다.

## 논리 vs 물리 사이드

### 물리 사이드

물리 사이드는 무슨 프로그램을 실행했느냐로 사이드를 구분합니다. 예를 들어 **물리 클라이언트**는 Minecraft Launcher에서 플레이 버튼을 눌러 실행하는 게임을 의미합니다. 물리 클라이언트의 "물리"는 실행한 것이 "클라이언트 프로그램"임을 의미합니다. 그래픽, 소리와 같은 기능은 물리 클라이언트에서만 사용 가능합니다. 그 반대로, **물리 서버**는 전용 서버를 의미하며, 버킷과 같은 마인크래프트 서버 JAR 파일로 실행한 프로그램을 의미합니다. 전용 서버는 관리를 위한 기초적인 GUI를 제공하지만, 3D 그래픽이나 소리와 같은 클라이언트 전용 기능들이 전부 누락되어 있습니다. 전용 서버에서 클라이언트 전용 기능을 사용하려 하면 클래스를 찾을 수 없다며 충돌이 일어나 주의해야 합니다.
물리 사이드는 무슨 프로그램을 실행했느냐로 사이드를 구분한 것입니다. 예를 들어 **물리 클라이언트**는 Minecraft Launcher에서 플레이 버튼을 눌러 실행하는 게임을 의미합니다. 물리 클라이언트의 "물리"는 실행한 것이 "클라이언트 프로그램"임을 의미합니다. 그래픽, 소리와 같은 기능은 물리 클라이언트에서만 사용 가능합니다. 그 반대로, **물리 서버**는 전용 서버를 의미하며, 버킷과 같은 마인크래프트 서버 JAR 파일로 실행한 프로그램을 의미합니다. 전용 서버는 관리를 위한 기초적인 GUI를 제공하지만, 3D 그래픽이나 소리와 같은 클라이언트 전용 기능들이 전부 누락되어 있습니다. 전용 서버에서 클라이언트 기능을 사용하려 하면 클래스를 찾을 수 없다며 충돌이 일어나 주의해야 합니다.

### 논리 사이드

논리 사이드는 마인크래프트 내부 구조에서 사이드를 나누는 기준입니다. **논리 서버**는 게임의 메카닉을 처리하는 코드입니다. 날씨 변동, 엔티티 소환, 시간의 흐름 등은 논리 서버에서 처리하며, 인벤토리 아이템, 체력과 같은 데이터도 서버가 관리합니다. 그 반대로, **논리 클라이언트**는 화면에 데이터를 띄우는 역할을 합니다. 마인크래프트는 클라이언트 코드를 분리해 `net.minecraft.client` 패키지에 작성합니다. 그리고 렌더 스레드에서 이 코드를 실행합니다. 그 외 나머지는 공용 코드로 취급되어, 클라이언트 및 서버 둘 다에서 사용할 수 있습니다.
논리 사이드는 마인크래프트 코드 구조로 사이드를 구분한 것입니다. 예를 들어 **논리 서버**는 게임의 메카닉을 처리하는 코드입니다. 날씨 변동, 엔티티 소환, 시간의 흐름 등은 논리 서버에서 처리합니다. 그 반대로, **논리 클라이언트**는 화면에 데이터를 띄우는 역할을 합니다. 마인크래프트는 클라이언트 코드를 `net.minecraft.client` 패키지에 작성합니다. 그리고 렌더 스레드에서 이 코드를 실행합니다.

### 차이가 뭔가요?

논리 사이드와 물리 사이드의 차이를 아래 예를 들어 설명하겠습니다:

- 플레이어가 **멀티 플레이어** 서버에 접속함: 플레이어의 물리 클라이언트 프로그램의 논리 클라이언트 부분이 물리 서버의 논리 서버 부분에 접속한 것.
- 플레이어가 **싱글 플레이어** 월드에 접속함: 플레이어의 물리 클라이언트 프로그램의 논리 서버 부분가 실행됨. 이후 논리 클라이언트가 이 논리 서버에 접속함. 네트워크로 비유하자면, `localhost`에 접속한 것과 유사함(네트워크 소켓은 사용하지 않음).
- 플레이어가 **멀티 플레이** 서버에 접속함: 플레이어의 물리 클라이언트 프로그램의 논리 클라이언트 부분이 물리 서버의 논리 서버 부분에 접속한 것.
- 플레이어가 **싱글 플레이** 월드에 접속함: 플레이어의 물리 클라이언트 프로그램의 논리 서버가 실행됨. 이후 논리 클라이언트가 이 논리 서버에 접속함.

위 상황을 살펴보면 문제가 드러나는데: 물리 클라이언트의 논리 서버에선 코드가 잘 실행되더라도, 물리 서버의 논리 서버에선 오류가 발생할 수 있습니다. 그렇기에 무조건 모드를 전용 서버에서도 테스트 해야 합니다. `NoClassDefFoundError``ClassNotFoundException`는 클라이언트와 서버의 코드를 제대로 분리하지 못해 발생하는 예외로, 모드 개발시 가장 많이 발생합니다. 이것 말고도 하나의 정적 필드를 양 논리 사이드에서 사용하는 것도 문제인데, 오류가 나긴 한건지, 뭐가 잘못된건지 드러나지 않기 때문입니다.
여기서 주의할 점은 물리 클라이언트의 논리 서버에선 클라이언트 코드를 사용할 수 있지만, 물리 서버의 논리 서버에서 같은 코드 실행시, `NoClassDefFoundError``ClassNotFoundException` 등의 오류가 발생합니다. 그렇기에 무조건 모드를 물리 서버에서도 테스트 해야 합니다.

:::tip
사이드끼리 데이터를 전달해야 한다면 무조건 [패킷][networking]을 사용하세요.
:::warn
하나의 정적 필드를 양 논리 사이드에서 공유하면 싱글 플레이 월드와 멀티 플레이 서버에서 완전 다르게 동작하기 때문에, 오류가 나긴 한건지, 뭐가 잘못된건지 드러나지 않습니다. 사이드끼리 데이터를 공유해야 한다면 무조건 [패킷][networking]을 사용하세요.
:::

네오 포지에선 물리 사이드는 `Dist`, 논리 사이드는 `LogicalSide`가 대표합니다.
네오 포지에선 물리 사이드는 `Dist`, 논리 사이드는 `LogicalSide`로 표현합니다.

:::info
옛날에는 전용 서버 JAR에 클라이언트엔 없는 클래스가 일부 있었습니다. 하지만 최근 버전은 그렇지 않으며, 물리 서버는 물리 클라이언트의 일부라 볼 수 있습니다.
옛날에는 전용 서버 JAR에 클라이언트엔 없는 클래스가 일부 있었습니다. 하지만 최근 버전의 물리 서버는 물리 클라이언트의 일부라 볼 수 있습니다.
:::

## 사이드 전용 기능 만들기
Expand All @@ -42,41 +42,41 @@ sidebar_position: 2

이 메서드는 사이드를 확인하는데 가장 많이 사용되며, `Level` 객체를 이용해 현재 **논리 사이드**가 어디인지 확인합니다: 반환값이 `true`라면 논리 클라이언트, `false`라면 논리 서버입니다. 물리 서버에서는 언제나 `false`만 반환되지만, `false`가 반환되었다고 물리 서버라 단정지을 순 없습니다, 물리 클라이언트의 논리 서버도 `false`를 반환하기 때문입니다(예: 싱글 플레이어 월드).

게임 메카닉을 처리해야 할지 결정할 때 이 메서드를 사용하세요. 엔티티의 위치, 체력, 플레이어의 동작, 아이템의 갯수와 종류, 블록 등의 정보를 수정하는 코드는 무조건 논리 서버에서만 실행해야 합니다. 논리 클라이언트에서 위 정보를 수정하면 동기화가 깨져 운 좋으면 시각적 오류(유령 엔티티, 가짜 블록 등)가 발생하고, 운 없으면 게임이 충돌합니다.
게임 메카닉을 처리할 때 먼저 이 메서드로 사이드를 확인하세요. 엔티티의 위치, 체력, 플레이어의 동작, 아이템의 갯수와 종류, 블록 등의 정보를 수정하는 코드는 무조건 논리 서버에서만 실행해야 합니다. 논리 클라이언트에서 위 정보를 수정하면 동기화가 깨져 운 좋으면 시각적 오류(유령 엔티티, 가짜 블록 등)가 발생하고, 운 없으면 게임이 충돌합니다.

:::tip
`Level` 객체를 사용할 수 있다면 사이드를 확인할 땐 거의 이 방법만 사용하세요.
:::

### `FMLEnvironment.dist`

`FMLEnvironment.dist``Level#isClientSide()`와 다르게, **물리** 사이드를 확인할 때 사용합니다. 만약 이 필드의 값이 `Dist.CLIENT`라면 물리 클라이언트, `Dist.DEDICATED_SERVER`라면 물리 서버입니다.
`FMLEnvironment.dist`**물리** 사이드를 확인할 때 사용합니다. 만약 이 필드의 값이 `Dist.CLIENT`라면 물리 클라이언트, `Dist.DEDICATED_SERVER`라면 물리 서버입니다.


#### `@Mod`

Checking the physical environment is important when dealing with client-only classes. The recommended way to separate code that should only be executed on one physical client is by specifying a separate [`@Mod` annotation][mod], setting the `dist` parameter to the physical side the mod class should be loaded on:
[`@Mod`][mod] 어노테이션에 아래와 같이 `dist` 인자를 지정하면 그 진입점은 해당하는 사이드에서만 실행됩니다, 클라이언트 전용 모드를 만들 때 유용합니다:

```java
@Mod("examplemod")
public class ExampleMod {
public ExampleMod(IEventBus modBus) {
// Perform logic in that should be executed on both sides
// 양 사이드에서 실행할 코드
}
}

@Mod(value = "examplemod", dist = Dist.CLIENT)
public class ExampleModClient {
public ExampleModClient(IEventBus modBus) {
// Perform logic in that should only be executed on the physical client
// 물리 클라이언트에서만 실행할 코드
Minecraft.getInstance().whatever();
}
}

@Mod(value = "examplemod", dist = Dist.DEDICATED_SERVER)
public class ExampleModDedicatedServer {
public ExampleModDedicatedServer(IEventBus modBus) {
// Perform logic in that should only be executed on the physical server
// 물리 서버에서만 실행할 코드
}
}
```
Expand All @@ -86,8 +86,8 @@ public class ExampleModDedicatedServer {
```

:::tip
모드는 물리 사이드 어디에 설치하든 동작해야 합니다. 다시 말해 클라이언트 전용 모드를 만들더라도 물리 사이드가 클라이언트인지 확인하세요, 그리고 물리 사이드가 서버라면 모든 기능을 비활성화 하세요.
모드는 물리 사이드 어디에 설치하든 동작은 해야 합니다. 예를 들어 클라이언트 전용 모드는 서버에 설치되더라도 오류가 발생하진 말아야 합니다, 대신 아무런 동작을 하지 말아야 합니다.
:::

[networking]: ../networking/index.md
[mod]: ../gettingstarted/modfiles.md#javafml-and-mod
[mod]: ../gettingstarted/modfiles.md#javafml과-mod
2 changes: 1 addition & 1 deletion docs/datagen/_category_.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"label": "Datagen"
"label": "데이터 생성"
}
52 changes: 24 additions & 28 deletions docs/datastorage/attachments.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
# 데이터 부착

데이터 부착 시스템은 블록 엔티티, 청크, 엔티티에 추가 데이터를 저장합니다.
데이터 부착 시스템은 블록 엔티티, 청크, 엔티티와 같은 게임속 요소에 추가 데이터를 저장합니다.

_레벨 자체에 데이터를 추가하려면 [SavedData](saveddata)를 대신 쓰세요._

:::note
Data attachments for item stacks have been superceeded by vanilla's [data components][datacomponents].
아이템에 추가 데이터를 저장하려면 [데이터 조각][datacomponents]를 사용하세요.
:::

## 부착물 생성하기
## 새로운 데이터 정의하기

데이터 부착 시스템을 쓰려면 부착물(`AttachmentType`)이 필요합니다. 부착물은 아래 설정을 포함하는데:
부착할 데이터는 `AttachmentType`으로 정의합니다. `AttachmentType`은 무슨 데이터를 어떻게 부착하고 저장할지 등을 정의하는 타입으로, 아래 설정들을 가집니다:

- 기본값 생성 코드. 데이터를 처음 접근하거나 데이터가 없는 객체와 비교할 때 사용함.
- 데이터를 저장하고 싶다면 사용할 직렬화 코드.
- (직렬화 코드를 사용한다면) 엔티티 사망 시 데이터를 자동으로 복사하는 `copyOnDeath` 플래그. (아래 참고)
- 기본값 생성 코드. 데이터를 처음 사용하거나, 아직 데이터가 없는 요소의 데이터 값을 비교할 때 사용함.
- (선택 사항) 데이터를 저장할 때 사용할 코드. 없으면 데이터가 저장되지 않음.
- (데이터를 저장한다면) 엔티티 사망 시 데이터를 자동으로 복사하는 `copyOnDeath` 플래그. (아래 참고)

:::tip
부착물의 데이터를 저장하고 싶지 않다면 직렬화를 사용하지 마세요.
:::

부착된 데이터가 직렬화되도록 하는 데에는 여러 가지 방법이 있습니다: `IAttachmentSerializer`를 구현하거나, `INBTSerializable`를 구현하고 `AttachmentType#serializable`에 전달해 빌더를 만들거나, 아니면 코덱을 빌더에 전달하는 방식이 있습니다.
부착된 데이터를 저장하는 데에는 여러 방법이 있습니다: 데이터 자체가 `INBTSerializable`을 구현하거나, 코덱 또는 `IAttachmentSerializer`를 빌더에 전달하는 방식이 있습니다.

In any case, the attachment **must be registered** to the `NeoForgeRegistries.ATTACHMENT_TYPES` registry. Here is an example:
무슨 데이터를 정의하시든, 무조건 `NeoForgeRegistries.ATTACHMENT_TYPES`에 아래와 같이 등록해야 합니다:

```java
// 부착물을 등록하는 DeferredRegister 생성
private static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.ATTACHMENT_TYPES, MOD_ID);

// INBTSerializable로 직렬화
// INBTSerializable로 저장
private static final Supplier<AttachmentType<ItemStackHandler>> HANDLER = ATTACHMENT_TYPES.register(
"handler", () -> AttachmentType.serializable(() -> new ItemStackHandler(1)).build()
);
// Serialization via codec
// 코덱으로 저장
private static final Supplier<AttachmentType<Integer>> MANA = ATTACHMENT_TYPES.register(
"mana", () -> AttachmentType.builder(() -> 0).serialize(Codec.INT).build()
);
// No serialization
// 저장 안함
private static final Supplier<AttachmentType<SomeCache>> SOME_CACHE = ATTACHMENT_TYPES.register(
"some_cache", () -> AttachmentType.builder(() -> new SomeCache()).build()
);
Expand All @@ -45,25 +41,25 @@ private static final Supplier<AttachmentType<SomeCache>> SOME_CACHE = ATTACHMENT
ATTACHMENT_TYPES.register(modBus);
```

## 부착물 사용하기
## 데이터 부착하기

부착물을 생성하셨다면 아무 holder 객체에나 사용하실 수 있습니다. 해당 부착물이 없는 객체에 `#getData`를 호출하면 새 기본값 데이터를 만들어 부착합니다.
위에서 정의한 데이터는 레벨, 청크, 엔티티, 블록 엔티티에 사용하실 수 있습니다. 해당 부착물이 없는 객체의 데이터를 `#getData`로 가져오면 새 기본값 데이터를 만들어 부착합니다.

```java
// ItemStackHandler를 받아옴. 만약 부착된 적 없다면 새로 하나 생성:
// ItemStackHandler를 받아옴. 만약 부착된 적 없다면 새로 하나 생성해서 부착함:
ItemStackHandler stackHandler = chunk.getData(HANDLER);
// 플레이어의 마나 값을 받아옴. 없다면 0을 부착:
// 플레이어의 마나 값을 받아옴. 없다면 0을 부착함:
int playerMana = player.getData(MANA);
// 기타 등등..
```

만약 기본값 데이터를 붙이고 싶지 않다면 `hasData`로 데이터 존재 유무를 확인할 수 있습니다:
만약 기본값 데이터를 붙이고 싶지 않다면 `hasData`로 데이터 존재 유무를 확인하세요:

```java
// 청크에 HANDLER가 붙어있는지 먼저 확인하고 처리
// 청크에 HANDLER가 붙어있는지 먼저 확인함
if (chunk.hasData(HANDLER)) {
ItemStackHandler stackHandler = chunk.getData(HANDLER);
// Do something with chunk.getData(HANDLER).
// 데이터 사용
}
```

Expand All @@ -75,13 +71,13 @@ player.setData(MANA, player.getData(MANA) + 10);
```

:::important
블록 엔티티와 청크는 데이터가 변경될 시 (`setChanged``setUnsaved(true)` 등을 통해) 그 사실을 표기해 두어야 합니다. `setData`는 자동으로 이를 표기합니다:
블록 엔티티와 청크는 데이터가 변경될 시 `setChanged``setUnsaved(true)` 등을 통해 그 사실을 표기해 두어야 합니다. `setData`는 자동으로 이를 표기하지만:

```java
chunk.setData(MANA, chunk.getData(MANA) + 10); // 자동으로 setUnsaved 호출
```

하지만 `getData`로 받은 데이터를 바로 수정한다면 수동으로 변경 사실을 표기해야 합니다:
`getData`로 받은 데이터를 바로 수정하시면 직접 변경 사실을 표기해야 합니다:

```java
var mana = chunk.getData(MUTABLE_MANA);
Expand All @@ -92,13 +88,13 @@ chunk.setUnsaved(true); // setData를 사용하지 않았기에 무조건 직접

## 클라이언트와 데이터 공유하기

블록 엔티티, 청크, 또는 엔티티에 부착된 데이터는 클라이언트에 직접 [패킷을 전송하셔야][network] 합니다. 청크의 경우 데이터가 전송될 때 `ChunkWatchEvent.Sent`를 방송하니 이때 변경된 추가 데이터를 같이 보내세요.
블록 엔티티, 청크, 또는 엔티티에 부착된 데이터는 클라이언트에 직접 [패킷을 전송하셔야][network] 합니다. 청크의 경우 데이터가 전송될 때 `ChunkWatchEvent.Sent`를 방송하니 이때 변경된 데이터를 같이 보내세요.

## 플레이어 사망 시 데이터 복사하기

기본적으로 사망한 엔티티의 추가 데이터는 복사되지 않습니다. 플레이어의 데이터를 복사하려면 부착물의 builder에 `#copyOnDeath`를 호출하세요.
원칙적으로, 엔티티 사망시 데이터는 삭제됩니다. 플레이어가 부활할 때 데이터를 유지하려면 데이터를 정의할 때 `AttachmentType#copyOnDeath`를 호출하세요, 리스폰 하면서 이전 데이터를 복사합니다.

데이터 복사 과정을 수정하시려면 `PlayerEvent.Clone`사용하세요. 이 이벤트는 `#isWasDeath` 메서드로 사망했다가 부활하는 것인지, 아니면 엔드에서 돌아오는 것인지 구분할 수 있습니다. 엔드에서 돌아오는 경우 데이터가 그대로 유지되니 데이터 복사를 피하기 위해 이 두 경우를 구분하세요.
직접 데이터를 복사하시려면 `PlayerEvent.Clone` 이벤트를 사용하세요. `#isWasDeath`사망했다가 부활하는 것인지, 아니면 엔드에서 돌아오는 것인지 구분할 수 있습니다. 엔드에서 돌아오는 경우 데이터가 그대로 유지되니 데이터를 복사하지 마세요, 안그럼 겹칩니다.

예시:

Expand Down
Loading

0 comments on commit d8e6857

Please sign in to comment.