Files
minecraftslop/main.cpp
2026-03-16 23:30:26 -03:00

983 lines
43 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <SDL2/SDL_opengl.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
#include <unordered_map>
#include <unordered_set>
#include <memory>
#include <algorithm>
#include <climits>
#include <string>
#include <thread>
#include <mutex>
#include <atomic>
#include <queue>
#include <deque>
#include <condition_variable>
#include <functional>
// Build:
// mkdir build && cd build && cmake .. && make -j$(nproc)
// ./minicraft
// ─── Constants ───────────────────────────────────────────────────────────────
static const int SCREEN_W = 1280;
static const int SCREEN_H = 720;
static const int CHUNK_SIZE = 16;
static const int WORLD_H = 64;
static const int RENDER_DIST = 16; // sensible default; easily changed
static const float MOVE_SPEED = 6.0f;
static const float MOUSE_SENS = 0.12f;
static const float GRAVITY = -22.0f;
static const float JUMP_VEL = 9.0f;
static const float HW = 0.3f;
static const float PH = 1.8f;
static const int HOTBAR_SIZE = 8;
// How many chunk meshes we build per frame (tune up/down for perf vs pop-in)
static const int MESH_BUILDS_PER_FRAME = 4;
// Worker threads for chunk generation
static const int GEN_THREADS = (int)std::max(1u, std::thread::hardware_concurrency() - 1);
// ─── Block types ─────────────────────────────────────────────────────────────
enum BlockType : uint8_t { AIR=0, GRASS, DIRT, STONE, WOOD, LEAVES, SAND, SNOW, WATER };
static const int NUM_BLOCK_TYPES = 9;
struct BlockColor { float r,g,b; };
static const BlockColor BLOCK_COLORS[] = {
{0,0,0},
{0.40f,0.72f,0.24f},
{0.55f,0.40f,0.22f},
{0.50f,0.50f,0.50f},
{0.45f,0.30f,0.15f},
{0.22f,0.55f,0.18f},
{0.88f,0.83f,0.52f},
{0.92f,0.95f,0.98f},
{0.18f,0.42f,0.88f},
};
static const char* BLOCK_NAMES[] = {
"Air","Grass","Dirt","Stone","Wood","Leaves","Sand","Snow","Water"
};
static const float FACE_SHADE[] = {1.0f, 0.5f, 0.8f, 0.7f, 0.65f, 0.65f};
// ─── Perlin Noise ─────────────────────────────────────────────────────────────
static int P[512];
static void initNoise(int seed){
int perm[256]; for(int i=0;i<256;i++) perm[i]=i;
unsigned rng=(unsigned)seed;
for(int i=255;i>0;i--){ rng=rng*1664525u+1013904223u; int j=(rng>>16)%(i+1); std::swap(perm[i],perm[j]); }
for(int i=0;i<512;i++) P[i]=perm[i&255];
}
static float fade(float t){return t*t*t*(t*(t*6-15)+10);}
static float lerp(float a,float b,float t){return a+t*(b-a);}
static float grad(int h,float x,float y){h&=3;float u=(h<2)?x:y,v=(h<2)?y:x;return((h&1)?-u:u)+((h&2)?-v:v);}
static float noise2(float x,float y){
int xi=(int)floorf(x)&255,yi=(int)floorf(y)&255;
float xf=x-floorf(x),yf=y-floorf(y),u=fade(xf),v=fade(yf);
int aa=P[P[xi]+yi],ab=P[P[xi]+yi+1],ba=P[P[xi+1]+yi],bb=P[P[xi+1]+yi+1];
return lerp(lerp(grad(aa,xf,yf),grad(ba,xf-1,yf),u),lerp(grad(ab,xf,yf-1),grad(bb,xf-1,yf-1),u),v);
}
static float fbm(float x,float y,int oct=6){
float val=0,amp=1,freq=1,mx=0;
for(int i=0;i<oct;i++){val+=noise2(x*freq,y*freq)*amp;mx+=amp;amp*=0.5f;freq*=2.0f;}
return val/mx;
}
// ─── World chunk ─────────────────────────────────────────────────────────────
struct Chunk {
int cx,cz;
uint8_t blocks[CHUNK_SIZE][WORLD_H][CHUNK_SIZE];
GLuint VAO=0,VBO=0,EBO=0;
int indexCount=0;
// meshReady: the VAO holds a valid (possibly empty) mesh
// meshDirty: block data changed, needs a rebuild
bool meshDirty=true, meshReady=false, generated=false;
Chunk(int cx,int cz):cx(cx),cz(cz){memset(blocks,AIR,sizeof(blocks));}
~Chunk(){
if(VAO){ glDeleteVertexArrays(1,&VAO); glDeleteBuffers(1,&VBO); glDeleteBuffers(1,&EBO); }
}
};
struct ChunkKey{
int x,z;
bool operator==(const ChunkKey& o)const{return x==o.x&&z==o.z;}
};
struct ChunkKeyHash{
size_t operator()(const ChunkKey& k)const{
return std::hash<long long>()((long long)(unsigned)k.x<<32|(unsigned)k.z);
}
};
using ChunkMap = std::unordered_map<ChunkKey,std::unique_ptr<Chunk>,ChunkKeyHash>;
// ─── Thread pool ─────────────────────────────────────────────────────────────
struct ThreadPool {
std::vector<std::thread> workers;
std::deque<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> stop{false};
std::atomic<int> pending{0};
void start(int n){
for(int i=0;i<n;i++)
workers.emplace_back([this]{
while(true){
std::function<void()> task;
{ std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk,[this]{return stop.load()||!tasks.empty();});
if(stop&&tasks.empty()) return;
task=std::move(tasks.front()); tasks.pop_front(); }
task(); --pending;
}
});
}
void enqueue(std::function<void()> f){
{ std::lock_guard<std::mutex> lk(mtx); ++pending; tasks.push_back(std::move(f)); }
cv.notify_one();
}
void shutdown(){
stop=true; cv.notify_all();
for(auto& w:workers) w.join();
}
} POOL;
// ─── Global state ─────────────────────────────────────────────────────────────
static ChunkMap CHUNKS;
// Finished-generation queue: workers push here, main thread drains
struct FinishedChunk { int cx,cz; std::unique_ptr<Chunk> chunk; };
static std::queue<FinishedChunk> FINISHED_Q;
static std::mutex FINISHED_MTX;
// In-flight set: prevents double-submitting
static std::unordered_set<ChunkKey,ChunkKeyHash> IN_FLIGHT;
static std::mutex IN_FLIGHT_MTX;
// Dirty mesh queue (ordered set so we don't double-process)
static std::unordered_set<ChunkKey,ChunkKeyHash> DIRTY_SET;
// ─── Chunk generation (worker thread, no GL) ──────────────────────────────────
static void generateChunkData(Chunk& c){
int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE;
for(int x=0;x<CHUNK_SIZE;x++)
for(int z=0;z<CHUNK_SIZE;z++){
float wx=(wx0+x)*0.008f, wz=(wz0+z)*0.008f;
float cont = fbm(wx*0.4f, wz*0.4f, 4);
float detail = fbm(wx*2.0f+5.3f, wz*2.0f+1.7f, 5);
float ridge = 1.0f-fabsf(fbm(wx*0.8f+3.1f, wz*0.8f+8.9f, 4));
float h = 12.0f + cont*18.0f + detail*6.0f + ridge*14.0f*std::max(0.0f,cont);
int top = std::max(2, std::min(WORLD_H-2, (int)h));
bool isSand=(top<16), isSnow=(top>38);
for(int y=0;y<WORLD_H;y++){
if(y==0) c.blocks[x][y][z]=STONE;
else if(y<top-4) c.blocks[x][y][z]=STONE;
else if(y<top) c.blocks[x][y][z]=isSand?SAND:DIRT;
else if(y==top) c.blocks[x][y][z]=isSand?SAND:(isSnow?SNOW:GRASS);
else if(y<=14) c.blocks[x][y][z]=WATER;
else c.blocks[x][y][z]=AIR;
}
if(!isSand&&!isSnow&&top>=16&&top<36&&top+6<WORLD_H){
unsigned col=(unsigned)((wx0+x)*73856093u^(unsigned)(wz0+z)*19349663u);
if((col&0xFF)<18){
int trunk=top+1;
for(int t=trunk;t<trunk+4&&t<WORLD_H;t++) c.blocks[x][t][z]=WOOD;
for(int dx=-2;dx<=2;dx++) for(int dz=-2;dz<=2;dz++) for(int dy=trunk+2;dy<=trunk+5;dy++){
int nx=x+dx, nz=z+dz;
if(nx>=0&&nx<CHUNK_SIZE&&nz>=0&&nz<CHUNK_SIZE&&dy<WORLD_H)
if(c.blocks[nx][dy][nz]==AIR) c.blocks[nx][dy][nz]=LEAVES;
}
}
}
}
c.generated=true;
}
static void submitChunkGen(int cx,int cz){
ChunkKey k{cx,cz};
{ std::lock_guard<std::mutex> lk(IN_FLIGHT_MTX);
if(IN_FLIGHT.count(k)) return;
IN_FLIGHT.insert(k); }
POOL.enqueue([cx,cz,k]{
auto c=std::make_unique<Chunk>(cx,cz);
generateChunkData(*c);
{ std::lock_guard<std::mutex> lk(FINISHED_MTX);
FINISHED_Q.push({cx,cz,std::move(c)}); }
{ std::lock_guard<std::mutex> lk(IN_FLIGHT_MTX);
IN_FLIGHT.erase(k); }
});
}
// ─── Block accessors (main thread only) ──────────────────────────────────────
// Returns the block at world coords, using only the chunk's own data when
// called during mesh building (no cross-chunk queries that might be stale).
static uint8_t getBlock(int wx,int wy,int wz){
if(wy<0) return STONE; if(wy>=WORLD_H) return AIR;
int cx=(int)floorf((float)wx/CHUNK_SIZE);
int cz=(int)floorf((float)wz/CHUNK_SIZE);
auto it=CHUNKS.find({cx,cz});
if(it==CHUNKS.end()||!it->second->generated) return AIR; // treat unknown as AIR so border faces show
int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE;
return it->second->blocks[lx][wy][lz];
}
static void setBlock(int wx,int wy,int wz,uint8_t val){
if(wy<0||wy>=WORLD_H) return;
int cx=(int)floorf((float)wx/CHUNK_SIZE);
int cz=(int)floorf((float)wz/CHUNK_SIZE);
auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return;
int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE;
it->second->blocks[lx][wy][lz]=val;
DIRTY_SET.insert({cx,cz});
// Also dirty the four face-adjacent neighbours
static const int NX[]={-1,1,0,0}, NZ[]={0,0,-1,1};
static const int BX[]={ 0,CHUNK_SIZE-1,lx,lx}, BZ[]={lz,lz,0,CHUNK_SIZE-1};
(void)BX; (void)BZ;
if(lx==0) DIRTY_SET.insert({cx-1,cz});
if(lx==CHUNK_SIZE-1) DIRTY_SET.insert({cx+1,cz});
if(lz==0) DIRTY_SET.insert({cx,cz-1});
if(lz==CHUNK_SIZE-1) DIRTY_SET.insert({cx,cz+1});
(void)NX; (void)NZ;
}
// ─── Mesh building (GL, main thread) ─────────────────────────────────────────
struct Vertex { float x,y,z,r,g,b; };
// When building a mesh we need to peek at neighbouring chunks for face culling.
// Key insight: we query getBlock() which returns AIR for unloaded chunks.
// This means border faces toward unloaded chunks will be SHOWN (not culled),
// which is correct — you can see the world edge. Once the neighbour loads,
// its arrival marks this chunk dirty and it gets rebuilt with proper culling.
static bool shouldDrawFace(uint8_t cur, int wx, int wy, int wz){
uint8_t nb = getBlock(wx,wy,wz);
if(nb==AIR) return true;
if(nb==WATER||nb==LEAVES) return cur!=nb;
return false;
}
static void addFace(std::vector<Vertex>& verts, std::vector<GLuint>& idx,
glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3,
float r,float g,float b,float shade)
{
GLuint base=(GLuint)verts.size();
float sr=r*shade, sg=g*shade, sb=b*shade;
verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb});
verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb});
verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb});
verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb});
idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3});
}
static void buildChunkMesh(Chunk& c){
std::vector<Vertex> verts;
std::vector<GLuint> idx;
verts.reserve(4096);
idx.reserve(6144);
int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE;
for(int x=0;x<CHUNK_SIZE;x++)
for(int y=0;y<WORLD_H;y++)
for(int z=0;z<CHUNK_SIZE;z++){
uint8_t b=c.blocks[x][y][z];
if(b==AIR) continue;
float r=BLOCK_COLORS[b].r, g=BLOCK_COLORS[b].g, bl=BLOCK_COLORS[b].b;
float bx=wx0+x, by=y, bz=wz0+z;
int wx=wx0+x, wz=wz0+z;
// +Y top
if(shouldDrawFace(b,wx,y+1,wz))
addFace(verts,idx,{bx,by+1,bz+1},{bx+1,by+1,bz+1},{bx+1,by+1,bz},{bx,by+1,bz},r,g,bl,FACE_SHADE[0]);
// -Y bottom
if(shouldDrawFace(b,wx,y-1,wz))
addFace(verts,idx,{bx,by,bz},{bx+1,by,bz},{bx+1,by,bz+1},{bx,by,bz+1},r,g,bl,FACE_SHADE[1]);
// +Z front
if(shouldDrawFace(b,wx,y,wz+1))
addFace(verts,idx,{bx,by,bz+1},{bx+1,by,bz+1},{bx+1,by+1,bz+1},{bx,by+1,bz+1},r,g,bl,FACE_SHADE[2]);
// -Z back
if(shouldDrawFace(b,wx,y,wz-1))
addFace(verts,idx,{bx+1,by,bz},{bx,by,bz},{bx,by+1,bz},{bx+1,by+1,bz},r,g,bl,FACE_SHADE[3]);
// -X left
if(shouldDrawFace(b,wx-1,y,wz))
addFace(verts,idx,{bx,by,bz},{bx,by,bz+1},{bx,by+1,bz+1},{bx,by+1,bz},r,g,bl,FACE_SHADE[4]);
// +X right
if(shouldDrawFace(b,wx+1,y,wz))
addFace(verts,idx,{bx+1,by,bz+1},{bx+1,by,bz},{bx+1,by+1,bz},{bx+1,by+1,bz+1},r,g,bl,FACE_SHADE[5]);
}
if(!c.VAO){ glGenVertexArrays(1,&c.VAO); glGenBuffers(1,&c.VBO); glGenBuffers(1,&c.EBO); }
glBindVertexArray(c.VAO);
glBindBuffer(GL_ARRAY_BUFFER,c.VBO);
glBufferData(GL_ARRAY_BUFFER,(GLsizeiptr)(verts.size()*sizeof(Vertex)),verts.data(),GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,c.EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,(GLsizeiptr)(idx.size()*sizeof(GLuint)),idx.data(),GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); glEnableVertexAttribArray(0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1);
glBindVertexArray(0);
c.indexCount = (int)idx.size();
c.meshDirty = false;
c.meshReady = true;
}
// Drain finished-generation queue, install chunks, schedule their meshes + neighbours
static void drainFinishedChunks(){
while(true){
FinishedChunk fc;
{ std::lock_guard<std::mutex> lk(FINISHED_MTX);
if(FINISHED_Q.empty()) break;
fc=std::move(FINISHED_Q.front()); FINISHED_Q.pop(); }
ChunkKey k{fc.cx,fc.cz};
CHUNKS[k]=std::move(fc.chunk);
// Schedule this chunk AND its four face-neighbours for meshing.
// The neighbours need to be rebuilt so their border faces toward this
// newly arrived chunk get properly culled.
DIRTY_SET.insert(k);
DIRTY_SET.insert({fc.cx-1,fc.cz});
DIRTY_SET.insert({fc.cx+1,fc.cz});
DIRTY_SET.insert({fc.cx,fc.cz-1});
DIRTY_SET.insert({fc.cx,fc.cz+1});
}
}
// Process up to N chunks from the dirty set each frame.
// Only build a chunk if it AND all four face-neighbours are generated —
// this ensures the border-face culling is correct on the first build.
static void processDirtyMeshes(int maxPerFrame, int pcx, int pcz){
if(DIRTY_SET.empty()) return;
// Sort dirty chunks by distance to player for closest-first pop-in
std::vector<ChunkKey> sorted(DIRTY_SET.begin(),DIRTY_SET.end());
std::sort(sorted.begin(),sorted.end(),[&](const ChunkKey& a,const ChunkKey& b){
int da=(a.x-pcx)*(a.x-pcx)+(a.z-pcz)*(a.z-pcz);
int db=(b.x-pcx)*(b.x-pcx)+(b.z-pcz)*(b.z-pcz);
return da<db;
});
int built=0;
for(auto& k : sorted){
if(built>=maxPerFrame) break;
auto it=CHUNKS.find(k);
if(it==CHUNKS.end()){ DIRTY_SET.erase(k); continue; } // evicted
Chunk& c=*it->second;
if(!c.generated){ continue; } // still generating
// Check all four face-neighbours exist and are generated.
// If a neighbour doesn't exist yet, keep this chunk dirty and skip —
// it will be rebuilt properly when the neighbour arrives.
bool allNeighbours=true;
const int dx[]={-1,1,0,0}, dz[]={0,0,-1,1};
for(int i=0;i<4;i++){
auto nit=CHUNKS.find({k.x+dx[i],k.z+dz[i]});
if(nit==CHUNKS.end()||!nit->second->generated){
allNeighbours=false; break;
}
}
if(!allNeighbours) continue; // wait for neighbours
buildChunkMesh(c);
DIRTY_SET.erase(k);
++built;
}
}
// ─── Shaders ─────────────────────────────────────────────────────────────────
static const char* VS_SRC = R"(
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
out vec3 vColor;
out float vFog;
uniform mat4 uMVP;
uniform vec3 uCamPos;
void main(){
gl_Position = uMVP * vec4(aPos,1.0);
vColor = aColor;
float dist = length(aPos - uCamPos);
vFog = clamp((dist - 80.0) / 60.0, 0.0, 1.0);
})";
static const char* FS_SRC = R"(
#version 330 core
in vec3 vColor;
in float vFog;
out vec4 fragColor;
void main(){
vec3 fogColor = vec3(0.53, 0.81, 0.98);
fragColor = vec4(mix(vColor, fogColor, vFog), 1.0);
})";
static const char* VS_UI = R"(
#version 330 core
layout(location=0) in vec2 aPos;
layout(location=1) in vec3 aColor;
out vec3 vColor;
uniform mat4 uProj;
void main(){ gl_Position = uProj*vec4(aPos,0.0,1.0); vColor = aColor; })";
static const char* FS_UI = R"(
#version 330 core
in vec3 vColor;
out vec4 fragColor;
uniform float uAlpha;
void main(){ fragColor = vec4(vColor, uAlpha); })";
static GLuint compileShader(GLenum type, const char* src){
GLuint s=glCreateShader(type); glShaderSource(s,1,&src,nullptr); glCompileShader(s);
GLint ok; glGetShaderiv(s,GL_COMPILE_STATUS,&ok);
if(!ok){ char buf[512]; glGetShaderInfoLog(s,512,nullptr,buf); std::cerr<<"Shader err: "<<buf<<"\n"; }
return s;
}
static GLuint buildProg(const char* vs, const char* fs){
GLuint v=compileShader(GL_VERTEX_SHADER,vs), f=compileShader(GL_FRAGMENT_SHADER,fs);
GLuint p=glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
glDeleteShader(v); glDeleteShader(f); return p;
}
// ─── 2D UI ───────────────────────────────────────────────────────────────────
struct UIRenderer {
GLuint prog=0,VAO=0,VBO=0;
GLint projLoc=-1,alphaLoc=-1;
glm::mat4 proj{1.0f};
void init(){
prog=buildProg(VS_UI,FS_UI);
projLoc =glGetUniformLocation(prog,"uProj");
alphaLoc=glGetUniformLocation(prog,"uAlpha");
proj=glm::ortho(0.0f,(float)SCREEN_W,(float)SCREEN_H,0.0f);
glGenVertexArrays(1,&VAO); glGenBuffers(1,&VBO);
glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,1024*sizeof(float),nullptr,GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)0); glEnableVertexAttribArray(0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float))); glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void begin(){
glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE);
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(prog);
glUniformMatrix4fv(projLoc,1,GL_FALSE,glm::value_ptr(proj));
glBindVertexArray(VAO);
}
void end(){
glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE);
glBindVertexArray(0);
}
void rect(float x,float y,float w,float h,float r,float g,float b,float a=1.0f){
float v[]={x,y,r,g,b, x+w,y,r,g,b, x+w,y+h,r,g,b, x,y,r,g,b, x+w,y+h,r,g,b, x,y+h,r,g,b};
glUniform1f(alphaLoc,a);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(v),v);
glDrawArrays(GL_TRIANGLES,0,6);
}
void outline(float x,float y,float w,float h,float r,float g,float b,float t=2.0f){
rect(x,y,w,t,r,g,b); rect(x,y+h-t,w,t,r,g,b);
rect(x,y,t,h,r,g,b); rect(x+w-t,y,t,h,r,g,b);
}
} UI;
// ─── Bitmap font 5×7 (ASCII 32126) ──────────────────────────────────────────
static const uint8_t FONT5x7[][5]={
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},
{0x14,0x7F,0x14,0x7F,0x14},{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},
{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},{0x00,0x1C,0x22,0x41,0x00},
{0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08},
{0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},
{0x20,0x10,0x08,0x04,0x02},{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},
{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},{0x18,0x14,0x12,0x7F,0x10},
{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
{0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},
{0x00,0x56,0x36,0x00,0x00},{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},
{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},{0x32,0x49,0x79,0x41,0x3E},
{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
{0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},
{0x3E,0x41,0x49,0x49,0x7A},{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},
{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},{0x7F,0x40,0x40,0x40,0x40},
{0x7F,0x02,0x04,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},
{0x46,0x49,0x49,0x49,0x31},{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},
{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},{0x63,0x14,0x08,0x14,0x63},
{0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7F,0x41,0x41,0x00},
{0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},
{0x40,0x40,0x40,0x40,0x40},{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},
{0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},{0x38,0x44,0x44,0x48,0x7F},
{0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x0C,0x52,0x52,0x52,0x3E},
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00},
{0x7F,0x10,0x28,0x44,0x00},{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78},
{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},{0x7C,0x14,0x14,0x14,0x08},
{0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},
{0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},
{0x3C,0x40,0x30,0x40,0x3C},{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C},
{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},{0x00,0x00,0x7F,0x00,0x00},
{0x00,0x41,0x36,0x08,0x00},{0x10,0x08,0x08,0x10,0x08},
};
static void drawText(const char* text,float x,float y,float scale,
float r,float g,float b,float a=1.0f){
for(int i=0;text[i];i++){
int c=text[i]-32;
if(c<0||c>=(int)(sizeof(FONT5x7)/sizeof(FONT5x7[0]))){x+=6*scale;continue;}
for(int col=0;col<5;col++){
uint8_t bits=FONT5x7[c][col];
for(int row=0;row<7;row++)
if(bits&(1<<row)) UI.rect(x+col*scale,y+row*scale,scale,scale,r,g,b,a);
}
x+=6*scale;
}
}
// ─── Loading screen ───────────────────────────────────────────────────────────
static void drawLoadingScreen(SDL_Window* win, int ready, int total){
glClearColor(0.08f,0.08f,0.12f,1.0f);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
UI.begin();
const char* title="MiniCraft";
drawText(title,(SCREEN_W-strlen(title)*6*4.f)*0.5f,SCREEN_H*0.35f,4.f,0.4f,0.8f,0.4f);
const char* lbl="Generating world...";
drawText(lbl,(SCREEN_W-strlen(lbl)*6*2.f)*0.5f,SCREEN_H*0.52f,2.f,0.8f,0.8f,0.8f);
float bw=500,bh=22,bx=(SCREEN_W-bw)*0.5f,by=SCREEN_H*0.58f;
float pct=total>0?(float)ready/total:0;
UI.rect(bx,by,bw,bh,0.2f,0.2f,0.2f);
UI.rect(bx,by,bw*pct,bh,0.3f,0.7f,0.3f);
UI.outline(bx,by,bw,bh,0.5f,0.5f,0.5f,2.f);
char buf[48]; snprintf(buf,48,"%d / %d chunks",ready,total);
drawText(buf,(SCREEN_W-strlen(buf)*6*1.5f)*0.5f,by+bh+8,1.5f,0.7f,0.7f,0.7f);
UI.end();
SDL_GL_SwapWindow(win);
}
// ─── Inventory ───────────────────────────────────────────────────────────────
struct Inventory {
uint8_t hotbar[HOTBAR_SIZE]={GRASS,DIRT,STONE,WOOD,LEAVES,SAND,SNOW,WATER};
int selected=0; bool open=false;
uint8_t selectedBlock()const{return hotbar[selected];}
};
static void drawInventory(Inventory& inv){
const float S=52,G=6,B=4;
float tw=HOTBAR_SIZE*(S+G)-G, hx=(SCREEN_W-tw)*0.5f, hy=SCREEN_H-S-16;
UI.rect(hx-8,hy-8,tw+16,S+16,0.15f,0.15f,0.15f,0.75f);
UI.outline(hx-8,hy-8,tw+16,S+16,0,0,0);
for(int i=0;i<HOTBAR_SIZE;i++){
float sx=hx+i*(S+G),sy=hy; bool sel=(i==inv.selected);
UI.rect(sx,sy,S,S,sel?.3f:.2f,sel?.3f:.2f,sel?.3f:.2f,.9f);
uint8_t bt=inv.hotbar[i];
if(bt!=AIR){
auto& bc=BLOCK_COLORS[bt];
UI.rect(sx+B,sy+B,S-2*B,S-2*B-10,bc.r,bc.g,bc.b);
UI.rect(sx+B,sy+S-B-14,S-2*B,4,bc.r*.65f,bc.g*.65f,bc.b*.65f);
}
if(sel) UI.outline(sx,sy,S,S,1,1,1,B*.5f);
else UI.outline(sx,sy,S,S,0,0,0,2);
char num[3]; snprintf(num,3,"%d",i+1);
drawText(num,sx+4,sy+S-12,1.5f,.8f,.8f,.8f);
}
{ const char* n=BLOCK_NAMES[inv.selectedBlock()];
drawText(n,(SCREEN_W-strlen(n)*12.f)*0.5f,hy-26,2,.9f,.9f,.9f,.9f); }
if(!inv.open) return;
const int COLS=4,ROWS=(NUM_BLOCK_TYPES-1+COLS-1)/COLS;
float iw=COLS*(S+G)-G+32,ih=ROWS*(S+G)-G+60;
float ix=(SCREEN_W-iw)*.5f,iy=(SCREEN_H-ih)*.5f;
UI.rect(0,0,(float)SCREEN_W,(float)SCREEN_H,0,0,0,.45f);
UI.rect(ix,iy,iw,ih,.18f,.18f,.18f,.95f);
UI.outline(ix,iy,iw,ih,.6f,.6f,.6f,2);
drawText("INVENTORY",ix+10,iy+10,2,1,1,1);
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
int row=(bi-1)/COLS,col=(bi-1)%COLS;
float sx=ix+16+col*(S+G),sy=iy+40+row*(S+G);
bool inH=false; for(int h=0;h<HOTBAR_SIZE;h++) if(inv.hotbar[h]==bi) inH=true;
auto& bc=BLOCK_COLORS[bi];
UI.rect(sx,sy,S,S,.25f,.25f,.25f,.9f);
UI.rect(sx+B,sy+B,S-2*B,S-2*B-8,bc.r,bc.g,bc.b);
UI.rect(sx+B,sy+S-B-12,S-2*B,4,bc.r*.65f,bc.g*.65f,bc.b*.65f);
UI.outline(sx,sy,S,S,inH?1.f:.4f,inH?1.f:.4f,inH?0.f:.4f,2);
float ntw=strlen(BLOCK_NAMES[bi])*9.f;
drawText(BLOCK_NAMES[bi],sx+(S-ntw)*.5f,sy+S-10,1.5f,.9f,.9f,.9f);
}
drawText("Click slot (1-8). E=close.",ix+10,iy+ih-18,1.5f,.7f,.7f,.7f);
}
// ─── HUD ─────────────────────────────────────────────────────────────────────
static void drawHUD(const glm::vec3& pos, bool thirdPerson,
int bgPending, int fps, int visChunks){
// Crosshair
float cx=SCREEN_W*.5f,cy=SCREEN_H*.5f;
UI.rect(cx-1,cy-10,2,20,1,1,1,.85f);
UI.rect(cx-10,cy-1,20,2,1,1,1,.85f);
UI.rect(cx-1,cy-1,2,2,0,0,0,.85f);
// Stats panel (top-left)
char buf[128];
snprintf(buf,128,"XYZ: %.0f / %.0f / %.0f",pos.x,pos.y-PH,pos.z);
float pw=strlen(buf)*6*1.5f+10;
UI.rect(5,5,pw,60,0,0,0,.55f);
drawText(buf,9,9,1.5f,1,1,1);
char fbuf[32]; snprintf(fbuf,32,"FPS: %d",fps);
drawText(fbuf,9,22,1.5f, fps>=55?0.3f:fps>=30?1.f:1.f,
fps>=55?1.0f:fps>=30?0.8f:0.3f,
fps>=55?0.3f:fps>=30?0.1f:0.1f);
char cbuf[48]; snprintf(cbuf,48,"Chunks: %d vis / %d load",visChunks,bgPending);
drawText(cbuf,9,35,1.5f,.7f,.7f,.7f);
if(thirdPerson) drawText("[F5] 3rd person",9,49,1.5f,.8f,.8f,.5f);
// Loading badge (top-right)
if(bgPending>0){
char lb[40]; snprintf(lb,40,"Loading %d...",bgPending);
float lw=strlen(lb)*9+8;
UI.rect(SCREEN_W-lw-4,5,lw,16,0,0,0,.6f);
drawText(lb,SCREEN_W-lw,8,1.5f,.4f,1,.4f);
}
}
// ─── Camera ──────────────────────────────────────────────────────────────────
struct Camera {
glm::vec3 pos{0,40,0}; float yaw=0,pitch=0;
glm::vec3 vel{0,0,0}; bool onGround=false;
glm::vec3 forward()const{
float y=glm::radians(yaw),p=glm::radians(pitch);
return glm::normalize(glm::vec3(cosf(p)*cosf(y),sinf(p),cosf(p)*sinf(y)));
}
glm::vec3 right()const{return glm::normalize(glm::cross(forward(),{0,1,0}));}
glm::mat4 firstPersonView()const{return glm::lookAt(pos,pos+forward(),{0,1,0});}
glm::mat4 thirdPersonView()const{
return glm::lookAt(pos-forward()*6.f+glm::vec3(0,2,0),pos,{0,1,0});
}
};
static bool solidAt(float fx,float fy,float fz){
if((int)floorf(fy)<0) return true;
if((int)floorf(fy)>=WORLD_H) return false;
uint8_t b=getBlock((int)floorf(fx),(int)floorf(fy),(int)floorf(fz));
return b!=AIR&&b!=WATER&&b!=LEAVES;
}
static bool aabbSolid(float px,float py,float pz){
int x0=(int)floorf(px-HW),x1=(int)floorf(px+HW);
int y0=(int)floorf(py-PH),y1=(int)floorf(py+0.05f);
int z0=(int)floorf(pz-HW),z1=(int)floorf(pz+HW);
for(int x=x0;x<=x1;x++) for(int y=y0;y<=y1;y++) for(int z=z0;z<=z1;z++)
if(solidAt((float)x,(float)y,(float)z)) return true;
return false;
}
static void moveCamera(Camera& cam,const glm::vec3& move,float dt){
cam.pos.x+=move.x*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.x-=move.x*dt;
cam.pos.z+=move.z*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.z-=move.z*dt;
cam.vel.y+=GRAVITY*dt; cam.pos.y+=cam.vel.y*dt;
if(cam.vel.y<0){
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
cam.pos.y=floorf(cam.pos.y-PH)+1.f+PH+.001f; cam.vel.y=0; cam.onGround=true;
} else cam.onGround=false;
} else {
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
cam.pos.y=floorf(cam.pos.y+.05f)-.05f-.001f; cam.vel.y=0;
}
cam.onGround=false;
}
}
// ─── Player model ─────────────────────────────────────────────────────────────
struct PlayerModel {
GLuint VAO=0,VBO=0,EBO=0;
std::vector<Vertex> verts; std::vector<GLuint> idx;
int indexCount=0;
void addBox(float x0,float y0,float z0,float x1,float y1,float z1,float r,float g,float b){
auto face=[&](glm::vec3 a,glm::vec3 b_,glm::vec3 c,glm::vec3 d,float s){
GLuint base=(GLuint)verts.size();
verts.push_back({a.x,a.y,a.z,r*s,g*s,b*s}); verts.push_back({b_.x,b_.y,b_.z,r*s,g*s,b*s});
verts.push_back({c.x,c.y,c.z,r*s,g*s,b*s}); verts.push_back({d.x,d.y,d.z,r*s,g*s,b*s});
idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3});
};
face({x0,y1,z0},{x1,y1,z0},{x1,y1,z1},{x0,y1,z1},1.f);
face({x0,y0,z1},{x1,y0,z1},{x1,y0,z0},{x0,y0,z0},.5f);
face({x0,y0,z1},{x1,y0,z1},{x1,y1,z1},{x0,y1,z1},.8f);
face({x1,y0,z0},{x0,y0,z0},{x0,y1,z0},{x1,y1,z0},.7f);
face({x0,y0,z0},{x0,y0,z1},{x0,y1,z1},{x0,y1,z0},.65f);
face({x1,y0,z1},{x1,y0,z0},{x1,y1,z0},{x1,y1,z1},.65f);
}
void build(){
verts.clear(); idx.clear();
addBox(-.3f,1.55f,-.3f,.3f,2.15f,.3f,.9f,.72f,.55f);
addBox(-.32f,1.9f,-.32f,.32f,2.17f,.32f,.3f,.18f,.08f);
addBox(-.3f,.7f,-.2f,.3f,1.5f,.2f,.25f,.45f,.75f);
addBox(-.55f,.7f,-.15f,-.32f,1.45f,.15f,.9f,.72f,.55f);
addBox(.32f,.7f,-.15f,.55f,1.45f,.15f,.9f,.72f,.55f);
addBox(-.28f,0,-.15f,-.05f,.7f,.15f,.2f,.2f,.6f);
addBox(.05f,0,-.15f,.28f,.7f,.15f,.2f,.2f,.6f);
indexCount=(int)idx.size();
if(!VAO){glGenVertexArrays(1,&VAO);glGenBuffers(1,&VBO);glGenBuffers(1,&EBO);}
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,(GLsizeiptr)(verts.size()*sizeof(Vertex)),verts.data(),GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,(GLsizeiptr)(idx.size()*sizeof(GLuint)),idx.data(),GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); glEnableVertexAttribArray(0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void draw(GLuint prog,GLint mvpLoc,GLint camLoc,
const glm::mat4& proj,const glm::mat4& view,const Camera& cam)const{
glm::vec3 feet=cam.pos-glm::vec3(0,PH,0);
glm::mat4 model=glm::rotate(glm::translate(glm::mat4(1),feet),
glm::radians(cam.yaw+180.f),glm::vec3(0,1,0));
glm::mat4 mvp=proj*view*model;
glUseProgram(prog);
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,(GLsizei)indexCount,GL_UNSIGNED_INT,0);
glBindVertexArray(0);
}
} PLAYER_MODEL;
// ─── Raycast ─────────────────────────────────────────────────────────────────
static bool raycast(const Camera& cam,glm::ivec3& hit,glm::ivec3& prev){
glm::vec3 dir=cam.forward(),p=cam.pos;
glm::ivec3 last{(int)floorf(p.x),(int)floorf(p.y),(int)floorf(p.z)};
for(float t=0;t<8.f;t+=0.05f){
glm::vec3 rp=p+dir*t;
glm::ivec3 bp{(int)floorf(rp.x),(int)floorf(rp.y),(int)floorf(rp.z)};
if(bp.y<0||bp.y>=WORLD_H){last=bp;continue;}
uint8_t b=getBlock(bp.x,bp.y,bp.z);
if(b!=AIR&&b!=WATER){hit=bp;prev=last;return true;}
last=bp;
}
return false;
}
// ─── Chunk range management ───────────────────────────────────────────────────
static void requestChunks(int pcx,int pcz){
struct E{int dx,dz,d2;};
std::vector<E> needed;
for(int dx=-RENDER_DIST;dx<=RENDER_DIST;dx++)
for(int dz=-RENDER_DIST;dz<=RENDER_DIST;dz++){
ChunkKey k{pcx+dx,pcz+dz};
if(CHUNKS.count(k)) continue;
needed.push_back({dx,dz,dx*dx+dz*dz});
}
std::sort(needed.begin(),needed.end(),[](auto& a,auto& b){return a.d2<b.d2;});
for(auto& e:needed) submitChunkGen(pcx+e.dx,pcz+e.dz);
}
static void evictChunks(int pcx,int pcz){
std::vector<ChunkKey> rem;
for(auto& [k,c]:CHUNKS)
if(abs(k.x-pcx)>RENDER_DIST+2||abs(k.z-pcz)>RENDER_DIST+2) rem.push_back(k);
for(auto& k:rem){ CHUNKS.erase(k); DIRTY_SET.erase(k); }
}
// ─── Main ─────────────────────────────────────────────────────────────────────
int main(){
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,24);
SDL_Window* win=SDL_CreateWindow("MiniCraft",
SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,SCREEN_W,SCREEN_H,
SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN);
SDL_GLContext ctx=SDL_GL_CreateContext(win);
SDL_GL_SetSwapInterval(1);
glewExperimental=GL_TRUE; glewInit();
glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK);
GLuint prog3d=buildProg(VS_SRC,FS_SRC);
GLint mvpLoc=glGetUniformLocation(prog3d,"uMVP");
GLint camLoc=glGetUniformLocation(prog3d,"uCamPos");
UI.init();
initNoise(12345);
std::cout<<"Using "<<GEN_THREADS<<" generation threads, RENDER_DIST="<<RENDER_DIST<<"\n";
POOL.start(GEN_THREADS);
// Pre-generate 5×5 spawn area synchronously so player has solid ground
for(int dx=-2;dx<=2;dx++)
for(int dz=-2;dz<=2;dz++){
auto c=std::make_unique<Chunk>(dx,dz);
generateChunkData(*c);
CHUNKS[{dx,dz}]=std::move(c);
}
// Build their meshes (all neighbours present so culling is correct)
for(auto& [k,c]:CHUNKS) buildChunkMesh(*c);
// Submit the rest async
requestChunks(0,0);
int totalChunks=(2*RENDER_DIST+1)*(2*RENDER_DIST+1);
int minToPlay=9; // 3×3 inner zone
Camera cam;
for(int y=WORLD_H-1;y>=0;y--){
if(getBlock(0,y,0)!=AIR&&getBlock(0,y,0)!=WATER){
cam.pos.y=y+1+PH+0.1f; break;
}
}
cam.pos.x=0.5f; cam.pos.z=0.5f;
PLAYER_MODEL.build();
glm::mat4 proj=glm::perspective(glm::radians(70.f),(float)SCREEN_W/SCREEN_H,0.05f,1000.f);
SDL_SetRelativeMouseMode(SDL_TRUE);
Inventory inv;
bool thirdPerson=false, running=true, loading=true;
Uint64 last=SDL_GetPerformanceCounter();
int lastPcx=INT_MAX, lastPcz=INT_MAX;
// FPS tracking
int fpsCounter=0;
float fpsTimer=0.f;
int fpsDisplay=0;
std::cout<<"WASD=move Space=jump LMB=break RMB=place\n"
"1-8=hotbar E=inv F5=3rd-person ESC=quit\n";
while(running){
// ── Delta time ──
Uint64 now=SDL_GetPerformanceCounter();
float dt=(float)(now-last)/SDL_GetPerformanceFrequency();
if(dt>0.1f) dt=0.1f;
last=now;
// ── FPS ──
fpsCounter++;
fpsTimer+=dt;
if(fpsTimer>=1.0f){ fpsDisplay=fpsCounter; fpsCounter=0; fpsTimer-=1.0f; }
// ── Drain finished chunks ──
drainFinishedChunks();
int readyChunks=(int)CHUNKS.size();
if(loading&&readyChunks>=minToPlay) loading=false;
// ── Events ──
SDL_Event e;
while(SDL_PollEvent(&e)){
if(e.type==SDL_QUIT) running=false;
if(e.type==SDL_KEYDOWN&&e.key.keysym.sym==SDLK_ESCAPE) running=false;
if(loading) continue;
if(e.type==SDL_KEYDOWN){
switch(e.key.keysym.sym){
case SDLK_e: inv.open=!inv.open;
SDL_SetRelativeMouseMode(inv.open?SDL_FALSE:SDL_TRUE); break;
case SDLK_F5: thirdPerson=!thirdPerson; break;
case SDLK_1: inv.selected=0; break; case SDLK_2: inv.selected=1; break;
case SDLK_3: inv.selected=2; break; case SDLK_4: inv.selected=3; break;
case SDLK_5: inv.selected=4; break; case SDLK_6: inv.selected=5; break;
case SDLK_7: inv.selected=6; break; case SDLK_8: inv.selected=7; break;
}
}
if(e.type==SDL_MOUSEWHEEL)
inv.selected=(inv.selected-e.wheel.y+HOTBAR_SIZE)%HOTBAR_SIZE;
if(!inv.open){
if(e.type==SDL_MOUSEMOTION){
cam.yaw+=e.motion.xrel*MOUSE_SENS;
cam.pitch=std::max(-89.f,std::min(89.f,cam.pitch-e.motion.yrel*MOUSE_SENS));
}
if(e.type==SDL_MOUSEBUTTONDOWN){
glm::ivec3 hit,prev;
if(raycast(cam,hit,prev)){
if(e.button.button==SDL_BUTTON_LEFT) setBlock(hit.x,hit.y,hit.z,AIR);
else if(e.button.button==SDL_BUTTON_RIGHT)
setBlock(prev.x,prev.y,prev.z,inv.selectedBlock());
}
}
} else if(e.type==SDL_MOUSEBUTTONDOWN&&e.button.button==SDL_BUTTON_LEFT){
const int COLS=4; const float SL=52,GL=6;
float iw=COLS*(SL+GL)-GL+32,ih=((NUM_BLOCK_TYPES-1+COLS-1)/COLS)*(SL+GL)-GL+60;
float ix=(SCREEN_W-iw)*.5f,iy=(SCREEN_H-ih)*.5f;
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
int row=(bi-1)/COLS,col=(bi-1)%COLS;
float sx=ix+16+col*(SL+GL),sy=iy+40+row*(SL+GL);
if(e.button.x>=sx&&e.button.x<=sx+SL&&e.button.y>=sy&&e.button.y<=sy+SL)
inv.hotbar[inv.selected]=(uint8_t)bi;
}
}
}
// ── Loading screen ──
if(loading){
drawLoadingScreen(win,readyChunks,totalChunks);
SDL_Delay(16);
continue;
}
// ── Player movement ──
if(!inv.open){
const Uint8* keys=SDL_GetKeyboardState(nullptr);
glm::vec3 fwd=cam.forward(); fwd.y=0; if(glm::length(fwd)>0) fwd=glm::normalize(fwd);
glm::vec3 rgt=cam.right(); rgt.y=0; if(glm::length(rgt)>0) rgt=glm::normalize(rgt);
glm::vec3 mv{0,0,0};
if(keys[SDL_SCANCODE_W]) mv+=fwd*MOVE_SPEED;
if(keys[SDL_SCANCODE_S]) mv-=fwd*MOVE_SPEED;
if(keys[SDL_SCANCODE_D]) mv+=rgt*MOVE_SPEED;
if(keys[SDL_SCANCODE_A]) mv-=rgt*MOVE_SPEED;
if(keys[SDL_SCANCODE_SPACE]&&cam.onGround) cam.vel.y=JUMP_VEL;
moveCamera(cam,mv,dt);
}
// ── Chunk streaming ──
int pcx=(int)floorf(cam.pos.x/CHUNK_SIZE);
int pcz=(int)floorf(cam.pos.z/CHUNK_SIZE);
if(pcx!=lastPcx||pcz!=lastPcz){
requestChunks(pcx,pcz);
evictChunks(pcx,pcz);
lastPcx=pcx; lastPcz=pcz;
}
// ── Build dirty meshes ──
processDirtyMeshes(MESH_BUILDS_PER_FRAME, pcx, pcz);
// ── 3D render ──
glClearColor(0.53f,0.81f,0.98f,1.0f);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glm::mat4 view=thirdPerson?cam.thirdPersonView():cam.firstPersonView();
glm::mat4 mvp=proj*view;
glUseProgram(prog3d);
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
int visChunks=0;
for(auto& [k,c]:CHUNKS)
if(c->meshReady&&c->indexCount>0){
glBindVertexArray(c->VAO);
glDrawElements(GL_TRIANGLES,c->indexCount,GL_UNSIGNED_INT,0);
++visChunks;
}
if(thirdPerson) PLAYER_MODEL.draw(prog3d,mvpLoc,camLoc,proj,view,cam);
// ── 2D UI ──
UI.begin();
drawHUD(cam.pos,thirdPerson,POOL.pending.load(),fpsDisplay,visChunks);
drawInventory(inv);
UI.end();
SDL_GL_SwapWindow(win);
}
POOL.shutdown();
CHUNKS.clear();
glDeleteProgram(prog3d);
glDeleteProgram(UI.prog);
glDeleteVertexArrays(1,&UI.VAO); glDeleteBuffers(1,&UI.VBO);
glDeleteVertexArrays(1,&PLAYER_MODEL.VAO);
glDeleteBuffers(1,&PLAYER_MODEL.VBO); glDeleteBuffers(1,&PLAYER_MODEL.EBO);
SDL_GL_DeleteContext(ctx);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}