네트워크와 동기화
페이로드 채널
PayloadChannel<String> greetings = mod.network().channel("greetings", PayloadCodec.string());
greetings.onServerReceive((message, sender) -> { /* 서버 스레드 */ });
greetings.onClientReceive(message -> { /* 클라이언트 스레드 */ });
greetings.sendToPlayer(player, "hello");
greetings.sendToServer("thanks"); // 클라이언트에서
- 채널은 common 엔트리포인트에서 선언하며, 채널 ID는
<mod id>:<path>형식입니다. - 받은 메시지는 해당 게임 스레드(서버 스레드 또는 클라이언트 스레드)에서 처리합니다.
- 크기 한도: 서버 방향은 32 KiB, 클라이언트 방향은 1 MiB입니다. 한도를 넘으면
POLY-NETWORK-003오류가 납니다. - 잘못된 페이로드: 버리고
POLY-NETWORK-004로 기록합니다. - 연결되기 전에 보내면
POLY-NETWORK-002오류가 납니다.
클라이언트는 어떤 값이든 보낼 수 있으므로, 서버는 클라이언트가 보낸 값을 항상 검증해야 합니다.
코덱 직접 만들기
문자열 외의 메시지는 PayloadCodec<T>의 encode와 decode를 구현해서 보냅니다.
record Ping(int id, String text) {}
PayloadCodec<Ping> PING_CODEC = new PayloadCodec<>() {
@Override
public void encode(Ping value, PayloadWriter out) {
out.writeInt(value.id()).writeString(value.text());
}
@Override
public Ping decode(PayloadReader in) {
return new Ping(in.readInt(), in.readString());
}
};
PayloadWriter/PayloadReader는 boolean, int, long, double, String, 바이트 배열을 읽고 쓸 수 있습니다. 코덱은 네트워크 스레드에서 실행되므로 게임 상태에 접근하지 마세요.
접속 준비 단계의 교환
플레이어가 월드에 들어오기 전, 게임의 접속 준비(configuration) 단계에서 서버가 클라이언트에 질문하고 답을 받을 수 있습니다. 버전 확인, 설정 맞추기, 클라이언트가 먼저 받아야 하는 데이터를 전달할 때 사용합니다. 교환이 끝나야 플레이어가 월드에 들어오며, 서버는 이 단계에서 접속을 거절할 수 있습니다.
// common 엔트리포인트: 서버와 클라이언트 양쪽에서 선언
handshake = mod.network().configurationExchange("handshake", PayloadCodec.string())
.serverRequest((id, name) -> Optional.of("version?")) // 이 플레이어에게 보낼 내용, 비우면 건너뜀
.serverCheck((id, name, reply) -> reply.filter(v -> v.startsWith("my_mod "))
.isPresent() ? Optional.empty() // 접속 허용
: Optional.of("Install My Mod on your client to join.")); // 거절 메시지
// client 엔트리포인트
handshake.clientAnswer(request -> "my_mod 1.2.0");
- 서버의
serverRequest가 플레이어마다 보낼 메시지를 정합니다.Optional.empty()를 돌려주면 그 플레이어는 교환을 건너뜁니다. - 클라이언트의
clientAnswer가 답합니다. - 서버의
serverCheck가 그 답을 확인합니다.timeoutTicks(기본 100틱, 5초) 안에 답이 오지 않으면 빈 값을 받으며, 모드가 없는 클라이언트는 답하지 않습니다. 빈 값을 반환하면 접속을 허용하고, 메시지를 반환하면 그 메시지와 함께 접속을 끊습니다.
serverCheck가 없으면 답이 없는 플레이어는 거절됩니다("This server needs the mod … on your client.").- 서버 쪽 코드는 서버 스레드에서, 클라이언트 쪽 코드는 클라이언트 스레드에서 호출됩니다.
- 교환은 게임이 레지스트리를 맞춘 뒤 플레이어를 월드에 넣기 전에, 선언한 순서대로 하나씩 진행합니다.
- 모드 코드가 예외를 던지면
POLY-NETWORK-005를 기록합니다. 요청이 실패하면 교환을 건너뛰고, 답이 실패하면 답하지 않으며, 검사가 실패하면 그 플레이어를 거절합니다.
엔티티 데이터
플레이어를 포함한 모든 엔티티에 모드 데이터를 붙일 수 있습니다.
EntityDataKey MANA = mod.content().entityData("mana", true); // true: 플레이어 자신의 클라이언트로 동기화
StoredData mana = player.data(MANA);
mana.data().putInt("value", 10);
mana.markChanged();
- 데이터는 엔티티 저장 데이터의
poly_entity_data에 저장되며, 플레이어는 리스폰해도 유지됩니다. - 클라이언트에서는 읽기만 할 수 있으므로 서버에서 바꾸세요.
동기화 옵션
EntityDataKey MOOD = mod.content().entityData("golem_mood", EntityDataOptions.DEFAULT
.syncedToTrackers() // 그 엔티티를 보는 모든 클라이언트로 전송
.withVersion(2, (data, fromVersion) -> { /* … */ })); // 옛 버전 저장 데이터는 불러올 때 변환
| 옵션 | 보내는 대상 | 보내는 시점 |
|---|---|---|
syncedToOwner() (entityData(path, true)) | 그 플레이어 자신의 클라이언트 | 값이 바뀔 때와 접속할 때 |
syncedToTrackers() | 그 엔티티를 보는 모든 클라이언트 | 클라이언트가 엔티티를 보기 시작할 때 한 번, 그리고 값이 바뀔 때마다 |
| (기본) | 보내지 않음 | — |
클라이언트에서는 entity.data(key)로 읽습니다. 동기화하지 않은 다른 플레이어나 몹의 데이터는 클라이언트로 전송되지 않습니다.
월드 저장소 동기화
StorageOptions options = StorageOptions.DEFAULT.syncedToClients();
world.storage(ResourceId.of("my_mod", "info"), options); // 클라이언트에서는 읽기 전용 사본
같은 저장소 ID를 다른 옵션으로 열면 IllegalArgumentException이 발생합니다. 클라이언트에서도 같은 옵션으로 여세요.
블록 엔티티 동기화
BlockEntityDefinition.builder().syncToClients()를 켜면 markChanged()를 호출할 때 블록 엔티티 데이터가 클라이언트로 전송됩니다. 자세한 내용은 블록 엔티티와 메뉴를 참고하세요.
오류 코드
| 코드 | 뜻 |
|---|---|
POLY-NETWORK-001 | 채널 ID 오류 |
POLY-NETWORK-002 | 연결되기 전에 전송함, 또는 등록이 닫힌 뒤에 선언함 |
POLY-NETWORK-003 | 크기 초과 |
POLY-NETWORK-004 | 해독 실패(버림) |
POLY-NETWORK-005 | 접속 준비 교환의 모드 코드가 예외를 던짐, 또는 잘못된 대기 시간 |
싱글 플레이 통합 서버의 네트워크 왕복만 자동으로 검증했습니다. 전용 서버와 별도 클라이언트 사이의 접속은 EULA 동의가 필요해 자동으로 검증하지 않았습니다.