- Witchcraft adalah proyek eksperimen yang mengimplementasikan server Minecraft dari nol hanya dengan Bash, menargetkan koneksi klien sungguhan dengan menangani protokol biner, Join Game, hingga pengiriman chunk
- Keterbatasan Bash yang tidak bisa mempertahankan null byte di dalam string diakali dengan memproses biner di dalam pipe
dddanxxdalih-alih menyimpannya ke variabel - Kesulitan implementasi berfokus pada format data khas Minecraft seperti VarInt/VarLong, floating point IEEE 754, Position, dan NBT; terutama konversi Double yang lambat karena biaya pemanggilan perintah eksternal
- Server diperluas secara bertahap melalui urutan paket Server List Ping, handshake, Join Game, dan Chunk Data And Update Light; Dimension codec memakai blob biner NBT yang diambil dari server vanilla
- Dengan plugin berbasis hook, pembuatan world dan efek bisa diubah, tetapi masih ada keterbatasan seperti multiplayer yang belum selesai, komunikasi thread berbasis
/dev/shm/witchcraft, pertukaran data yang lambat, dan ketergantungan pada BusyBox 1.35.0
Menangani protokol Minecraft biner di Bash
- Upaya awal menargetkan Classic protocol tahun 2009, tetapi segera terlihat batasannya: Bash sulit mem-parsing data biner dengan benar
- String Bash mengabaikan null byte (0x00), dan tidak ada cara untuk mendeteksi apakah null byte muncul, sehingga data bisa rusak pada protokol biner yang ketat
- Solusinya adalah menjaga data biner tetap berada di dalam pipe, bukan memasukkannya ke variabel Bash atau command substitution
dd count=$len bs=1 status=none | xxd -pdipakai untuk membaca jumlah byte yang dibutuhkan lalu mengubahnya menjadi string hex- Pencocokan pola, substitusi, dan ekstraksi data dilakukan di atas string hex
- Pengiriman respons mengubahnya kembali menjadi biner dengan opsi reverse milik
xxd
- Untuk menerima koneksi pada port TCP default Minecraft, digunakan
ncat, lalu saat ada koneksi masuk dijalankan skrip shell utamamc.sh
Implementasi tipe data protokol
- Target paling cocok untuk diimplementasikan terlebih dahulu adalah paket Server List Ping
- Ini bukan paket wajib, jadi meski server tidak merespons dengan benar, klien tetap bisa masuk ke game
- Namun paket ini memudahkan pemahaman konsep inti protokol seperti data types
-
VarInt dan VarLong
- VarInt/VarLong di Minecraft adalah varian LEB128 yang mungkin terasa familier bagi pengguna MQTT
- LEB128 adalah metode kompresi untuk menyimpan panjang integer dengan membagi byte menjadi 1 bit sinyal dan 7 bit data
- Jika bit pertama 0, byte itu adalah yang terakhir; jika 1, byte berikutnya masih berlanjut
- Karena sulit memindahkan reference implementation apa adanya ke Bash, dibuat encoder sendiri dengan modulo dan pembagian
- Decoder diimplementasikan mirip cara referensi dengan operasi AND dan perkalian
- Yang lebih merepotkan daripada LEB128 itu sendiri adalah karena tipe ini muncul di seluruh protokol bercampur dengan int, long, dan signed short biasa
-
Floating point IEEE 754
- Konversi IEEE 754 Double adalah salah satu bagian yang paling merepotkan dalam implementasi
- Implementasi dasarnya memerlukan loop untuk menerapkan pangkat negatif, tetapi Bash tidak mendukung pangkat negatif secara bawaan
- Perl dihindari, dan
bctampaknya tidak mendukung perpangkatan di lingkungan yang dipakai atau pada versi BusyBox tersebut awkbisa menangani pangkat negatif seperti2**-1, sehingga dipakai untuk implementasi konversi- Implementasi awal sangat lambat, sampai butuh beberapa menit untuk memproses sekitar 50~100 paket yang dikirim klien pada paket Player Move, masing-masing dengan 3 nilai Double X·Y·Z
- Struktur yang semula berulang kali memanggil
awkdi dalam loopforBash dipindahkan ke loop internalawkuntuk mengurangi jumlah pemanggilan perintah eksternal - Setelah itu, konversinya turun menjadi sekitar 10ms pada Xeon E5-2680v2
- Angka sekitar 350ms pada versi sebelumnya bukan hasil pengukuran yang benar-benar pasti
-
Tipe data Position
- Position adalah tipe data berbasis Long 64-bit buatan Mojang
- X disimpan di 26 bit teratas, Z di 26 bit tengah, dan Y di 12 bit terbawah
- Bash memiliki operator bitshift yang diperlukan, jadi implementasinya cukup mudah
- Namun tipe ini tidak terlalu sering digunakan, dan banyak paket justru menyimpan koordinat X·Y·Z sebagai nilai Double terpisah
- Akibatnya, data posisi membesar dari 64 bit menjadi 192 bit per paket
- Jika diasumsikan hanya perlu angka hingga world border bawaan 30.000.000, maka didapat ketelitian floating point 9 digit
- Komunikasi server biasa memakai zlib, dan untuk representasi posisi di dalam blok secara realistis tidak perlu lebih dari 2~3 digit desimal
-
NBT
- NBT adalah format internal Mojang, semacam JSON untuk data biner
- Seperti JSON, format ini juga dipakai untuk menyimpan data arbitrer di luar spesifikasi formal
- Misalnya, Mojang menyimpan bitstream panjang variabel sebagai array Long
- Jika array tidak sejajar dengan Long atau byte, Long terakhir akan di-padding dengan 0
- Parser NBT sempat hampir selesai, tetapi pada akhirnya dianggap tidak layak untuk dituntaskan
- Kode tersebut hilang karena sistem crash saat direktori proyek banyak memakai
tmpfs
Membuat server yang bisa dimasuki
- Langkah setelah Server Ping adalah menangani handshake dan paket tambahan yang diperlukan agar benar-benar bisa masuk ke game
- Agar klien bisa masuk ke server, handshake harus selesai dan klien harus menerima paket chunk, player position, inventory, serta join game
- Hambatan terbesar adalah struktur data di dalam paket Join Game dan paket Chunk
-
Join Game
- Paket Join Game mengirim metadata awal
- Isinya mencakup entity ID pemain, gamemode, informasi world, dan Dimension codec yang hadir mulai sekitar Minecraft 1.16
- Dimension codec adalah NBT Compound sehingga beban implementasinya besar
- Witchcraft memakai field NBT ini dengan mengambilnya dari server vanilla
- Bagian ini adalah satu-satunya blob biner dalam implementasi, dan meski bisa diimplementasikan ulang, dianggap tidak perlu dikustomisasi
-
Chunk Data And Update Light
- Paket Chunk Data And Update Light awalnya terlihat besar dan rumit, tetapi menjadi sederhana jika beberapa field BitSet dibiarkan
0x00dan field Block Entity tidak dikirim - Field yang tersisa adalah X, Y, heightmaps, dan field Data
- heightmaps adalah bentuk pengkodean yang lebih rumit dari pengulangan
b000000010, dan pada dasarnya dianggap bisa berisi nilai apa pun
- Paket Chunk Data And Update Light awalnya terlihat besar dan rumit, tetapi menjadi sederhana jika beberapa field BitSet dibiarkan
Penanganan Chunk Section dan palette
- Field Data adalah array Chunk Section
- Chunk Section berisi blok 16×16×16, dan beberapa section dapat ditumpuk untuk membentuk satu Chunk
- Untuk menyederhanakan implementasi, array ini hanya memakai satu elemen
- Chunk Section terdiri dari block count, block states container, dan biome container
- block states dan biome container memakai struktur palette
- Sebelum data blok sebenarnya, server harus mendefinisikan pemetaan antara local block ID dan global block ID
- Strukturnya dibuat agar sebanyak mungkin data muat di ruang sekecil mungkin
- Definisi blok bisa diperkecil hingga minimal 4 bit
- Demi kemudahan pengelolaan, Witchcraft memakai 8 bits per block alih-alih minimum 4 bit
- Ini menyediakan 256 entri palette yang bisa dipakai
- Data chunk yang sebenarnya cukup mengirim angka hex yang menunjuk ke entri palette
- Palette 4 bit juga mudah ditangani karena satu byte pada string hex bisa mewakili dua blok, tetapi itu membatasi chunk menjadi 16 blok per chunk
- Standarnya mengizinkan dari 4 bits per block sampai 9 bits per block, dan selain itu dianggap sebagai direct palette mapping 15 bits per block
- Palette biome ditangani dengan cara terpisah
- Mengirim palette kosong lalu memetakan biome ID
0x01, yaituminecraft:plains, secara langsung ke semua region di dalam chunk - Ini berdasarkan hasil reverse engineering terhadap perilaku vanilla
- Diduga dokumentasi yang ada untuk bagian paket ini bisa jadi tidak akurat
- Mengirim palette kosong lalu memetakan biome ID
Struktur plugin berbasis hook dan demo
- Implementasi dasar saja hanya menampilkan chunk biasa, sehingga dibutuhkan demo untuk menunjukkan bahwa server bisa melakukan lebih dari sekadar menampilkan chunk
- Agar tidak perlu membuat source tree terpisah untuk tiap demo, fungsi-fungsi yang bisa dioverride disebut hooks, dan server dapat memuat kode pengguna
- Dengan struktur ini, dimungkinkan mengubah bentuk world sampai menyambungkan
pkt_effectagar saat menggerakkan mouse pemain mengeluarkan ticking noise - Plugin contoh membuat chunk dengan memilih blok acak dari palette bawaan
- Di
hook_chunks(), dipanggilchunk_header - Untuk 4096 blok, ditambahkan nilai hex dari
RANDOM%30 - Chunk yang dihasilkan disimpan ke
$TEMP/world/0000000000000000 - Lalu
pkt_chunkdipanggil beberapa kali untuk mengirim chunk ke koordinat sekitar
- Di
- Demo lain, digmeout, adalah game sederhana berbasis skor
- Pemain dilempar ke chunk berisi batu dan bijih yang ditempatkan secara acak
- Tujuannya menambang bijih paling bernilai hingga batas waktu habis
Keterbatasan Witchcraft
- Bash sangat lemah dalam menangani pecahan desimal
- Integer masih bisa ditangani sampai tingkat tertentu
- Pecahan desimal harus diperlakukan dengan mengalikannya saat input lalu menaruh titik di posisi yang sesuai saat output
- Karena itu, sebagian besar atau bahkan semua angka yang diproses Witchcraft adalah int
- Multiplayer tidak benar-benar berfungsi dengan baik
- Sampai taraf tertentu bisa jalan, tetapi tidak ada waktu yang dihabiskan untuk menyelesaikan atau memolesnya
- Secara teknis, Witchcraft adalah multi-threaded server
- Akibatnya dibutuhkan hack yang kurang baik untuk komunikasi antar-thread
- Sebagian besar data global disimpan di bawah
/dev/shm/witchcraft, yang secara internal dirujuk sebagai$TEMP
- Performa tetap menjadi keterbatasan besar
- Terutama pertukaran data antar beberapa thread yang lambat
- Sulit mengirim data dalam jumlah besar, dan membuat lalu mengirim 16 solid chunk bisa memakan waktu hingga 1 detik
- Saat ini hanya berjalan jika BusyBox 1.35.0 terbaru terpasang
- Belum diuji dengan GNU coreutils, dan diperkirakan tidak akan berfungsi
1 komentar
Komentar Hacker News
Sebagai orang yang sudah banyak mengerjakan server Minecraft yang bisa diskrip untuk Java dan Bedrock, ini cukup mengesankan
Poin plus juga karena memakai “duckduckgoing” di dalam kalimat
Diskusi saat itu: https://news.ycombinator.com/item?id=30347501 — 92 komentar
Eksponen negatif hanyalah 2^(-n) = 1/(2^n)
Agak mengejutkan penulis tidak terpikir akan hal ini meski dalam contohnya sendiri sudah memakai 2^-1 = 0.5, dan akhirnya menyerahkannya ke awk
Seharusnya memakai library bodoh(?) buatanku, ctypes.sh: https://github.com/taviso/ctypes.sh
Dengan begitu, libm, poll(), select(), dan semacamnya bisa diakses dari Bash :)
Rasanya seperti menghidupkan kembali Frankenstein
Kalau sampai butuh kode C terkompilasi yang dipasang terpisah di Bash, ya sama saja bisa meminta C atau Python, jadi alasan untuk menyebutnya “ditulis dengan Bash” jadi lemah. Tentu ini kukatakan dengan hormat, dan aku suka proyek-proyek “tidak berguna” seperti ini
Namun proyek Minecraft ini juga bergantung pada tool eksternal seperti xxd, jadi dalam kasus ini memakai ctypes.sh tidak akan lebih buruk, malah masuk akal
Sebagai catatan, tanpa dd atau xxd pun kita bisa membaca, menyimpan, memanipulasi, menghitung, dan mencetak biner murni dengan Bash. Byte yang bermasalah hanya null; meski tidak bisa disimpan langsung, kita bisa mendeteksi dan mencatat fakta bahwa byte itu terbaca, lalu memulihkannya saat output nanti atau memakainya sebagai nilai angka untuk indeks array, offset byte, dan sebagainya
Contoh minimal untuk menyalin file biner adalah
while LANG=C IFS= read -d '' -r -n 1 x ;do printf '%c' "$x" ;done bin2. Namun ini hanya seperti melakukan cat tanpa cat, jadi kegunaannya tidak terlalu terlihatContoh yang lebih umum dan berguna ada di https://gist.github.com/bkw777/c1413d0e3de6c54524ddae890fe8d705
Dengan kombinasi
LANG=C,IFS=, dan-d'', semua byte selain 0x00 bisa diakses, dan dari nilai balikread()kita bisa membedakan “apakah membaca 0x00”, “apakah tidak membaca apa pun”, dan “apakah input sudah berakhir”Tidak perlu juga memakai
<dan>untuk seluruh perintahwhile(). Buka saja denganexec 3<>file_or_fifo_or_tty, lalu di dalam loop gunakanread -u3,printf >&3, dan seterusnyaContoh membaca dari port serial ada di https://gist.github.com/bkw777/ddde771cc85fdd888c7ec74953193d66, dan kode yang benar-benar kupakai ada di https://github.com/bkw777/pdd.sh. Lihat
tpdd_read,tpdd_write,file_to_fhex,str_to_shex; semuanya membaca dan menulis port serial serta file lokal sambil memproses data dengan berbagai caraLoop-loop ini bahkan bukan proses eksternal, apalagi subshell. Variabel yang dimanipulasi di dalam loop tetap berada di konteks aslinya. Karena tidak ada child, menyebutnya parent shell pun agak janggal
Contoh-contohnya membaca satu byte demi satu byte dengan
read -n 1, tetapi itu tidak wajib. Jika membaca tanpa-n1, setiap iterasi loop akan mengumpulkan sebanyak mungkin data di antara null, sehingga memakai lebih sedikit memori dan jumlah iterasiUntuk kebutuhanku, aku harus memotong dan memanipulasi byte individual serta rentang byte berdasarkan offset numerik, dan datanya juga kecil menurut standar sekarang, jadi array pasangan heksadesimal sangat nyaman. Jadinya
a[n] == byte n, semua karakter yang bisa dicetak, yang tidak bisa dicetak, dan null dapat diperlakukan sama, dan selain bisa disimpan/dibuat ulang denganprintf, nilainya juga bisa langsung dipakai sebagai angka seperti0x$natau0x${a[n]}Misalnya, byte kedua dari
h[]bisa dibaca sebagai panjang payload yang mengikutinya, lalu payload itu dipotong dengan${h[@]:2:0x${h[1]}}Nah, ini baru benar-benar situs hacker. Luar biasa
Aku tidak jago pemrograman Bash, tapi mengejutkan bahwa pada praktiknya Bash lebih kuat dan tidak semengerikan yang dibayangkan
Bash adalah salah satu tool favoritku, setara dengan vim dan Lua
Ia berupa satu binary kurang dari 2MB, ada di mana-mana, dan bisa melakukan jauh lebih banyak hal daripada yang dipikirkan banyak orang
Dengan shellcheck dan kebiasaan yang baik, Bash juga bisa dibuat mudah dibaca dan aman
Kalau butuh hal lain, cukup tempelkan utility C/C++ tanpa FFI yang rumit, dan tidak perlu menarik banyak dependency entah dari mana lewat pip/npm dan semacamnya
Sering ada skrip Bash inline di dalam CI dan Dockerfile, dan aku juga suka memindahkan semuanya ke file skrip terpisah. Sekarang karena ada shellcheck di CI, kode-kode itu juga diperiksa
Developer tidak suka karena katanya “mengurangi keterbacaan”, padahal bukannya kurang terbaca, justru jauh lebih aman
Masalah berikutnya adalah skrip Bash yang sama diduplikasi ke file yang sama di beberapa repository. Ini struktur build terdistribusi, dan aku terbuka kalau ada ide perbaikan
Aku menyesal tidak lebih banyak meluangkan waktu saat masih mahasiswa. Karena Bash ternyata salah satu dari sedikit tool yang terus mengikutiku sepanjang karier
Aku penasaran apakah membuat server kustom untuk game komersial masih ada sampai sekarang
Khususnya versi Java berada di posisi yang cukup unik: terus mendapat update konten besar gratis, tetapi tetap secara resmi mendukung menjalankan semua versi lama, dan komunitas modding-nya juga sangat aktif
Minecraft (Java) adalah game sekaligus nyaris seperti game engine yang di atasnya orang lain membangun sesuatu
Mereka melakukannya untuk memperbaiki bug, belajar bahasa, atau ingin melihat cara kerja bagian dalamnya
Minecraft adalah salah satu game terbaik yang muncul setelah Quake
Astaga, tidak menyangka hal seperti ini benar-benar terjadi
Aku sudah membaca banyak tulisan tentang cara implementasi, tetapi yang ini luar biasa. Ini jelas salah satu tulisan proyek aneh terbaik yang pernah kubaca, dan benar-benar tulisan yang hebat