- Witchcraft सिर्फ Bash से Minecraft सर्वर को शुरुआत से implement करने वाला एक प्रयोगात्मक project है, जो binary protocol और Join Game से लेकर chunk transmission तक संभालता है, ताकि असली client connection संभव हो सके
- Bash की यह सीमा कि वह strings में null byte को preserve नहीं कर सकता, binary को variable में रखने के बजाय
ddऔरxxdpipes के अंदर process करके bypass की गई - Implementation की मुश्किलें Minecraft के अपने data formats जैसे VarInt/VarLong, IEEE 754 floating point, Position, और NBT पर केंद्रित थीं; खासकर Double conversion बाहरी commands call करने की लागत के कारण धीमा था
- Server को Server List Ping, handshake, Join Game, और Chunk Data And Update Light packets के क्रम में expand किया गया; Dimension codec के लिए vanilla server से लिया गया NBT binary blob इस्तेमाल हुआ
- Hook-based plugins से world generation और effects बदले जा सकते हैं, लेकिन multiplayer अधूरा है,
/dev/shm/witchcraftआधारित thread communication, धीमा data exchange, और BusyBox 1.35.0 dependency अभी भी बची हुई हैं
Bash में binary Minecraft protocol संभालना
- शुरुआती प्रयास 2009 के Classic protocol को target कर रहे थे, लेकिन पहले ही यह सीमा सामने आ गई कि Bash में binary data को ठीक से parse करना मुश्किल है
- Bash strings null byte (0x00) को ignore कर देती हैं, और null byte आया है या नहीं यह detect करने का भी कोई तरीका नहीं है, इसलिए strict binary protocol में data corrupt हो सकता है
- Workaround यह है कि binary data को Bash variables या command substitution में न डालकर pipe के अंदर ही रखा जाए
dd count=$len bs=1 status=none | xxd -pसे जरूरी byte count पढ़कर उसे hex string में convert किया जाता है- Hex string पर pattern matching, substitution, और data extraction किया जाता है
- Response भेजने से पहले
xxdके reverse option से उसे फिर binary में बदला जाता है
- Minecraft के default TCP port पर connections accept करने के लिए
ncatका उपयोग किया जाता है, और connection आने पर main shell scriptmc.shचलती है
Protocol data types का implementation
- सबसे पहले implement करने के लिए अच्छा target Server List Ping packet है
- यह mandatory packet नहीं है, इसलिए server सही response न दे तब भी game connection खुद संभव रहता है
- हालांकि data types जैसे core protocol concepts सीखना आसान हो जाता है
-
VarInt और VarLong
- Minecraft के VarInt/VarLong LEB128 variant हैं, जो MQTT का अनुभव रखने वालों को परिचित लग सकते हैं
- LEB128 एक compression method है जो byte को 1 signal bit और 7 data bits में बांटकर integer length store करता है
- अगर पहला bit 0 है तो वह byte आखिरी है, और 1 है तो अगला byte जारी रहता है
- Bash में reference implementation को ज्यों का त्यों port करना मुश्किल था, इसलिए modulo और division का उपयोग करते हुए custom encoder लिखा गया
- Decoder को AND operation और multiplication का उपयोग करके reference method जैसा implement किया गया
- LEB128 खुद से ज्यादा परेशानी इस बात से हुई कि यह normal int, long, signed short के साथ मिलकर protocol में जगह-जगह आता है
-
IEEE 754 floating point
- IEEE 754 Double conversion implementation के सबसे झंझट वाले हिस्सों में से एक था
- Basic implementation में negative powers लगाने वाला loop चाहिए, लेकिन Bash negative exponentiation को natively support नहीं करता
- Perl से बचा गया, और
bcusage environment या BusyBox version में exponentiation support करता नहीं दिखा awk2**-1जैसी negative powers handle कर सकता है, इसलिए conversion implementation में इसका उपयोग किया गया- शुरुआती implementation इतना धीमा था कि Player Move packets में client द्वारा भेजे गए लगभग 50~100 packets और हर packet के 3 X·Y·Z Double values process करने में कई मिनट लग जाते थे
- Bash
forloop के अंदर बार-बारawkcall करने वाली structure कोawkके internal loop में ले जाकर external command calls की संख्या घटाई गई - इसके बाद conversion Xeon E5-2680v2 पर लगभग 10ms तक कम हो गया
- पुराने version का लगभग 350ms वाला आंकड़ा पक्का measurement नहीं है
-
Position data type
- Position Mojang द्वारा बनाया गया 64-bit Long आधारित data type है
- X upper 26 bits में, Z middle 26 bits में, और Y lower 12 bits में store होता है
- Bash में जरूरी bitshift operators मौजूद हैं, इसलिए implementation आसान था
- हालांकि यह type ज्यादा इस्तेमाल नहीं होता, और कई packets X·Y·Z coordinates को अलग-अलग Double values के रूप में store करते हैं
- नतीजतन position data प्रति packet 64 bits से 192 bits हो जाता है
- अगर मान लें कि default world border 30,000,000 तक के numbers ही चाहिए, तो 9-digit floating point precision मिलती है
- सामान्य server communication zlib का उपयोग करता है, और block के अंदर position representation के लिए practically 2~3 decimal places से ज्यादा की जरूरत नहीं लगती
-
NBT
- NBT Mojang का internal format है, और binary data के लिए JSON जैसा format है
- JSON की तरह इसे spec से बाहर arbitrary data store करने के लिए भी इस्तेमाल किया जाता है
- उदाहरण के लिए Mojang variable-length bitstream को Long array के रूप में store करता है
- अगर array Long या byte aligned नहीं है, तो आखिरी Long को 0 से padding किया जाता है
- NBT parser लगभग पूरा कर लिया गया था, लेकिन अंत तक पूरा करना worth नहीं लगा
- Project directory के लिए
tmpfsका बहुत इस्तेमाल करने के दौरान system crash से वह code खो गया
Connect हो सकने वाला server बनाना
- Server Ping के बाद अगला step असली game connection के लिए जरूरी handshake और additional packet handling था
- Client को server में enter करने के लिए handshake पूरा करना और chunk, player position, inventory, join game related packets प्राप्त करना जरूरी है
- सबसे बड़ी बाधा Join Game packet और Chunk packet के अंदर के data structures थे
-
Join Game
- Join Game packet initial metadata transmit करता है
- इसमें player entity ID, gamemode, world related info, और Minecraft 1.16 के आसपास से शामिल हुआ Dimension codec होता है
- Dimension codec NBT Compound है, इसलिए implementation burden बड़ा था
- Witchcraft इस NBT field को vanilla server से लेकर इस्तेमाल करता है
- यह implementation में अकेला binary blob है, और reimplementation संभव है लेकिन customization की जरूरत नहीं मानी गई
-
Chunk Data And Update Light
- Chunk Data And Update Light packet शुरू में बड़ा और जटिल दिखता है, लेकिन कई BitSet fields को
0x00पर रखकर और Block Entity field न भेजकर यह सरल हो जाता है - बचे हुए fields X, Y, heightmaps, और Data field हैं
- Heightmaps
b000000010repetition को ज्यादा complex तरीके से encode किया हुआ रूप हैं, और practically कोई भी value हो सकती है ऐसा माना गया
- Chunk Data And Update Light packet शुरू में बड़ा और जटिल दिखता है, लेकिन कई BitSet fields को
Chunk Section और palette handling
- Data field Chunk Section array है
- Chunk Section 16×16×16 blocks का होता है, और कई sections को stack करके एक Chunk बनाया जा सकता है
- Implementation को सरल रखने के लिए इस array में सिर्फ single element इस्तेमाल किया गया
- Chunk Section block count, block states container, और biome container से बना होता है
- Block states और biome container palette structure का उपयोग करते हैं
- असली block data से पहले server को local block ID और global block ID की mapping define करनी होती है
- यह structure अधिक से अधिक data को छोटी space में भरने के लिए है
- Block definition कम से कम 4 bits तक छोटी हो सकती है
- Witchcraft manage करना आसान रहे इसलिए minimum 4 bits के बजाय 8 bits per block का उपयोग करता है
- Available palette entries 256 हो जाती हैं
- असली chunk data में palette entry को point करने वाला hex number भेजना ही काफी है
- 4-bit palette भी handle करना आसान है, क्योंकि hex string में एक byte दो blocks represent कर सकता है, लेकिन यह प्रति chunk 16 blocks तक सीमित हो जाता है
- Standard 4 bits per block से 9 bits per block तक allow करता है, और उसके अलावा सबको 15 bits per block direct palette mapping माना जाता है
- Biome palette को अलग तरीके से handle किया जाता है
- Empty palette भेजकर biome ID
0x01, यानीminecraft:plains, को chunk के अंदर सभी regions पर directly map किया जाता है - यह vanilla behavior को reverse-engineer करने के result पर आधारित है
- संदेह है कि packet के इस हिस्से की मौजूदा documentation inaccurate हो सकती है
- Empty palette भेजकर biome ID
Hook-based plugin structure और demo
- Basic implementation से सिर्फ साधारण chunk ही दिखता है, इसलिए यह दिखाने के लिए demo चाहिए था कि server chunk display से आगे भी काम कर सकता है
- हर demo के लिए अलग source tree न बनाने के लिए overridable functions को hooks कहा गया, और server को user code load करने योग्य बनाया गया
- इस structure से world shape बदलने से लेकर
pkt_effectजोड़कर mouse हिलाने पर player से ticking noise निकलवाने जैसे behavior तक implement किए जा सकते हैं - Example plugin default palette से random blocks चुनकर chunk generate करता है
hook_chunks()मेंchunk_headercall करता है- 4096 blocks के लिए
RANDOM%30values को hex में append करता है - Generated chunk को
$TEMP/world/0000000000000000में save करता है - आस-पास के coordinates पर
pkt_chunkकई बार call करके chunk transmit करता है
- एक और demo digmeout score-based simple game है
- Player को randomly placed stones और ores वाले chunk में फेंकता है
- Time limit खत्म होने तक सबसे valuable ores mine करने का तरीका है
Witchcraft की सीमाएँ
- Bash decimal fractions संभालने में बहुत कमजोर है
- Integers को कुछ हद तक handle किया जा सकता है
- Decimal fractions को input में multiply करके और output में सही जगह point डालकर handle करना पड़ता है
- इसी वजह से Witchcraft जो ज्यादातर या सभी numbers handle करता है वे int हैं
- Multiplayer ठीक से काम नहीं करता
- कुछ हद तक चलता है, लेकिन इसे complete या polish करने में समय नहीं लगाया गया
- Witchcraft technically multi-threaded server है
- इसके कारण threads के बीच communication के लिए खराब hack चाहिए
- ज्यादातर global data
/dev/shm/witchcraftके नीचे store होता है, और internally$TEMPके रूप में reference किया जाता है
- Performance बड़ी constraint बनी हुई है
- खासकर कई threads के बीच data exchange धीमा है
- बड़ी मात्रा में data भेजना मुश्किल है, और 16 solid chunks generate करके भेजने में 1 second तक लग सकता है
- फिलहाल यह सिर्फ तब चलता है जब latest BusyBox 1.35.0 installed हो
- GNU coreutils के साथ test नहीं किया गया है, और इसके काम न करने की उम्मीद है
1 टिप्पणियां
Hacker News की राय
Java और Bedrock के लिए scriptable Minecraft servers पर काफी काम कर चुका हूँ, और उस नजरिए से यह काफी प्रभावशाली है
वाक्य में “duckduckgoing” लिखने के लिए भी extra points
उस समय की चर्चा: https://news.ycombinator.com/item?id=30347501 — 92 comments
ऋणात्मक exponent बस 2^(-n) = 1/(2^n) होता है
हैरानी हुई कि लेखक ने अपने उदाहरण में 2^-1 = 0.5 तक दिया, फिर भी यह बात नहीं सूझी और आखिरकार उसे awk को सौंप दिया
मेरी मूर्खतापूर्ण(?) library ctypes.sh इस्तेमाल करनी चाहिए थी: https://github.com/taviso/ctypes.sh
तब Bash से libm, poll(), select() जैसी चीजों तक भी access मिल जाता :)
Frankenstein को फिर से जिंदा करने जैसा लगता है
अगर Bash में अलग से install किया हुआ compiled C code भी चाहिए, तो फिर सीधे C या Python जैसी चीज मांग लेने में ही क्या जाता है; ऐसे में इसे “Bash में लिखा” कहना थोड़ा कमजोर पड़ता है। बेशक यह सम्मान के साथ कह रहा हूँ, और ऐसे “बेकार” projects मुझे पसंद हैं
हालांकि यह Minecraft project भी xxd जैसे external tools पर निर्भर है, इसलिए इस मामले में ctypes.sh इस्तेमाल करना और बुरा नहीं होता, बल्कि बात सही ही है
वैसे dd या xxd के बिना भी pure Bash में binary पढ़ी, store, manipulate, calculate और output की जा सकती है। दिक्कत वाला byte सिर्फ null है; उसे सीधे store नहीं कर सकते, लेकिन यह detect करके record कर सकते हैं कि उसे पढ़ा गया था, और बाद में output देते समय restore कर सकते हैं या numeric value के रूप में array index, byte offset आदि में इस्तेमाल कर सकते हैं
binary file copy करने का छोटा-सा example है
while LANG=C IFS= read -d '' -r -n 1 x ;do printf '%c' "$x" ;done bin2। हालांकि यह cat के बिना cat करने जैसा है, इसलिए usefulness बहुत साफ नहीं दिखतीथोड़ा ज्यादा generic और उपयोगी example यहां है: https://gist.github.com/bkw777/c1413d0e3de6c54524ddae890fe8d705
LANG=C,IFS=,-d''को मिलाने पर 0x00 को छोड़कर हर byte access हो सकता है, औरread()की return value से “0x00 पढ़ा गया या नहीं”, “कुछ भी नहीं पढ़ा गया”, और “input खत्म हुआ या नहीं” में फर्क किया जा सकता हैपूरे
while()command में<और>लगाने की जरूरत भी नहीं है।exec 3<>file_or_fifo_or_ttyसे open करें और loop के अंदरread -u3,printf >&3आदि इस्तेमाल करेंserial port से पढ़ने का example https://gist.github.com/bkw777/ddde771cc85fdd888c7ec74953193d66 पर है, और असल में इस्तेमाल किया code https://github.com/bkw777/pdd.sh पर है।
tpdd_read,tpdd_write,file_to_fhex,str_to_shexदेखें—ये serial port और local file को read/write करते हुए data को कई तरीकों से process करते हैंये loops external process तो दूर, subshell भी नहीं हैं। loop के अंदर variables modify करने पर भी वही original context रहता है। child नहीं है, इसलिए इसे parent shell कहना भी थोड़ा अजीब है
examples
read -n 1से एक-एक byte पढ़ते हैं, लेकिन ऐसा करना जरूरी नहीं है।-n1के बिना पढ़ें तो loop की हर iteration में nulls के बीच का जितना हो सके उतना data इकट्ठा होता है, इसलिए memory और iterations कम लगती हैंमेरे use case में numeric offsets के आधार पर individual bytes और byte ranges काटने और manipulate करने पड़ते हैं, और data भी आज के हिसाब से छोटा है, इसलिए hex-pair array बहुत सुविधाजनक है।
a[n] == byte nहो जाता है, printable characters, non-printable characters और nulls सभी को एक जैसा handle किया जा सकता है, औरprintfसे store/regen करने के अलावा0x$nया0x${a[n]}की तरह numeric value के रूप में भी सीधे इस्तेमाल किया जा सकता हैउदाहरण के लिए
h[]का दूसरा byte उसके बाद आने वाले payload length के रूप में पढ़कर,${h[@]:2:0x${h[1]}}से वह payload काटा जा सकता हैयही तो असली hacker site है। शानदार
Bash programming में बहुत अच्छा नहीं हूँ, लेकिन हैरानी होती है कि असल में यह सोच से ज्यादा powerful और कम भयानक है
Bash मेरे पसंदीदा tools में से एक है, vim और Lua जितना
यह 2MB से भी छोटा single binary है, हर जगह मौजूद है, और ज्यादातर लोग जितना सोचते हैं उससे कहीं ज्यादा काम कर सकता है
shellcheck और अच्छी habits इस्तेमाल करें तो Bash भी readable और safe बन सकता है
अगर और कुछ चाहिए हो तो complicated FFI के बिना C/C++ में utilities जोड़ सकते हैं, और pip/npm जैसी चीजों से जाने-अनजाने dependencies का ढेर खींचने की जरूरत नहीं पड़ती
CI और Dockerfile के अंदर अक्सर inline Bash scripts होते हैं, और मुझे उन सबको अलग script files में ले जाना भी पसंद है। अब CI में shellcheck है, तो वह code भी check होगा
developers कहते हैं “readability घटती है”, लेकिन readability घटने के बजाय यह कहीं ज्यादा safe हो जाता है
अगली समस्या यह है कि वही Bash script कई repositories में उसी file में duplicate हो जाती है। distributed build structure है; सुधार के ideas हों तो मैं open हूँ
अफसोस है कि student रहते इस पर ज्यादा समय नहीं लगाया। क्योंकि यह उन गिने-चुने tools में से एक निकला जो मेरे पूरे career में साथ चलता रहा
सोचता हूँ कि commercial games के लिए custom servers बनाने का काम अभी भी होता है क्या
खासकर Java version एक relatively अनोखी जगह पर है: बड़े free content updates लगातार मिलते रहते हैं, फिर भी सभी पुराने versions चलाने को officially support करता है, और modding community भी जबरदस्त active है
Minecraft(Java) एक game होने के साथ-साथ लगभग ऐसा game engine भी है जिस पर दूसरे लोग अपनी चीजें build करते हैं
bug fix करना हो, कोई language सीखनी हो, या अंदरूनी कामकाज कैसे चलता है यह देखना हो—लोग यह करते हैं
Minecraft, Quake के बाद आए सबसे अच्छे games में से एक है
यकीन नहीं होता कि यह सच में हुआ
implementation approach पर कई लेख पढ़े हैं, लेकिन यह सबसे ऊपर है। अब तक पढ़े अजीब projects वाले writeups में यह निश्चित रूप से top tier है, और सच में शानदार लेख है