Pull to refresh

Создание генератора мира для minecraft

Reading time 4 min
Views 48K

Введение


Думаю, почти все читатели Хабра слышали про майнкрафт, кто-то играл в сингле, кто-то на одном из многочисленных серверов, был даже небольшой сервер у кого-то из хабраюзеров. После двух месяцев игры я задумался — а реально ли написать свой генератор карты? Как оказалось, это вполне возможно сделать за несколько дней неторопливого гугления и кодинга.

Немного технической части

Согласно вики-проекту майна, карты хранятся в файлах регионов (32x32 блока по 16x16x128 кубов, итого 262144 квадратных блоков на регион), имеющих следующую структуру:
  1. 4096 байт, содержащих оффсеты чанков (так называются блоки 16x16x128) и их размер в блоках по 4кб, округляя вверх, 3 байта оффсет, 1 — размер
  2. 4096 байт timestamp'ов чанков, по 4 байта на каждый
  3. Оставшееся место до конца файла — собственно, данные чанков, сжатые Zlib. 4 байта — размер сжатых данных, 1 — способ сжатия (по умолчанию 2, Zlib (RFC1950)), размер-1 запакованная злибом NBT-структура, т.е сам контейнер кубов
    Если упакованные данные занимают меньше целого числа секторов по 4 кб, то остаток сектора заполняется нулями, т.к каждый чанк должен начинаться с оффсета, выраженного целым числом секторов по 4096 байт

Выбор языка

Реализовать такую структуру можно на любом языке, я остановился Delphi 7. Во-первых, это пока единственный язык, который я знаю, во-вторых, именно на 7 версии года 4 назад я начинал писать блокноты по мануалам из Игромании.

Код

Так как данные хранятся в сжатом виде, нам необходим модуль zlib.
Я использовал ZlibEx

Для начала создадим класс чанка, в который будем впоследствии писать данные

  Tchunk = class(TObject)
  private
  public
    Data:     tmemorystream;
    c_data:   tmemorystream;
    c_stream: tzcompressionstream;
    constructor Create;
    procedure writeblock(x, y, z, block: integer); overload;
    procedure writeblock(x, y, z, block, color: integer); overload;
    procedure compress;
  end;


Код этого класса:

constructor tchunk.Create;
begin
  Data      := TMemoryStream.Create;
  Data.size := 82360;
  Data.LoadFromFile('data.bin');
  c_data   := TMemoryStream.Create;
  c_stream := tzcompressionstream.Create(c_data, zcdefault, 15, 8, zsdefault);
end;

procedure tchunk.writeblock(x, y, z, block: integer);
begin
  Data.Seek(form1.getoffset(x, y, z) + 16487, 0);
  Data.Write(block, 1);
end;




procedure tchunk.compress;
var
  buffer: array  [0..82360] of byte;
begin
  c_data.Position := 0;
  Data.Position   := 0;
  Data.Read(buffer, 82360);
  c_stream.writebuffer(buffer, 82360);
  c_stream.Free;
  c_data.SaveToFile('file' + IntToStr(n));
end;


Функция getoffset выдает нужое смещение по формуле y + ( z *128 + ( x * 128 * 16 ) )
function tform1.getoffset(x, y, z: integer): integer;
begin
  Result := y + (z * 128 + (x * 128 * 16));
end;


Добавим в var пару переменных:

chunks:array[0..32] of array[0..32] of tchunk;
n:         integer=0;


Процедура для сборки всех чанков в готовый файл:

procedure tform1.SwapEndiannessOfBytes(var Value: cardinal);
var
  tmp: cardinal;
  i:   integer;
begin
  tmp := 0;
  for i := 0 to sizeof(Value) - 1 do
    Inc(tmp, ((Value shr (8 * i)) and $FF) shl (8 * (sizeof(Value) - i - 1)));
  Value := tmp;
end;

procedure tform1.generatefile;
var
  fileoffset: integer;
  time, compressiontype, counter: integer;
  filename: string;
  regionfile: tfilestream;
  tmp: cardinal;
  size: integer;
  n_x, n_z: integer;
  bu: array[0..99999] of byte;
  n:  integer;
  roundedsize: integer;
  neededsize: integer;
  d:  byte;
