- Witchcraft เป็นโปรเจกต์ทดลองที่สร้างเซิร์ฟเวอร์ Minecraft ขึ้นใหม่ตั้งแต่ต้นด้วย Bash เพียงอย่างเดียว โดยจัดการตั้งแต่โปรโตคอลไบนารี, Join Game, ไปจนถึงการส่ง chunk เพื่อให้ไคลเอนต์จริงเชื่อมต่อได้
- ข้อจำกัดที่ Bash ไม่สามารถเก็บ null byte ไว้ในสตริงได้ ถูกเลี่ยงด้วยวิธีไม่เก็บไบนารีลงในตัวแปร แต่ประมวลผลภายใน pipe ของ
ddและxxdแทน - ความยากหลักของการพัฒนาอยู่ที่ชนิดข้อมูลเฉพาะของ Minecraft เช่น VarInt/VarLong, IEEE 754 floating-point, Position และ NBT โดยเฉพาะการแปลง Double ที่ช้ามากเพราะต้นทุนการเรียกคำสั่งภายนอก
- เซิร์ฟเวอร์ค่อย ๆ ขยายจาก Server List Ping ไปสู่ handshake, Join Game และแพ็กเก็ต Chunk Data And Update Light โดยใช้ binary blob ของ NBT สำหรับ Dimension codec ที่ดึงมาจาก vanilla server
- แม้จะมีปลั๊กอินแบบ hook สำหรับเปลี่ยนการสร้างโลกและเอฟเฟ็กต์ได้ แต่ก็ยังมีข้อจำกัดทั้ง multiplayer ที่ยังไม่สมบูรณ์, การสื่อสารระหว่างเธรดผ่าน
/dev/shm/witchcraft, การแลกเปลี่ยนข้อมูลที่ช้า และการพึ่งพา BusyBox 1.35.0
การจัดการโปรโตคอล Minecraft แบบไบนารีใน Bash
- ความพยายามแรกเริ่มมุ่งไปที่ Classic protocol ในปี 2009 แต่ก็พบข้อจำกัดก่อนเลยว่า Bash จัดการการ parse ข้อมูลไบนารีได้ยากมาก
- สตริงของ Bash จะ มองข้าม null byte (0x00) และไม่มีวิธีตรวจสอบได้ด้วยว่ามี null byte เกิดขึ้นหรือไม่ จึงอาจทำให้ข้อมูลเสียหายในโปรโตคอลไบนารีที่เข้มงวด
- วิธีหลบเลี่ยงคือไม่ใส่ข้อมูลไบนารีลงในตัวแปรของ Bash หรือ command substitution แต่คงมันไว้ใน pipe
- ใช้
dd count=$len bs=1 status=none | xxd -pเพื่ออ่านจำนวนไบต์ที่ต้องการแล้วแปลงเป็นสตริง hex - ทำ pattern matching, การแทนที่ และการดึงข้อมูลบนสตริง hex
- ตอนส่งคำตอบก็แปลงกลับเป็นไบนารีด้วย reverse option ของ
xxd
- ใช้
- เพื่อรับการเชื่อมต่อบนพอร์ต TCP มาตรฐานของ Minecraft จึงใช้
ncatและเมื่อมีการเชื่อมต่อเข้ามาก็รันสคริปต์เชลล์หลักmc.sh
การพัฒนาชนิดข้อมูลของโปรโตคอล
- สิ่งแรกที่เหมาะจะทำคือแพ็กเก็ต Server List Ping
- มันไม่ใช่แพ็กเก็ตบังคับ ดังนั้นต่อให้เซิร์ฟเวอร์ตอบไม่สมบูรณ์ ตัวเกมก็ยังเข้าเซิร์ฟเวอร์ได้
- แต่ช่วยให้ทำความเข้าใจแนวคิดหลักของโปรโตคอล เช่น data types ได้ง่าย
-
VarInt และ VarLong
- VarInt/VarLong ของ Minecraft เป็น LEB128 แบบดัดแปลง ซึ่งคนที่เคยใช้ MQTT อาจคุ้นเคย
- LEB128 เป็นวิธีบีบอัดความยาวจำนวนเต็มโดยแบ่งไบต์ออกเป็นบิตสัญญาณ 1 บิตและบิตข้อมูล 7 บิต
- ถ้าบิตแรกเป็น 0 แปลว่าไบต์นั้นเป็นตัวสุดท้าย ถ้าเป็น 1 แปลว่ายังมีไบต์ถัดไป
- เนื่องจากย้าย reference implementation มาใช้ใน Bash ตรง ๆ ได้ยาก จึงเขียน encoder เองโดยใช้ modulo และ division
- ส่วน decoder ก็ทำคล้าย reference โดยใช้การคำนวณ AND และการคูณ
- สิ่งที่ยุ่งยากกว่าตัว LEB128 เองคือมันถูกใช้ปะปนกับ int, long และ signed short ทั่วทั้งโปรโตคอล
-
IEEE 754 floating-point
- การแปลง IEEE 754 Double เป็นส่วนที่น่าปวดหัวที่สุดอย่างหนึ่งของการพัฒนา
- แนวทางพื้นฐานต้องใช้ลูปที่มีการยกกำลังติดลบ แต่ Bash ไม่รองรับการยกกำลังติดลบโดยตรง
- ผู้พัฒนาเลี่ยง Perl ส่วน
bcก็ดูเหมือนจะไม่รองรับการยกกำลังในสภาพแวดล้อมที่ใช้งานหรือใน BusyBox บางเวอร์ชัน - จึงใช้
awkแทน เพราะจัดการกับการยกกำลังติดลบอย่าง2**-1ได้ - เวอร์ชันแรกช้ามาก ถึงขั้นใช้เวลาหลายนาทีในการประมวลผลแพ็กเก็ต Player Move ที่ไคลเอนต์ส่งมาประมาณ 50~100 แพ็กเก็ต และแต่ละแพ็กเก็ตมีค่า Double ของ X·Y·Z อย่างละ 3 ค่า
- จากนั้นจึงย้ายโครงสร้างที่เดิมเรียก
awkซ้ำ ๆ ในลูปforของ Bash ไปไว้เป็นลูปภายในawkเพื่อลดจำนวนการเรียกคำสั่งภายนอก - หลังปรับแล้ว เวลาการแปลงลดลงมาเหลือประมาณ 10ms บน Xeon E5-2680v2
- ส่วนตัวเลขประมาณ 350ms ของเวอร์ชันก่อนหน้าไม่ใช่ค่าที่วัดอย่างแม่นยำ
-
ชนิดข้อมูล Position
- Position เป็นชนิดข้อมูลที่ Mojang ออกแบบบนพื้นฐานของ Long 64-bit
- X ใช้ 26 บิตบนสุด, Z ใช้ 26 บิตตรงกลาง และ Y ใช้ 12 บิตล่างสุด
- Bash มีตัวดำเนินการ bitshift ที่ต้องใช้ จึงพัฒนาได้ไม่ยาก
- แต่ชนิดข้อมูลนี้ไม่ได้ถูกใช้งานบ่อย และหลายแพ็กเก็ตก็เก็บพิกัด X·Y·Z เป็นค่า Double แยกกัน
- ผลคือข้อมูลตำแหน่งขยายจาก 64 บิตต่อแพ็กเก็ตเป็น 192 บิต
- หากสมมติว่าต้องรองรับเพียงค่าตัวเลขถึง world border มาตรฐานที่ 30,000,000 ก็จะได้ความแม่นยำ floating-point ระดับ 9 หลัก
- การสื่อสารของเซิร์ฟเวอร์ทั่วไปใช้ zlib และสำหรับการแสดงตำแหน่งภายในบล็อกก็ดูไม่จำเป็นต้องมีทศนิยมเกิน 2~3 ตำแหน่งในทางปฏิบัติ
-
NBT
- NBT เป็นฟอร์แมตภายในของ Mojang คล้าย JSON สำหรับข้อมูลไบนารี
- เหมือน JSON ตรงที่มันถูกใช้เก็บข้อมูลตามใจนอกเหนือจากสเปกด้วย
- ตัวอย่างเช่น Mojang ใช้เก็บ bitstream แบบความยาวแปรผันลงในอาร์เรย์ของ Long
- ถ้าอาร์เรย์ไม่พอดีกับการจัดแนวแบบ Long หรือ byte ค่า Long ตัวสุดท้ายจะถูก padding ด้วย 0
- ผู้พัฒนาเคยทำ NBT parser ได้เกือบเสร็จ แต่สุดท้ายมองว่าไม่คุ้มจะทำต่อให้สมบูรณ์
- โค้ดส่วนนั้นสูญหายไปจาก system crash ระหว่างที่ใช้
tmpfsกับไดเรกทอรีโปรเจกต์อย่างหนัก
สร้างเซิร์ฟเวอร์ที่เชื่อมต่อได้จริง
- หลังจาก Server Ping ขั้นต่อไปคือจัดการ handshake และแพ็กเก็ตเพิ่มเติมที่จำเป็นต่อการเข้าเกมจริง
- ถ้าจะให้ไคลเอนต์เข้าเซิร์ฟเวอร์ได้ จำเป็นต้องผ่าน handshake และรับแพ็กเก็ตที่เกี่ยวกับ chunk, player position, inventory และ join game
- อุปสรรคที่ใหญ่ที่สุดคือโครงสร้างข้อมูลภายในของแพ็กเก็ต Join Game และแพ็กเก็ต Chunk
-
Join Game
- แพ็กเก็ต Join Game ใช้ส่ง metadata เริ่มต้น
- ข้อมูลที่รวมอยู่มี entity ID ของผู้เล่น, gamemode, ข้อมูลเกี่ยวกับโลก และ Dimension codec ที่มีเข้ามาตั้งแต่ช่วง Minecraft 1.16
- Dimension codec เป็น NBT Compound จึงมีภาระการพัฒนาสูง
- Witchcraft เลือกใช้ฟิลด์ NBT นี้โดยดึงมาจาก vanilla server
- นี่เป็น binary blob เพียงส่วนเดียวในงานพัฒนา และแม้จะพัฒนาใหม่เองได้ แต่ก็มองว่าไม่จำเป็นต้องปรับแต่ง
-
Chunk Data And Update Light
- แพ็กเก็ต Chunk Data And Update Light ตอนแรกดูใหญ่และซับซ้อน แต่จะง่ายขึ้นถ้าตั้งฟิลด์ BitSet หลายตัวเป็น
0x00และไม่ส่งฟิลด์ Block Entity - ฟิลด์ที่เหลือจึงมี X, Y, heightmaps และฟิลด์ Data
- heightmaps เป็นรูปแบบการเข้ารหัสที่ซับซ้อนขึ้นจากการทำซ้ำ
b000000010และในทางปฏิบัติดูเหมือนว่าจะเป็นค่าอะไรก็ได้
- แพ็กเก็ต Chunk Data And Update Light ตอนแรกดูใหญ่และซับซ้อน แต่จะง่ายขึ้นถ้าตั้งฟิลด์ BitSet หลายตัวเป็น
การจัดการ Chunk Section และ palette
- ฟิลด์ Data คืออาร์เรย์ของ Chunk Section
- Chunk Section มีขนาด 16×16×16 บล็อก และสามารถซ้อนหลาย section เพื่อสร้าง Chunk หนึ่งก้อนได้
- เพื่อให้พัฒนาได้ง่าย อาร์เรย์นี้จึงใช้เพียง element เดียว
- Chunk Section ประกอบด้วย block count, block states container และ biome container
- ทั้ง block states และ biome container ใช้โครงสร้างแบบ palette
- ก่อนถึงข้อมูลบล็อกจริง เซิร์ฟเวอร์ต้องนิยาม mapping ระหว่าง local block ID กับ global block ID เสียก่อน
- จุดประสงค์คือบรรจุข้อมูลให้ได้มากที่สุดในพื้นที่ที่เล็กที่สุด
- ข้อมูลบล็อกสามารถย่อให้เล็กสุดได้ถึง 4 บิต
- เพื่อให้ง่ายต่อการจัดการ Witchcraft ใช้ 8 bits per block แทนขั้นต่ำ 4 บิต
- ทำให้มี palette entry ใช้งานได้ 256 ค่า
- ข้อมูล chunk จริงจึงส่งเป็นตัวเลข hex ที่อ้างถึง palette entry ได้เลย
- แม้ palette 4 บิตจะยังจัดการได้ง่ายเพราะหนึ่งไบต์ในสตริง hex แทนได้สองบล็อก แต่ก็จะจำกัดเหลือ 16 บล็อกต่อ chunk
- มาตรฐานอนุญาตตั้งแต่ 4 bits per block ถึง 9 bits per block และนอกเหนือจากนั้นจะถือเป็น direct palette mapping แบบ 15 bits per block
- ส่วน biome palette ถูกจัดการอีกแบบหนึ่ง
- โดยส่ง palette ว่าง แล้ว map biome ID
0x01หรือminecraft:plainsแบบตรงไปยังทุก region ภายใน chunk - สิ่งนี้อ้างอิงจากการ reverse engineer พฤติกรรมของ vanilla
- ผู้พัฒนาสงสัยว่าเอกสารเดิมของส่วนนี้อาจไม่ถูกต้อง
- โดยส่ง palette ว่าง แล้ว map biome ID
โครงสร้างปลั๊กอินแบบ hook และเดโม
- เพราะการพัฒนาพื้นฐานอย่างเดียวแสดงได้เพียง chunk ธรรมดา จึงจำเป็นต้องมีเดโมที่แสดงให้เห็นว่าเซิร์ฟเวอร์ทำอะไรได้มากกว่าการแสดง chunk
- เพื่อไม่ต้องแยก source tree ต่อเดโม จึงตั้งชื่อฟังก์ชันที่ override ได้เหล่านี้ว่า hooks และเปิดให้เซิร์ฟเวอร์โหลดโค้ดของผู้ใช้ได้
- ด้วยโครงสร้างนี้สามารถทำได้ตั้งแต่เปลี่ยนรูปร่างของโลก ไปจนถึงต่อ
pkt_effectเพื่อให้ผู้เล่นส่งเสียง ticking noise เมื่อขยับเมาส์ - ตัวอย่างปลั๊กอินหนึ่งจะสุ่มเลือกบล็อกจาก palette พื้นฐานเพื่อสร้าง chunk
- เรียก
hook_chunks()จากchunk_header - สำหรับบล็อกทั้ง 4096 ก้อน จะเติมค่า
RANDOM%30เป็นเลข hex ต่อท้าย - บันทึก chunk ที่สร้างไว้ที่
$TEMP/world/0000000000000000 - แล้วเรียก
pkt_chunkหลายครั้งเพื่อส่ง chunk ไปยังพิกัดรอบข้าง
- เรียก
- อีกเดโมหนึ่งชื่อ digmeout เป็นเกมง่าย ๆ แบบมีคะแนน
- มันโยนผู้เล่นลงใน chunk ที่สุ่มวางหินและแร่ไว้
- ผู้เล่นต้องขุดแร่ที่มีค่ามากที่สุดก่อนเวลาจำกัดจะหมด
ข้อจำกัดของ Witchcraft
- Bash อ่อนแอมากในการจัดการ เลขทศนิยม
- จำนวนเต็มยังพอจัดการได้ระดับหนึ่ง
- แต่เลขทศนิยมต้องใช้วิธีคูณตอนรับเข้า แล้วค่อยใส่จุดทศนิยมกลับในตำแหน่งที่เหมาะสมตอนส่งออก
- เพราะเหตุนี้ ตัวเลขส่วนใหญ่หรืออาจทั้งหมดที่ Witchcraft จัดการจึงเป็น int
- multiplayer ยังทำงานได้ไม่ดี
- พอใช้งานได้บ้าง แต่ไม่ได้ใช้เวลาไปกับการทำให้เสร็จสมบูรณ์หรือขัดเกลา
- ในทางเทคนิค Witchcraft เป็น multi-threaded server
- ผลคือจำเป็นต้องมี hack ที่ไม่ค่อยดีนักเพื่อให้สื่อสารกันระหว่างเธรดได้
- ข้อมูล global ส่วนใหญ่ถูกเก็บไว้ใต้
/dev/shm/witchcraftและอ้างถึงภายในด้วย$TEMP
- ประสิทธิภาพยังคงเป็นข้อจำกัดใหญ่
- โดยเฉพาะการแลกเปลี่ยนข้อมูลระหว่างหลายเธรดที่ช้า
- การส่งข้อมูลจำนวนมากทำได้ยาก และการสร้างกับส่ง solid chunk 16 ชุดอาจใช้เวลาสูงสุดถึง 1 วินาที
- ปัจจุบันรันได้เฉพาะเมื่อมี BusyBox 1.35.0 รุ่นล่าสุดติดตั้งอยู่เท่านั้น
- ยังไม่ได้ทดสอบกับ GNU coreutils และคาดว่าอาจไม่ทำงาน
1 ความคิดเห็น
ความเห็นจาก Hacker News
ในฐานะคนที่เคยทำงานกับ Minecraft server แบบสคริปต์ได้ทั้งสำหรับ Java และ Bedrock มาเยอะ อันนี้ถือว่า น่าประทับใจพอสมควร
แถมยังได้คะแนนเพิ่มจากการใช้คำว่า “duckduckgoing” ในประโยคด้วย
กระทู้ตอนนั้น: https://news.ycombinator.com/item?id=30347501 — 92 คอมเมนต์
เลขชี้กำลังติดลบ ก็แค่ 2^(-n) = 1/(2^n) เท่านั้นเอง
ผู้เขียนยกตัวอย่างเองถึง 2^-1 = 0.5 แล้วแท้ ๆ แต่กลับนึกเรื่องนี้ไม่ออกและสุดท้ายโยนไปให้ awk จัดการ น่าแปลกดี
น่าจะใช้ไลบรารีงี่เง่าของผม(?) อย่าง ctypes.sh มากกว่า: https://github.com/taviso/ctypes.sh
แบบนั้นก็เข้าถึง libm, poll(), select() และอื่น ๆ จาก Bash ได้ :)
ให้ความรู้สึกเหมือนปลุก Frankenstein ขึ้นมาใหม่
ถ้า Bash ยังต้องพึ่ง C code ที่คอมไพล์แยกมาติดตั้งเพิ่มอยู่ดี ก็ไม่ค่อยมีเหตุผลจะเรียกมันว่า “เขียนด้วย Bash” เท่าไรแล้ว จะบอกว่าต้องใช้ C หรือ Python ไปเลยก็คงได้ ทั้งหมดนี้พูดด้วยความเคารพนะ และผมก็ชอบโปรเจ็กต์ “ไร้สาระ” แบบนี้ด้วย
แต่อีกด้านหนึ่ง โปรเจ็กต์ Minecraft นี้ก็พึ่งเครื่องมือภายนอกอย่าง xxd อยู่แล้ว ดังนั้นกรณีนี้จะใช้ ctypes.sh ก็ไม่ได้แย่กว่าอะไร แถมอาจจะพูดได้ถูกด้วย
อนึ่ง คุณสามารถอ่าน, เก็บ, จัดการ, คำนวณ และแสดงผล ข้อมูลไบนารีด้วย Bash ล้วน ๆ ได้โดยไม่ต้องใช้ dd หรือ xxd ปัญหามีแค่ไบต์ nul เท่านั้น ซึ่งถึงจะเก็บตรง ๆ ไม่ได้ แต่ตรวจได้ว่าอ่านเจอมัน และบันทึกไว้เพื่อกู้คืนตอนพิมพ์ออกมาทีหลัง หรือใช้เป็นค่าตัวเลขสำหรับ index ของ array หรือ byte offset ก็ได้
ตัวอย่างขั้นต่ำสำหรับคัดลอกไฟล์ไบนารีคือ
while LANG=C IFS= read -d '' -r -n 1 x ;do printf '%c' "$x" ;done bin2แม้ว่านี่จะออกแนวทำ cat โดยไม่ใช้ cat เลยยังไม่ค่อยเห็นประโยชน์นักตัวอย่างที่ทั่วไปและมีประโยชน์กว่านั้นอยู่ที่ https://gist.github.com/bkw777/c1413d0e3de6c54524ddae890fe8d705
การใช้
LANG=C,IFS=,-d''ร่วมกันจะทำให้เข้าถึงทุกไบต์ได้ยกเว้น 0x00 และใช้ค่าที่read()คืนกลับมาเพื่อแยกได้ว่า “อ่าน 0x00 เจอ”, “ไม่ได้อ่านอะไรเลย” หรือ “อินพุตจบแล้ว”ไม่จำเป็นต้องใช้
<กับ>ทั้งคำสั่งwhile()ด้วยซ้ำ เปิดด้วยexec 3<>file_or_fifo_or_ttyแล้วใช้read -u3,printf >&3ฯลฯ ภายในลูปแทนได้ตัวอย่างการอ่านจาก serial port อยู่ที่ https://gist.github.com/bkw777/ddde771cc85fdd888c7ec74953193d66 และโค้ดที่ใช้งานจริงอยู่ที่ https://github.com/bkw777/pdd.sh ดู
tpdd_read,tpdd_write,file_to_fhex,str_to_shexจะเห็นการอ่านเขียนทั้ง serial port และไฟล์ในเครื่องพร้อมจัดการข้อมูลได้หลายแบบลูปพวกนี้ไม่ใช่แค่ไม่เรียก external process แต่ไม่ใช่แม้แต่ subshell ด้วย การแก้ตัวแปรในลูปจึงยังอยู่ใน context เดิม จะเรียกว่า parent shell ก็ไม่ค่อยตรงเพราะไม่มี child
ตัวอย่างเหล่านี้อ่านทีละหนึ่งไบต์ด้วย
read -n 1แต่ก็ไม่จำเป็นเสมอไป ถ้าไม่ใส่-n1ลูปจะรวบข้อมูลให้มากที่สุดในแต่ละรอบจนกว่าจะเจอ nul จึงใช้หน่วยความจำและจำนวนรอบน้อยลงได้งานที่ผมใช้ต้องตัดและจัดการทีละไบต์หรือช่วงไบต์ตาม numeric offset และข้อมูลก็เล็กตามมาตรฐานปัจจุบัน เลยพบว่า array ของคู่เลขฐานสิบหกสะดวกมาก
a[n] == byte nพอดี และจัดการได้เหมือนกันหมดทั้งอักขระพิมพ์ได้, พิมพ์ไม่ได้, และ nul อีกทั้งยังใช้กับprintfเพื่อเก็บและสร้างกลับได้ รวมถึงใช้เป็นค่าตัวเลขโดยตรงอย่าง0x$nหรือ0x${a[n]}ได้ทันทีตัวอย่างเช่น อ่านไบต์ที่สองของ
h[]เป็นความยาว payload ที่ตามมา แล้วตัด payload ออกด้วย${h[@]:2:0x${h[1]}}แบบนี้แหละถึงจะเรียกว่า เว็บของแฮ็กเกอร์ ของจริง ยอดเยี่ยมมาก
ผมไม่ได้เก่ง Bash programming มาก แต่ก็น่าทึ่งที่จริง ๆ แล้วมัน ทรงพลังกว่าที่คิดและน่ากลัวน้อยกว่าที่คิด
Bash เป็นหนึ่งในเครื่องมือที่ผมชอบพอ ๆ กับ vim และ Lua
มันเป็นไบนารีเดี่ยวขนาดไม่ถึง 2MB มีอยู่ทุกที่ และทำอะไรได้มากกว่าที่หลายคนคิดมาก
ถ้าใช้ shellcheck และยึดแนวปฏิบัติที่ดี Bash ก็อ่านง่ายและปลอดภัยได้
ถ้าต้องการอะไรเพิ่ม ก็แค่ต่อยูทิลิตี C/C++ เข้าไปโดยไม่ต้องมี FFI ซับซ้อน และไม่ต้องดึง dependency ลึกลับกองโตผ่าน pip/npm ด้วย
ใน CI กับ Dockerfile มักมี Bash script แบบ inline อยู่เยอะ ผมก็ชอบย้ายพวกนั้นออกมาเป็นไฟล์สคริปต์แยกทั้งหมด พอ CI มี shellcheck แล้ว โค้ดพวกนั้นก็ถูกตรวจไปด้วย
นักพัฒนามักบ่นว่า “อ่านยากขึ้น” ทั้งที่จริงไม่ใช่เลย แถมยังปลอดภัยกว่ามาก
ปัญหาถัดไปคือ Bash script เดียวกันถูกก็อปปี้เป็นไฟล์เดียวกันไปหลาย repository นี่เป็นโครงสร้าง build แบบกระจายอยู่แล้ว ถ้าใครมีไอเดียปรับปรุงก็ยินดีรับฟัง
เสียดายที่สมัยเรียนไม่ได้ทุ่มเวลาให้มันมากกว่านี้ เพราะมันเป็นหนึ่งในเครื่องมือไม่กี่อย่างที่ตามมาอยู่กับอาชีพผมตลอด
สงสัยว่าทุกวันนี้ยังมีคนทำ custom server สำหรับเกมเชิงพาณิชย์อยู่ไหม
โดยเฉพาะเวอร์ชัน Java ที่อยู่ในจุดค่อนข้างพิเศษ เพราะยังได้รับอัปเดตคอนเทนต์ใหญ่ฟรีต่อเนื่อง ขณะเดียวกันก็รองรับการรันทุกเวอร์ชันเก่าอย่างเป็นทางการ และชุมชน modding ก็ใหญ่มาก
Minecraft(Java) ไม่ได้เป็นแค่เกม แต่แทบจะเป็น game engine ที่คนอื่นเอาไปต่อยอดกันอยู่ข้างบนด้วย
ไม่ว่าจะเพื่อแก้บั๊ก, เรียนภาษาใหม่ หรือแค่อยากรู้ว่าข้างในมันทำงานยังไง
Minecraft เป็นหนึ่งใน เกมที่ดีที่สุด นับตั้งแต่ Quake เป็นต้นมา
เรื่องแบบนี้เกิดขึ้นจริงได้ยังไงเนี่ย พระเจ้า
ผมอ่านบทความแนว implementation มาเยอะ แต่นี่เหนือชั้นไปอีก มันเป็นหนึ่งในบทความโปรเจ็กต์ประหลาดที่ดีที่สุดเท่าที่เคยอ่านมา และเป็นงานเขียนที่ยอดเยี่ยมจริง ๆ