[{"data":1,"prerenderedAt":504},["ShallowReactive",2],{"content-/projects/macerun-minecraft-server-on-esp32":3},{"article":4,"surround":499},{"id":5,"title":6,"author":7,"authorUrl":8,"body":9,"date":476,"dateUpdated":477,"description":478,"excerpt":477,"extension":479,"headline":480,"meta":481,"navigation":64,"path":482,"seo":483,"socialImage":484,"stem":490,"tags":491,"__hash__":498,"_path":482},"content/projects/macerun-minecraft-server-on-esp32.md","Macerun - Building a Minecraft Server on an ESP32 Microcontroller","angeldev0","https://github.com/4ngel2769",{"type":10,"value":11,"toc":466},"minimark",[12,16,29,34,37,44,47,53,58,61,73,77,85,88,118,125,271,275,278,281,291,295,298,305,373,380,384,387,392,410,415,426,435,439,442,445,448,462],[13,14,15],"p",{},"Running a Minecraft server on a beefy PC with upward of 16GB of RAM isn't super difficult these days. But as an embedded developer, I wanted to ask a different question: what is the absolute minimum hardware required to host a functional Minecraft server?",[13,17,18,19,23,24,28],{},"Inspired by projects like PortalRunner's ",[20,21,22],"em",{},"bareiron",", I decided to build ",[25,26,27],"strong",{},"Macerun",". It's a bare-metal Minecraft 1.16.5 (Java edition) server written completely in C for the ESP32-S3 microcontroller. There's no Java runtime, no Linux kernel, and no standard server wrappers.",[30,31,33],"h2",{"id":32},"visual-showcase","Visual Showcase",[13,35,36],{},"Here is a look at the tiny ESP32-S3 running the entire server, plugged into my PC with just a single USB-C cable:",[13,38,39],{},[40,41],"img",{"alt":42,"src":43},"ESP32-S3 Board connected via a single USB-C cable","/images/macerun-hardware.jpg",[13,45,46],{},"And here is what it looks like in-game!",[13,48,49],{},[40,50],{"alt":51,"src":52},"Gameplay screenshot of Macerun","https://ipp.angellabs.xyz/s/macerun-gameplay-1",[54,55,57],"h3",{"id":56},"video-demonstration","Video Demonstration",[13,59,60],{},"Check out this short video showing a player joining, breaking blocks, and using the chat directly through the microcontroller:",[62,63,66,67,72],"video",{"controls":64,"width":65},true,"100%","\n  ",[68,69],"source",{"src":70,"type":71},"https://i.imgur.com/nC9TFg4.mp4","video/mp4","\n  Your browser does not support the video tag.\n",[30,74,76],{"id":75},"the-challenge-speaking-minecraft-in-c","The Challenge: Speaking Minecraft in C",[13,78,79,80,84],{},"The ESP32-S3 ",[81,82,83],"code",{},"esp32s3n16r8"," has 16MB of Flash and 8MB of octal PSRAM. To make a Minecraft server fit, I had to throw out conventional file systems and memory models.",[13,86,87],{},"The architecture is built completely from scratch:",[89,90,91,98,112],"ol",{},[92,93,94,97],"li",{},[25,95,96],{},"Raw Sockets:"," Using lwIP TCP sockets running on FreeRTOS tasks to juggle connections.",[92,99,100,103,104,107,108,111],{},[25,101,102],{},"Protocol 754:"," Manually crafting the byte structures to speak Minecraft 1.16.5 protocol using community docs like ",[81,105,106],{},"wiki.vg"," and ",[81,109,110],{},"PrismarineJS",".",[92,113,114,117],{},[25,115,116],{},"No Anvil Files:"," Chunks are procedurally generated on the fly.",[13,119,120,121,124],{},"To communicate with the Java client, I had to build a custom packet reader/writer that handles Minecraft's specific data types, like ",[81,122,123],{},"VarInt","s.",[126,127,133],"pre",{"className":128,"code":129,"filename":130,"language":131,"meta":132,"style":132},"language-c shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","bool proto_write_varint(proto_writer_t *writer, int32_t value)\n{\n    uint32_t encoded = (uint32_t)value;\n\n    while (true)\n    {\n        uint8_t byte = encoded & 0x7F;\n        encoded >>= 7;\n        if (encoded != 0)\n        {\n            byte |= 0x80;\n        }\n        if (!proto_write_u8(writer, byte))\n        {\n            return false;\n        }\n        if (encoded == 0)\n        {\n            break;\n        }\n    }\n    return true;\n}\n","proto_framing.c","c","",[81,134,135,143,149,155,161,167,173,179,185,191,197,203,209,215,220,226,231,237,242,248,253,259,265],{"__ignoreMap":132},[136,137,140],"span",{"class":138,"line":139},"line",1,[136,141,142],{},"bool proto_write_varint(proto_writer_t *writer, int32_t value)\n",[136,144,146],{"class":138,"line":145},2,[136,147,148],{},"{\n",[136,150,152],{"class":138,"line":151},3,[136,153,154],{},"    uint32_t encoded = (uint32_t)value;\n",[136,156,158],{"class":138,"line":157},4,[136,159,160],{"emptyLinePlaceholder":64},"\n",[136,162,164],{"class":138,"line":163},5,[136,165,166],{},"    while (true)\n",[136,168,170],{"class":138,"line":169},6,[136,171,172],{},"    {\n",[136,174,176],{"class":138,"line":175},7,[136,177,178],{},"        uint8_t byte = encoded & 0x7F;\n",[136,180,182],{"class":138,"line":181},8,[136,183,184],{},"        encoded >>= 7;\n",[136,186,188],{"class":138,"line":187},9,[136,189,190],{},"        if (encoded != 0)\n",[136,192,194],{"class":138,"line":193},10,[136,195,196],{},"        {\n",[136,198,200],{"class":138,"line":199},11,[136,201,202],{},"            byte |= 0x80;\n",[136,204,206],{"class":138,"line":205},12,[136,207,208],{},"        }\n",[136,210,212],{"class":138,"line":211},13,[136,213,214],{},"        if (!proto_write_u8(writer, byte))\n",[136,216,218],{"class":138,"line":217},14,[136,219,196],{},[136,221,223],{"class":138,"line":222},15,[136,224,225],{},"            return false;\n",[136,227,229],{"class":138,"line":228},16,[136,230,208],{},[136,232,234],{"class":138,"line":233},17,[136,235,236],{},"        if (encoded == 0)\n",[136,238,240],{"class":138,"line":239},18,[136,241,196],{},[136,243,245],{"class":138,"line":244},19,[136,246,247],{},"            break;\n",[136,249,251],{"class":138,"line":250},20,[136,252,208],{},[136,254,256],{"class":138,"line":255},21,[136,257,258],{},"    }\n",[136,260,262],{"class":138,"line":261},22,[136,263,264],{},"    return true;\n",[136,266,268],{"class":138,"line":267},23,[136,269,270],{},"}\n",[30,272,274],{"id":273},"procedural-world-generation","Procedural World Generation",[13,276,277],{},"You can't load massive world files into 8MB of RAM. Instead, the world is generated procedurally right before it's sent over the network to the player.",[13,279,280],{},"The engine uses 2D biome generation combined with bilinear interpolated heightmaps. By using FNV-1a hashing, I can ensuring the terrain is deterministic without storing it all in memory.",[282,283,285],"alert",{"type":284},"tip",[13,286,287,290],{},[25,288,289],{},"Memory Optimization:"," By generating chunks on the fly into a shared transmission buffer, we prevent the heap fragmentation that usually plagues embedded web servers.",[30,292,294],{"id":293},"state-persistence-where-do-blocks-go","State Persistence: Where Do Blocks Go?",[13,296,297],{},"If we don't store the world in memory, what happens when a player places a dirt block?",[13,299,300,301,304],{},"Instead of saving whole chunks, Macerun only saves ",[25,302,303],{},"Block Deltas"," (the specific coordinates and block IDs of changes). These are kept in a tiny memory footprint and written directly to the ESP32's Non-Volatile Storage (NVS) flash partition.",[126,306,309],{"className":128,"code":307,"filename":308,"language":131,"meta":132,"style":132},"typedef struct __attribute__((packed))\n{\n    int16_t x;\n    uint8_t y;\n    int16_t z;\n    uint8_t block_id;\n} world_block_delta_t;\n\ntypedef struct\n{\n    world_block_delta_t entries[WORLD_MAX_BLOCK_DELTAS];\n    size_t count;\n} world_deltas_t;\n","block_deltas.h",[81,310,311,316,320,325,330,335,340,345,349,354,358,363,368],{"__ignoreMap":132},[136,312,313],{"class":138,"line":139},[136,314,315],{},"typedef struct __attribute__((packed))\n",[136,317,318],{"class":138,"line":145},[136,319,148],{},[136,321,322],{"class":138,"line":151},[136,323,324],{},"    int16_t x;\n",[136,326,327],{"class":138,"line":157},[136,328,329],{},"    uint8_t y;\n",[136,331,332],{"class":138,"line":163},[136,333,334],{},"    int16_t z;\n",[136,336,337],{"class":138,"line":169},[136,338,339],{},"    uint8_t block_id;\n",[136,341,342],{"class":138,"line":175},[136,343,344],{},"} world_block_delta_t;\n",[136,346,347],{"class":138,"line":181},[136,348,160],{"emptyLinePlaceholder":64},[136,350,351],{"class":138,"line":187},[136,352,353],{},"typedef struct\n",[136,355,356],{"class":138,"line":193},[136,357,148],{},[136,359,360],{"class":138,"line":199},[136,361,362],{},"    world_block_delta_t entries[WORLD_MAX_BLOCK_DELTAS];\n",[136,364,365],{"class":138,"line":205},[136,366,367],{},"    size_t count;\n",[136,369,370],{"class":138,"line":211},[136,371,372],{},"} world_deltas_t;\n",[13,374,375,376,379],{},"When a chunk generator runs, it overlays these ",[81,377,378],{},"world_deltas_t"," entries on top of the procedural terrain before sending it to the client.",[30,381,383],{"id":382},"what-works-and-what-doesnt","What Works (And What Doesn't)",[13,385,386],{},"Surprisingly, the server is highly playable! Up to 4 players can join concurrently.",[13,388,389],{},[25,390,391],{},"Currently Working:",[393,394,395,398,401,404,407],"ul",{},[92,396,397],{},"Terrain generation and chunk sending.",[92,399,400],{},"Block breaking and placing.",[92,402,403],{},"Basic physics, health, and hunger loops.",[92,405,406],{},"2x2 inventory crafting.",[92,408,409],{},"Multiplayer entity tracking and chat.",[13,411,412],{},[25,413,414],{},"Not Implemented Yet:",[393,416,417,420,423],{},[92,418,419],{},"Mob spawning and AI.",[92,421,422],{},"3x3 crafting benches, furnaces, and tile entities (chests).",[92,424,425],{},"Persisting player inventory and location upon disconnect.",[282,427,429],{"type":428},"caution",[13,430,431,434],{},[25,432,433],{},"Disclaimer:"," Please do not try to port your 100-player survival SMP to this code! It is strictly a proof-of-concept prototype.",[30,436,438],{"id":437},"conclusion","Conclusion",[13,440,441],{},"Building Macerun proved that with extreme optimization and a little bit of networking magic, even a complex game protocol like Minecraft can run on a $5 microcontroller. It's an incredible exercise in memory management and C programming.",[13,443,444],{},"The project is entirely open source. If you enjoy C programming, low-level networking, or constrained hardware, I would be happy to get some feedback or pull requests!",[446,447],"hr",{},[13,449,450,453,454,461],{},[25,451,452],{},"Want to check out the code?"," Head over to the ",[455,456,460],"a",{"href":457,"rel":458},"https://github.com/4ngel2769/macerun",[459],"nofollow","Macerun GitHub repository"," and take a look around! Let me know what you think.",[463,464,465],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":132,"searchDepth":145,"depth":145,"links":467},[468,471,472,473,474,475],{"id":32,"depth":145,"text":33,"children":469},[470],{"id":56,"depth":151,"text":57},{"id":75,"depth":145,"text":76},{"id":273,"depth":145,"text":274},{"id":293,"depth":145,"text":294},{"id":382,"depth":145,"text":383},{"id":437,"depth":145,"text":438},"2026-04-08T15:10:03",null,"A deep dive into writing a bare-metal Minecraft 1.16.5 server in C for the ESP32-S3. Learn how to handle custom procedural generation, raw TCP sockets, and extreme memory constraints.","md","Macerun: Running Minecraft 1.16.5 on an ESP32",{},"/projects/macerun-minecraft-server-on-esp32",{"title":6,"description":478},{"src":485,"mime":486,"alt":487,"width":488,"height":489},"https://i.imgur.com/l5k0cU7.png","png","Macerun Minecraft server running on ESP32-S3",1200,630,"projects/macerun-minecraft-server-on-esp32",[492,493,494,495,496,497],"Minecraft","ESP32","C","Embedded","FreeRTOS","Networking","IgMH890Y-GPX5gHmXXb0umC_FfRmxMBt_Rs89DQUfn4",[477,500],{"path":501,"headline":502,"excerpt":477,"date":503,"_path":501},"/projects/pwnagotchi-generator","Pwnagotchi Generator: Understanding opwngrid Through Reverse Engineering","2025-10-21T12:00:00",1775649124542]