begin
  fileoffset := 2;
  time := $d8de2f4e;
  compressiontype := $02;
  filename := GetVar('Appdata') + '\.minecraft\saves\NewWorld\region\r.0.0.mcr';
  regionfile := tfilestream.Create(filename, fmcreate);
  n := 0;
  for n_x := 0 to 31 do
    for n_z := 0 to 31 do
    begin
      chunks[n_x][n_z].compress;
      roundedsize := ((chunks[n_x][n_z].c_data.Size) div 4096);
      if (((chunks[n_x][n_z].c_data.Size) mod 4096) > 0) then
        Inc(roundedsize);
      regionfile.seek((4 * ((n_x mod 32) + (n_z mod 32) * 32)), 0);
      tmp := fileoffset;
      SwapEndiannessOfBytes(tmp);
      tmp := tmp shr 8;
      regionfile.Write(tmp, 4);
      regionfile.seek(4 * ((n_x mod 32) + (n_z mod 32) * 32) + 3, 0);
      regionfile.Write(roundedsize, 1);
      size := chunks[n_x][n_z].c_data.Size + 1;
      regionfile.seek(fileoffset * 4096, 0);
      tmp := size;
      SwapEndiannessOfBytes(tmp);
      regionfile.Write(tmp, 4);
      regionfile.Write(compressiontype, 1);
      chunks[n_x][n_z].c_data.Position := 0;
      chunks[n_x][n_z].c_data.readbuffer(bu, chunks[n_x][n_z].c_data.size);
      regionfile.Writebuffer(bu, chunks[n_x][n_z].c_data.size);
      regionfile.seek((n) * 4 + 4096, 0);
      regionfile.Write(time, 4);
      fileoffset := fileoffset + ((chunks[n_x][n_z].c_data.Size) div 4096);
      if (((chunks[n_x][n_z].c_data.Size) mod 4096) > 0) then
        fileoffset := fileoffset + 1;
      Inc(n);
    end;
  neededsize := 4096 * fileoffset - regionfile.Size - 1;
  regionfile.Seek(regionfile.Size, 0);
  d := 00;
  for n := 0 to neededsize do
    regionfile.Write(d, 1);
  regionfile.Free;
end;


Всё, теперь мы имеем метод записи любого блока по любой координате, в пределах региона. При желании, несложно повторить то же для остальных регионов, надо строк 10 кода.

Обертка для writeblock:

procedure tform1.writeworld(x, y, z, block: integer);
var
  xw, zw: integer;
begin
  xw := (x div 16);
  zw := (z div 16);
  chunks[xw][zw].writeblock(x mod 16, y, z mod 16, block);
end;


Генерация мира, его сжатие и сохранение.

procedure TForm1.Button4Click(Sender: TObject);
var
  x, y, z: integer;
  xx, zz:  integer;
  image:   tbitmap;
begin
  for xx := 0 to 31 do
    for zz := 0 to 31 do
    begin
      chunks[xx][zz] := tchunk.Create;
    end;
  image := tbitmap.Create;
  image.LoadFromFile('image.bmp');
     for x:=0 to 127 do
      for y:=0 to 116 do
      begin
      if image.Canvas.Pixels[x,y]=clblack then
        form1.writeworld(x,117-y,0,49);
      if image.Canvas.Pixels[x,117-y]=clwhite then
        form1.writeworld(x,y,0,80);
      end;
form1.generatefile;


Результат:
image
image

Можно генерировать не только пиксельарт, но произвольные фигуры, все, что можно задать какой-либо формулой. Например, пол в виде синусоиды:
image

Проект можно скачать тут.

Known bugs:
  • Невозможно сохранять изменения в сгенерированном регионе (возможно, из-за того, что пишется одинаковый timestamp, который не совпадает с временем последнего сохранения в level.dat, как разберусь с форматом последнего — попробую реализовать)
  • Спавн лучше переставить с помощью McEdit, т.к вполне возможно, что после генерации он окажется в сотне блоков над землей, что чревато летальным исходом(тоже можно менять в level.dat)
  • Нет генерации света, вместо этого освещены все блоки, даже под землей(Рассчет освещения — отдельная серьёзная задача, пока не готов ее решать)

ToDo:
  • Починить сохранение, т.к без этого теряется половина смысла
  • Сделать поддержку записи дополнительной инфы(цвет шерсти, листвы, ориентация печек, etc) // частично готово
  • Какое-то подобие ландшафта(холмы/дома/озера)


Update:
  • Доработал generatefile, сделал нормальный разворот
  • Форматирование кода
  • Наброски additional block data, см. в проекте, ссылка обновлена

Ссылки:
Tags:
Hubs:
+39
Comments 49
Comments Comments 49

Articles