#1.2 Самописное ядро для Minecraft — ULE на Go lang

от автора

Приветствую всех! Раз вы попали сюда, значит, вы хотите создать собственное ядро для игры Minecraft на языке программирования Go. Эта статья — римейк статьи о создании ядра, поэтому авторство можно приписать первоначальному автору. Однако так как он перешел на Rust, я получил эстафету и продолжил написание ядра на Go. В его коде было много ошибок, так как почти всё хранилось на его GitHub, который он почистил для Rust. Я переработал систему, чтобы вся основная часть хранилась локально, кроме библиотеки.

Итак, мы будем использовать компилятор GoLand от JetBrains. Версия Go — 1.20.

Если вы используете VS Code, скачайте GoLand и установите расширение для компиляции Go.

Итак, версия Go — 1.20,
версия ядра — 1.12.2.

Создадим проект.
Когда откроете проект, вы увидите следующее:

окно программы GoLand от JetBrains

окно программы GoLand от JetBrains

При ошибке:
$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"

Здесь CyberCoreqwerty — это имена переменных, а mainproject — имена проектов, которые вы создали. Но даже после этого у вас будет красным подсвечиваться импорт, но пока оставим это.Также у вас будут гореть строки:

_, 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), то, что вы видите в виде текста под названием сервера. Я также добавил функцию для поиска картинки 64×64, чтобы было красивее.

Для генерации 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
Почему бы и нет?

Куда дальше?

Генерация мира. Куда же ещё. И подключение игрока.


ссылка на оригинал статьи https://habr.com/ru/articles/888816/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *