4 puan yazan GN⁺ 2024-03-04 | 1 yorum | WhatsApp'ta paylaş
  • 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 dd ve xxd pipe'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/witchcraft tabanlı 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 -p ile 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 ncat kullanılıyor; bağlantı geldiğinde ana kabuk betiği mc.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ı; bc ise kullanılan ortamda ya da BusyBox sürümünde üslü işlemleri desteklemiyor gibi görünüyordu
    • awk, 2**-1 gibi 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 for döngüsü içinde tekrar tekrar awk çağıran yapı, dış komut çağrılarını azaltmak için awk iç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 tmpfs kullanı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ı 0x00 yapılıp Block Entity alanı gönderilmezse sadeleşiyor
    • Geriye X, Y, heightmaps ve Data alanları kalıyor
    • heightmaps, b000000010 tekrarının daha karmaşık kodlanmış bir biçimi ve aslında neredeyse herhangi bir değer olabileceği düşünülüyor

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, yani minecraft: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

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_effect bağ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çinde chunk_header çağrılıyor
    • 4096 blok için RANDOM%30 değeri hex olarak ekleniyor
    • Oluşturulan chunk $TEMP/world/0000000000000000 içine kaydediliyor
    • Çevredeki koordinatlar için pkt_chunk birkaç 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/witchcraft altında tutuluyor ve içeride $TEMP olarak 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

Referanslar

1 yorum

 
GN⁺ 2024-03-04
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ı

    • Bash zaten kayan noktalı sayıları desteklemiyor; bunun yazara nasıl yardımcı olacağını bilmiyorum
  • 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 :)

    • O kadar ileri gitmek gerekir miydi bilmiyorum ama sırf o gözü karalık bile etkileyici
      Frankenstein’ı diriltmek gibi bir his
    • Havalı ama teknik olarak Bash olmasına hiç gerek olmayan bir şeyi Bash’te yazma özünden biraz uzaklaşıyor
      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üyor
      Daha 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ılabilir
      Tüm while() komutuna < ve > kullanmak zorunda da değilsiniz. exec 3<>file_or_fifo_or_tty ile açıp döngü içinde read -u3, printf >&3 vb. kullanabilirsiniz
      Seri 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_shex bölümlerine bakarsanız seri port ve yerel dosyaları okuyup yazarken veriyi çeşitli şekillerde işliyor
      Bu 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 1 ile bayt bayt okuyor ama böyle olmak zorunda değil. -n1 olmadan 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 kullanabilirsiniz
      Benim 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 n oluyor; yazdırılabilir karakter, yazdırılamayan karakter ve null’ın hepsini aynı şekilde ele alabiliyorsunuz. Ayrıca printf ile saklayıp yeniden üretmenin yanında 0x$n ya da 0x${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
    • Vay, az önce ne okudum ben? Müthiş. Hemen kurdum. Ben de sorunun bir parçası olmak istiyorum
    • Ah harika :) Ve selam tavis
    • Bu yaklaşımla poll() ve select()’in nasıl çalıştığını görmek isterim
  • İş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

    • shellcheck ile en sevdiğim şey onu CI’a koymak. Geliştiricilerin nefret ettiği hamle!
      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
    • Bash’te iyi olmadığım için sevmiyorum. Pek iyi değil ama yine de çoğu zaman mümkün olan en iyi çözüm oluyor
      Öğ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

    • Minecraft’ta çok canlı
      Ö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
    • Sadece sunucu değil, bazı oyunlarda istemci de var: https://runelite.net/
    • Oyun oynarken teknolojiyle kurcalamayı seven insanlar için bunun hep olacağını düşünüyorum
      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
    • Bildiğim kadarıyla tersine mühendislik tarafı hâlâ aktif
  • 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ı