Приветствую всех! Раз вы попали сюда, значит, вы хотите создать собственное ядро для игры Minecraft на языке программирования Go. Эта статья — римейк статьи о создании ядра, поэтому авторство можно приписать первоначальному автору. Однако так как он перешел на Rust, я получил эстафету и продолжил написание ядра на Go. В его коде было много ошибок, так как почти всё хранилось на его GitHub, который он почистил для Rust. Я переработал систему, чтобы вся основная часть хранилась локально, кроме библиотеки.
Итак, мы будем использовать компилятор GoLand от JetBrains. Версия Go — 1.20.
Если вы используете VS Code, скачайте GoLand и установите расширение для компиляции Go.
Итак, версия Go — 1.20,
версия ядра — 1.12.2.
Создадим проект.
Когда откроете проект, вы увидите следующее:

При ошибке:
$GOPATH/go.mod exists but should not
Берём и пересоздаём проект. Буквально. Увидели эту ошибку — просто кликаем File -> New Project.
Для остальных случаев создаём файл вручную.

Жмякните Empty File и назовите его main.
P.S. Смысла в выборе других опций нет, так как всё равно будете вставлять мой код. Если что, Empty File — это файл без содержимого, который можно создать в блокноте, а Simple Application — там будет модуль и главный метод. Но в обоих случаях на всё про всё будет всего 3 строчки кода.
Открываем вкладку Terminal внизу и вводим:
go get github.com/Tnze/go-mc@master
После этого в терминале вы увидите:
go: downloading github.com/Tnze/go-mc v1.19.4-0.20230417163417-4315d1440ce1
go: added github.com/Tnze/go-mc v1.19.4-0.20230417163417-4315d1440ce1
Это означает, что библиотека, которая нам нужна, была скачана.
Также был создан файл go.sum в проекте, в котором хранятся все импортированные файлы с хешами. Оттуда ничего удалять нельзя, если только вы не хотите принудительно отказаться от библиотеки (грубо говоря, удалить её).
Итак, у нас есть файл main.go. Вставляем в него код:
И так. У нас есть файл main.go.
Запишем туда код:
package main
// Импортируем пакеты
import (
"github.com/Tnze/go-mc/net"
"log"
)
func main() {
// InitSRV - Функция запуска сервера
// Запускаем сокет по адрессу 0.0.0.0:25565
loop, err := net.ListenMC(":25565")
// Если есть ошибка, то выводим её
if err != nil {
log.Fatalf("Ошибка при запуске сервера: %v", err)
}
// Цикл обрабатывающий входящие подключеня
for {
// Принимаем подключение или ждём
connection, err := loop.Accept()
// Если произошла ошибка - пропускаем соденение
if err != nil {
continue
}
// Принимаем подключение и обрабатываем его не блокируя основной поток
go acceptConnection(connection)
}
}
Как мы видим, для работы мы используем net из go-mc для подключения. В отличие от Distemi, мы поместили весь код в функцию main, что даст точку старта программе и укажет компилятору, что main.go — это главный файл.
У вас должна засветиться красным строка:go acceptConnection(connection)
Так как это должна быть отдельная функция в другом файле. Создадим его с названием accepter.go.
package main
import (
CyberCore "CyberCore/serverbound"
"github.com/Tnze/go-mc/net"
//server "github.com/Tnze/go-mc/server"
)
func acceptConnection(conn net.Conn) {
defer func(conn *net.Conn) {
err := conn.Close()
if err != nil {
return
}
}(&conn)
// Читаем пакет-рукопожатие(HandSnake)
_, nextState, _, _, err := CyberCore.ReadHandSnake(conn)
// Если при чтении была некая ошибка, то просто перестаём обрабатывать подключение
if err != nil {
return
}
// Обрабатываем следющее состояние(1 - пинг, 2 - игра)
switch nextState {
case 1:
acceptPing(conn)
default:
return
}
}
Теперь в файле main не будет подсвечиваться ошибка с acceptConnection, и работу с файлом main мы завершаем. Что нужно заменить в этом коде?
1) Во время импорта вы могли заметить строку:
CyberCore "CyberCore/serverbound" Мы объявили переменную CyberCore, импортируя файл по пути CyberCore/serverbound. Вы можете назвать переменную как угодно, но при импорте файла serverbound вам нужно указать правильный путь к файлу. Например:CyberCore "main/serverbound"
qwerty "project/serverbound"
Здесь CyberCore, qwerty — это имена переменных, а main, project — имена проектов, которые вы создали. Но даже после этого у вас будет красным подсвечиваться импорт, но пока оставим это.Также у вас будут гореть строки:
_, nextState, _, _, err := CyberCore.ReadHandSnake(conn)
иacceptPing(conn)
первая строчка связана с проблемой импорта, а как я говорил вернёмся позже. Сейчас разберёмся с ошибкой acceptPing - это файл, который отсутствует. Создадим его.
package main
import (
"encoding/json"
"log"
"CyberCore/config"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
_ "io/ioutil"
)
// Получаем пинг-подкючение(PingList)
func acceptPing(conn net.Conn) {
// Инициализируем пакет
var p packet.Packet
// Пинг или описание, будем принимать только 3 раза
for i := 0; i < 3; i++ {
// Читаем пакет
err := conn.ReadPacket(&p)
// Если ошибка - перестаём обрабатывать
if err != nil {
return
}
// Обрабатываем пакет по типу
switch p.ID {
case 0x00: // Описание
// Отправляем пакет со списком
err = conn.WritePacket(packet.Marshal(0x00, packet.String(listResp())))
case 0x01: // Пинг
// Отправляем полученный пакет
err = conn.WritePacket(p)
}
// При ошибке - прекращаем обработку
if err != nil {
return
}
}
}
// Тип игрока для списка при пинге
type listRespPlayer struct {
Name string `json:"name"`
ID uuid.UUID `json:"id"`
}
// Генерация JSON строки для ответа на описание
func listResp() string {
// Строение пакета для ответа( https://wiki.vg/Server_List_Ping#Response )
var list struct {
Version struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
} `json:"version"`
Players struct {
Max int `json:"max"`
Online int `json:"online"`
Sample []listRespPlayer `json:"sample"`
} `json:"players"`
Description chat.Message `json:"description"`
FavIcon string `json:"favicon,omitempty"`
}
// Устанавливаем данные для ответа
list.Version.Name = "ULE #1"
list.Version.Protocol = int(config.ProtocolVersion)
list.Players.Max = 100
list.Players.Online = -1
list.Players.Sample = []listRespPlayer{{
Name: "Пример игрока :)",
ID: uuid.UUID{},
}}
list.Description = config.MOTD
// Добавляем иконку сервера, если она есть
faviconPath := config.GetFaviconPath()
if faviconPath != "" {
faviconBase64, err := config.GetFaviconBase64(faviconPath)
if err == nil {
list.FavIcon = "data:image/png;base64," + faviconBase64
} else {
log.Printf("Ошибка получения иконки сервера: %v", err)
}
}
// Превращаем структуру в JSON байты
data, err := json.Marshal(list)
if err != nil {
log.Panic("Ошибка перевода в JSON из обьекта")
}
// Возращаем результат в виде строки, переведя из байтов
return string(data)
}
Опять ошибки, не так ли? Мы всё починим. Теперь в файле accepter не горит строка acceptPing. Окей, продолжим работу с файлом accepter.
Создадим новую папку (Directory) с названием serverbound и создадим новый файл handsnake.go для "рукопожатий".
В нём мы пока будем использовать только nextState, так как в первой части будет готов только пинг. Поэтому в обработке типа подключения из HandSnake мы используем только 1, что означает, что это пинг.
Далее по очереди идёт очень важный компонент для работы ядра — чтение HandSnake, который, как я описывал, был расположен в server/protocol/serverbound/handsnake.go. Всё, что находится в директории, связанной с протоколом, будет разделяться на ServerBound (для сервера) и ClientBound (для клиента). Поэтому при таком разделении у нас будет именно чтение HandSnake со следующим содержимым:
package serverbound
import (
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/net/packet"
)
// ReadHandSnake - чтение HandSnake пакета( https://wiki.vg/Protocol#Handshake )
func ReadHandSnake(conn net.Conn) (protocol, intention int32, address string, port uint16, err error) {
// Переменные пакета
var (
p packet.Packet
Protocol, NextState packet.VarInt
ServerAddress packet.String
ServerPort packet.UnsignedShort
)
// Читаем входящий пакет и при ошибке ничего не возращаем
if err = conn.ReadPacket(&p); err != nil {
return
}
// Читаем содержимое пакета
err = p.Scan(&Protocol, &ServerAddress, &ServerPort, &NextState)
// Возращаем результат чтения в привычной форме для работы(примитивные типы)
return int32(Protocol), int32(NextState), string(ServerAddress), uint16(ServerPort), err
}
И теперь когда в строке:
_, nextState, _, _, err := CyberCore.ReadHandSnake(conn)
вы замените CyberCore на ту переменную, которую вы указали при импорте, все ошибки с файла пропадут.
Теперь плавно переходим к файлу acceptPing, в котором остались ошибки. И снова проблема с импортом. Как я уже говорил ранее, нужно заменить CyberCore на имя вашего проекта (или хотя бы имя папки, в которой находится ваш проект).
Теперь создаём новую директорию (Directory) с именем config и создаём новый файл basic.go. В нём будут установлены некоторые дефолтные значения, такие как версия протокола (для версии 1.12.2 — это 340), а также MOTD (Message of the Day), то, что вы видите в виде текста под названием сервера. Я также добавил функцию для поиска картинки 64x64, чтобы было красивее.
Для генерации JSON из структуры используем json.Marshal, который может вывести ошибку. Так как он не должен вывести ошибку, мы завершаем работу программы с ошибкой, если это случится.
Код будет следующим:
package config
import (
"encoding/base64"
"github.com/Tnze/go-mc/chat"
"io/ioutil"
)
var (
ProtocolVersion uint16 = 340
MOTD chat.Message = chat.Text("Тестовое ядро §aULE")
)
// получить место расположение иконки 64х64 в папке config
func GetFaviconPath() string {
return "config/icon.png"
}
// GetFaviconBase64 - Возвращает Base64-кодированную строку с иконкой сервера.
func GetFaviconBase64(faviconPath string) (string, error) {
// Читаем файл с иконкой
faviconData, err := ioutil.ReadFile(faviconPath)
if err != nil {
return "", err
}
// Кодируем в Base64
faviconBase64 := base64.StdEncoding.EncodeToString(faviconData)
return faviconBase64, nil
}
Я добавил в код поддержку своей картинки. У меня она называется icon и имеет формат png. Эта картинка находится в папке с файлом basic.go, то есть в папке config.
Для тестирования нужно нажать сюда:

Жмём на Add new Configuration и нажимаем Go build

Теперь устанавливаем следующие вещи:Красная стрелка — ставите имя для конфигурации (можно игнорировать).Голубая стрелка — меняем тип запуска на Directory (место Package).

Конечный результат должен быть таким:

После этого жмём Apply и Ok потом нажимаем Shift+F10, видим в консоли такую картинку
GOROOT=C:\Users\Ukraine\go\go1.20 #gosetup
GOPATH=C:\Users\Ukraine\go #gosetup
C:\Users\Ukraine\go\go1.20\bin\go.exe build -o C:\Users\Ukraine\AppData\Local\JetBrains\GoLand2023.1\tmp\GoLand___go_build_awesomeProject1.exe . #gosetup
C:\Users\Ukraine\AppData\Local\JetBrains\GoLand2023.1\tmp\GoLand___go_build_awesomeProject1.exe
Залетаем в Minecraft, добавляем в список сервера 0.0.0.0:25565
Видим это

-1 - я поставил в файле acceptPing.go
Почему бы и нет?
Куда дальше?
Генерация мира. Куда же ещё. И подключение игрока.