- Witchcraft, yalnızca Bash ile sıfırdan uygulanmış deneysel bir Minecraft sunucusu projesi; ikili protokolü, Join Game'i ve hatta chunk gönderimini ele alarak gerçek istemci bağlantısını hedefliyor
- Bash'in null byte karakterini dizgiler içinde koruyamama sınırlaması, ikili veriyi değişkenlere koymak yerine
ddvexxdpipe'ları içinde işleyerek aşılıyor - Uygulamadaki asıl zorluklar VarInt/VarLong, IEEE 754 kayan nokta, Position ve NBT gibi Minecraft'a özgü veri biçimlerinde yoğunlaştı; özellikle Double dönüşümü, dış komut çağrılarının maliyeti nedeniyle yavaştı
- Sunucu, Server List Ping, handshake, Join Game ve Chunk Data And Update Light paketleri sırasıyla eklenerek genişletildi; Dimension codec için vanilla sunucudan alınmış bir NBT ikili blob'u kullanılıyor
- Hook tabanlı eklentilerle dünya üretimi ve efektler değiştirilebiliyor; ancak çok oyunculu desteğin tamamlanmamış olması,
/dev/shm/witchcrafttabanlı thread iletişimi, yavaş veri alışverişi ve BusyBox 1.35.0 bağımlılığı hâlâ sürüyor
Bash'te ikili Minecraft protokolünü ele almak
- İlk deneme 2009 tarihli Classic protocol hedeflenerek yapıldı; ancak Bash ile ikili veriyi düzgün biçimde ayrıştırmanın zor olduğu sınırı daha en başta ortaya çıktı
- Bash dizgileri null byte (0x00) karakterini yok sayıyor ve null byte oluşup oluşmadığını tespit etmenin de bir yolu olmadığından, katı ikili protokollerde veri bozulabiliyor
- Geçici çözüm, ikili veriyi Bash değişkenlerine ya da komut ikamesine koymak yerine pipe içinde tutmak oldu
dd count=$len bs=1 status=none | xxd -pile gereken byte sayısı okunup hex dizgisine dönüştürülüyor- Desen eşleme, değiştirme ve veri çıkarma işlemleri hex dizgisi üzerinde yapılıyor
- Yanıt gönderirken
xxd'nin reverse seçeneğiyle tekrar ikili biçime çevriliyor
- Minecraft'ın varsayılan TCP portunda bağlantı kabul etmek için
ncatkullanılıyor; bağlantı geldiğinde ana kabuk betiğimc.shçalıştırılıyor
Protokol veri tiplerinin uygulanması
- İlk uygulanması en uygun hedef Server List Ping paketiydi
- Zorunlu bir paket olmadığı için sunucu buna düzgün yanıt vermese bile oyuna bağlanmak yine de mümkün
- Ancak data types gibi temel protokol kavramlarını öğrenmek için elverişli
-
VarInt ve VarLong
- Minecraft'taki VarInt/VarLong, MQTT deneyimi olanlara tanıdık gelebilecek bir LEB128 türevi
- LEB128, byte'ları 1 işaret biti ve 7 veri biti olarak ayırıp tamsayı uzunluğunu saklayan sıkıştırılmış bir biçim
- İlk bit 0 ise bu byte son byte, 1 ise sonraki byte da devam ediyor
- Bash'te reference implementation'ı birebir taşımak zor olduğundan, modulo ve bölme kullanan özel bir encoder yazıldı
- Decoder ise AND işlemleri ve çarpma kullanılarak referans yaklaşıma benzer şekilde kuruldu
- Asıl zahmet, LEB128'in kendisinden çok bunun normal int, long ve signed short ile karışık biçimde protokolün her yerine serpiştirilmiş olmasıydı
-
IEEE 754 kayan nokta
- IEEE 754 Double dönüşümü, uygulamanın en can sıkıcı bölümlerinden biriydi
- Temel uygulama negatif üsleri işleyen bir döngü gerektiriyor, fakat Bash negatif üs desteğini yerleşik olarak sunmuyor
- Perl'den kaçınıldı;
bcise kullanılan ortamda ya da BusyBox sürümünde üslü işlemleri desteklemiyor gibi görünüyordu awk,2**-1gibi negatif üsleri işleyebildiği için dönüşüm uygulamasında kullanıldı- İlk sürüm o kadar yavaştı ki, Player Move paketlerinde istemcinin gönderdiği yaklaşık 50~100 paket ile her paketteki X·Y·Z olmak üzere 3 Double'ı işlemek birkaç dakika sürebiliyordu
- Bash
fordöngüsü içinde tekrar tekrarawkçağıran yapı, dış komut çağrılarını azaltmak içinawkiçindeki döngülere taşındı - Bunun ardından dönüşüm süresi Xeon E5-2680v2 üzerinde yaklaşık 10ms düzeyine indi
- Önceki yaklaşık 350ms değeri ise kesin ölçülmüş bir sonuç değildi
-
Position veri tipi
- Position, Mojang'ın tasarladığı 64-bit Long tabanlı bir veri tipi
- X üst 26 bitte, Z orta 26 bitte, Y ise alt 12 bitte saklanıyor
- Bash'te gerekli bitshift işleçleri bulunduğu için uygulaması kolaydı
- Ancak bu tip çok yaygın kullanılmıyor; birçok paket X·Y·Z koordinatlarını ayrı Double değerleri olarak saklıyor
- Sonuç olarak konum verisi paket başına 64 bitten 192 bite çıkıyor
- Varsayılan world border olan 30,000,000'a kadar olan sayılara ihtiyaç duyulduğu varsayılırsa, 9 basamaklı kayan nokta doğruluğu elde ediliyor
- Normal sunucu iletişimi zlib kullanıyor ve blok içi konum gösteriminde pratikte 2-3 ondalık basamaktan fazlasına gerek olmadığı düşünülüyor
-
NBT
- NBT, Mojang'a ait dahili bir biçim; ikili veri için JSON'a benzeyen bir format
- JSON gibi, belirtimin dışındaki keyfi verileri saklamak için de kullanılıyor
- Örneğin Mojang, değişken uzunluklu bir bitstream'i Long dizisi olarak saklıyor
- Dizi Long ya da byte hizalı değilse, son Long 0 ile padding yapılarak tamamlanıyor
- NBT ayrıştırıcısı neredeyse tamamlanmıştı, ancak sonuna kadar bitirmenin değerli olmadığına karar verildi
- İlgili kod, proje dizini için yoğun biçimde
tmpfskullanılırken yaşanan bir sistem çökmesinde kayboldu
Bağlanılabilir bir sunucu yapmak
- Server Ping'den sonraki adım, gerçek oyun bağlantısı için gerekli handshake ve ek paketleri işlemeye geçmekti
- Bir istemcinin sunucuya girebilmesi için handshake'i tamamlayıp chunk, player position, inventory ve join game ile ilgili paketleri alması gerekiyor
- En büyük engel, Join Game paketi ile Chunk paketi içindeki veri yapılarıydı
-
Join Game
- Join Game paketi başlangıç meta verilerini gönderir
- İçeriğinde oyuncu entity ID'si, gamemode, dünya ile ilgili bilgiler ve Minecraft 1.16 civarında eklenmiş Dimension codec bulunur
- Dimension codec bir NBT Compound olduğu için uygulama yükü büyüktü
- Witchcraft bu NBT alanını vanilla sunucudan alıp kullanıyor
- Bu bölüm uygulamadaki tek ikili blob; yeniden yazmak mümkün olsa da özelleştirme ihtiyacı olmadığı düşünülmüş
-
Chunk Data And Update Light
- Chunk Data And Update Light paketi ilk bakışta büyük ve karmaşık görünse de, çeşitli BitSet alanları
0x00yapılıp Block Entity alanı gönderilmezse sadeleşiyor - Geriye X, Y, heightmaps ve Data alanları kalıyor
- heightmaps,
b000000010tekrarının daha karmaşık kodlanmış bir biçimi ve aslında neredeyse herhangi bir değer olabileceği düşünülüyor
- Chunk Data And Update Light paketi ilk bakışta büyük ve karmaşık görünse de, çeşitli BitSet alanları
Chunk Section ve palette işleme
- Data alanı, Chunk Section dizisidir
- Chunk Section, 16×16×16 bloktan oluşur ve birden fazla section üst üste konularak bir Chunk oluşturulabilir
- Uygulamayı sade tutmak için bu dizide yalnızca tek bir öğe kullanılıyor
- Chunk Section; block count, block states container ve biome container bileşenlerinden oluşuyor
- block states ve biome container, palette yapısı kullanıyor
- Sunucu, gerçek blok verisinden önce yerel block ID ile genel block ID arasındaki eşlemeyi tanımlamak zorunda
- Yapı, mümkün olduğunca çok veriyi küçük alana sığdırmak için tasarlanmış
- Blok tanımları en az 4 bit kadar küçülebiliyor
- Witchcraft, yönetim kolaylığı için en düşük 4 bit yerine blok başına 8 bit kullanıyor
- Böylece kullanılabilir palette girdisi sayısı 256 oluyor
- Gerçek chunk verisinde sadece palette girdisini gösteren hex sayılar gönderiliyor
- 4 bit palette de, hex dizgisinde bir byte iki bloğu temsil edebildiği için kullanımı kolay; ancak chunk başına 16 blokla sınırlı kalıyor
- Standart, blok başına 4 bitten 9 bite kadar değerlere izin veriyor; bunun dışındakiler 15 bitlik direct palette mapping olarak kabul ediliyor
- biome palette ayrı bir yöntemle ele alınıyor
- Boş bir palette gönderilip biome ID
0x01, yaniminecraft:plains, chunk içindeki tüm region'lara doğrudan eşleniyor - Bu yaklaşım vanilla davranışının tersine mühendislik edilmesiyle elde edildi
- Paketin bu kısmına dair mevcut belgelerin hatalı olabileceğinden şüphe ediliyor
- Boş bir palette gönderilip biome ID
Hook tabanlı eklenti yapısı ve demolar
- Sadece temel uygulamayla sıradan chunk'lar gösterilebildiğinden, sunucunun chunk göstermekten fazlasını yapabildiğini gösterecek demolara ihtiyaç vardı
- Her demo için ayrı bir kaynak ağacı oluşturmamak adına, override edilebilen fonksiyonlara hook adı verildi ve sunucunun kullanıcı kodu yüklemesine izin verildi
- Bu yapı sayesinde dünya şeklinin değiştirilmesinden
pkt_effectbağlanarak oyuncunun fareyi hareket ettirdiğinde ticking noise çıkarmasına kadar çeşitli davranışlar uygulanabiliyor - Örnek eklenti, varsayılan palette'ten rastgele bloklar seçerek chunk üretiyor
hook_chunks()içindechunk_headerçağrılıyor- 4096 blok için
RANDOM%30değeri hex olarak ekleniyor - Oluşturulan chunk
$TEMP/world/0000000000000000içine kaydediliyor - Çevredeki koordinatlar için
pkt_chunkbirkaç kez çağrılarak chunk'lar gönderiliyor
- Bir diğer demo olan digmeout, puan tabanlı basit bir oyun
- Oyuncu, rastgele yerleştirilmiş taş ve cevherler bulunan bir chunk'ın içine atılıyor
- Süre dolana kadar en değerli cevheri kazmaya çalışıyor
Witchcraft'ın kısıtları
- Bash, ondalıklı sayılar konusunda çok zayıf
- Integer değerleri bir ölçüde işleyebiliyor
- Ondalıklı sayılar, girişte çarpılıp çıkışta uygun yere nokta koyularak ele alınmak zorunda
- Bu yüzden Witchcraft'ın işlediği sayıların çoğu ya da tamamı int
- Çok oyunculu mod düzgün çalışmıyor
- Kısmen çalışıyor, ancak tamamlamak ya da cilalamak için zaman harcanmadı
- Witchcraft teknik olarak multi-threaded server
- Bunun sonucu olarak thread'ler arası iletişim için pek hoş olmayan hack'ler gerekiyor
- Global verilerin çoğu
/dev/shm/witchcraftaltında tutuluyor ve içeride$TEMPolarak anılıyor
- Performans önemli bir kısıt olmaya devam ediyor
- Özellikle birden fazla thread arasındaki veri alışverişi yavaş
- Büyük miktarda veri göndermek zor; 16 dolu chunk üretip göndermek en fazla 1 saniye sürebiliyor
- Şu anda yalnızca en güncel BusyBox 1.35.0 kuruluysa çalışıyor
- GNU coreutils ile test edilmedi ve çalışmayacağı tahmin ediliyor
1 yorum
Hacker News yorumları
Java ve Bedrock için betiklenebilir Minecraft sunucuları üzerinde epey çalışmış biri olarak, bu oldukça etkileyici
Cümle içinde “duckduckgoing” kullanmasına da artı puan
O zamanki tartışma: https://news.ycombinator.com/item?id=30347501 — 92 yorum
Negatif üs dediğin sadece 2^(-n) = 1/(2^n)
Yazarın kendi örneğinde 2^-1 = 0.5’e kadar vermesine rağmen bunu düşünemeyip sonunda awk’a devretmesi şaşırtıcıydı
Benim aptalca(?) kütüphanem ctypes.sh’yi kullanmalıydı: https://github.com/taviso/ctypes.sh
Böylece Bash’ten libm, poll(), select() gibi şeylere de erişilebilir :)
Frankenstein’ı diriltmek gibi bir his
Bash’e ayrıca kurulmuş derlenmiş C kodu gerekiyorsa, C ya da Python gibi bir şeyi şart koşsan da olur; o zaman buna “Bash’te yazıldı” demenin gerekçesi zayıflıyor. Elbette bunu saygıyla söylüyorum; bu tür “gereksiz” projeleri de seviyorum
Öte yandan bu Minecraft projesi de xxd gibi harici araçlara bağlı, dolayısıyla bu durumda ctypes.sh kullanmak daha kötü olmazdı; hatta doğru bir nokta
Bu arada dd ya da xxd olmadan da saf Bash ile ikili veriyi okuyabilir, saklayabilir, değiştirebilir, hesaplayabilir ve çıktılayabilirsiniz. Sorunlu tek bayt null; onu doğrudan saklayamasanız da okunduğunu algılayıp kaydedebilir, daha sonra çıktı verirken geri koyabilir ya da sayısal değer olarak dizi indeksi, bayt ofseti vb. için kullanabilirsiniz
İkili bir dosyayı kopyalamanın en küçük örneği
while LANG=C IFS= read -d '' -r -n 1 x ;do printf '%c' "$x" ;done bin2. Ama bu, cat olmadan cat yapmak seviyesinde olduğu için faydası pek görünmüyorDaha genel ve faydalı bir örnek https://gist.github.com/bkw777/c1413d0e3de6c54524ddae890fe8d705 adresinde
LANG=C,IFS=,-d''birleşimiyle 0x00 dışındaki tüm baytlara erişilebilir;read()dönüş değeriyle de “0x00 okundu mu”, “hiçbir şey okunamadı mı”, “girdi bitti mi” ayrımı yapılabilirTüm
while()komutuna<ve>kullanmak zorunda da değilsiniz.exec 3<>file_or_fifo_or_ttyile açıp döngü içinderead -u3,printf >&3vb. kullanabilirsinizSeri porttan okuma örneği https://gist.github.com/bkw777/ddde771cc85fdd888c7ec74953193d66 adresinde; gerçekten kullandığım kod da https://github.com/bkw777/pdd.sh içinde.
tpdd_read,tpdd_write,file_to_fhex,str_to_shexbölümlerine bakarsanız seri port ve yerel dosyaları okuyup yazarken veriyi çeşitli şekillerde işliyorBu döngüler dış süreç olmak bir yana, alt kabuk bile değil. Döngü içinde değişkenlerle oynasanız da aynı bağlamda kalıyorsunuz. Çocuk süreç yoksa üst kabuk demek de biraz tuhaf
Örnekler
read -n 1ile bayt bayt okuyor ama böyle olmak zorunda değil.-n1olmadan okursanız döngü her seferinde null’lar arasındaki mümkün olduğunca çok veriyi toplar; böylece daha az bellek ve daha az yineleme kullanabilirsinizBenim kullanımımda sayısal ofsetlere göre tek tek baytları ve bayt aralıklarını kesip değiştirmem gerekiyor; veri de bugünün ölçülerine göre küçük olduğundan onaltılık çiftlerden oluşan dizi çok kullanışlı.
a[n] == byte noluyor; yazdırılabilir karakter, yazdırılamayan karakter ve null’ın hepsini aynı şekilde ele alabiliyorsunuz. Ayrıcaprintfile saklayıp yeniden üretmenin yanında0x$nya da0x${a[n]}gibi doğrudan sayısal değer olarak da kullanabiliyorsunuzÖrneğin
h[]içindeki ikinci baytı onu izleyen payload uzunluğu olarak okuyup${h[@]:2:0x${h[1]}}ile o payload’u kesip alabilirsinizİşte gerçek hacker sitesi böyle olur. Harika
Bash programlamada iyi değilim ama aslında düşündüğümden daha güçlü ve daha az korkunç olması şaşırtıcı
Bash, vim ve Lua kadar sevdiğim araçlardan biri
2 MB’tan küçük tek bir ikili dosya, her yerde var ve çoğu kişinin sandığından çok daha fazlasını yapabiliyor
shellcheck ve iyi alışkanlıklarla Bash de okunabilir ve güvenli hale gelebilir
Daha fazlası gerekirse karmaşık FFI olmadan C/C++ ile yardımcı araçlar ekleyebilirsiniz; pip/npm gibi şeylerle nereden geldiği belli olmayan bir yığın bağımlılığı çekmeye de gerek yok
CI ve Dockerfile içinde inline Bash betikleri sık oluyor; onların hepsini ayrı betik dosyalarına taşımayı da seviyorum. Artık CI’da shellcheck olduğuna göre o kodlar da denetleniyor
Geliştiriciler “okunabilirliği düşürüyor” diye sevmiyor ama okunabilirliği düşürmek bir yana, çok daha güvenli
Sıradaki sorun, aynı Bash betiğinin birden fazla deponun aynı dosyasına kopyalanması. Dağıtık bir build yapısı; iyileştirme fikri olan varsa açığım
Öğrenciyken daha çok zaman ayırmadığıma pişmanım. Çünkü kariyerim boyunca peşimden gelen nadir araçlardan biri oldu
Ticari oyunlar için özel sunucu yapma işi hâlâ var mı merak ediyorum
Özellikle Java sürümü nispeten kendine özgü bir konumda: büyük ücretsiz içerik güncellemeleri almaya devam ederken tüm eski sürümlerin çalıştırılmasını da resmen destekliyor; modlama topluluğu da inanılmaz aktif
Minecraft (Java) bir oyun olmasının yanında, başkalarının üzerine bir şeyler inşa ettiği bir oyun motoruna yakın
Hata düzeltmek, bir dil öğrenmek ya da içeride işlerin nasıl yürüdüğünü görmek istediğiniz için yapıyorsunuz
Minecraft, Quake’ten sonra çıkan en iyi oyunlardan biri
Böyle bir şeyin gerçekten yaşanmış olması, aman tanrım
Uygulama biçimiyle ilgili çok yazı okudum ama bu ezici. Şimdiye kadar okuduğum tuhaf proje yazıları arasında açık ara en üst seviyelerden biri ve gerçekten harika bir yazı