summaryrefslogtreecommitdiff
path: root/src/game
diff options
context:
space:
mode:
authornavewindre <boneyaard@gmail.com>2025-09-03 20:10:09 +0200
committernavewindre <boneyaard@gmail.com>2025-09-03 20:10:09 +0200
commitf8b92ce3aa08b1445c9f956d8166830946562d12 (patch)
tree94e63a5aec9f8f52b577f56799e0c9201fd976a5 /src/game
a
Diffstat (limited to 'src/game')
-rw-r--r--src/game/assets.cpp78
-rw-r--r--src/game/assets.h23
-rw-r--r--src/game/object.cpp0
-rw-r--r--src/game/object.h73
-rw-r--r--src/game/objlist.cpp44
-rw-r--r--src/game/objlist.h122
-rw-r--r--src/game/physics/movement.h0
-rw-r--r--src/game/player.cpp82
-rw-r--r--src/game/player.h22
-rw-r--r--src/game/raycast.cpp162
-rw-r--r--src/game/raycast.h65
-rw-r--r--src/game/vars.cpp115
-rw-r--r--src/game/vars.h50
-rw-r--r--src/game/world/bsp.cpp662
-rw-r--r--src/game/world/bsp.h103
-rw-r--r--src/game/world/bsp_draw.cpp124
-rw-r--r--src/game/world/bsp_draw.h4
-rw-r--r--src/game/world/draw.cpp216
-rw-r--r--src/game/world/draw.h19
-rw-r--r--src/game/world/map.cpp640
-rw-r--r--src/game/world/map.h137
-rw-r--r--src/game/world/trace.cpp393
-rw-r--r--src/game/world/trace.h29
-rw-r--r--src/game/world/world.cpp14
-rw-r--r--src/game/world/world.h11
25 files changed, 3188 insertions, 0 deletions
diff --git a/src/game/assets.cpp b/src/game/assets.cpp
new file mode 100644
index 0000000..7d755c0
--- /dev/null
+++ b/src/game/assets.cpp
@@ -0,0 +1,78 @@
+#include "assets.h"
+
+#include "../game.h"
+#include "../render/gl_2d_font.h"
+
+void assets_init( GAME_DATA *game ) {
+ game->assets.test = gl_texture_create( game->gl, "assets/test.png" );
+ game->assets.fonts_init = 0;
+}
+
+void assets_create_fonts( struct GAME_DATA *game ) {
+ dlog( "creating fonts\n" );
+
+ game->gl->fonts.each( fn( GL_FONT** font ) {
+ gl_font_destroy( game->gl, *font );
+ });
+
+ game->gl->fonts.clear();
+ GAME_ASSETS* a = &game->assets;
+ a->font = gl_font_create( game->gl, "cour.ttf", 16 );
+ a->jpn12 = gl_font_create( game->gl, "jpn12.ttf", 12 );
+ a->jpn16 = gl_font_create( game->gl, "jpn16.ttf", 16 );
+}
+
+U8 assets_on_frame( struct GAME_DATA *game ) {
+ thread_mutex_lock( &font_mutex );
+ if( !game->assets.fonts_init ) {
+ assets_create_fonts( game );
+ game->assets.fonts_init = 1;
+ thread_mutex_unlock( &font_mutex );
+ return 0;
+ }
+
+ thread_mutex_unlock( &font_mutex );
+ return 1;
+}
+
+LIST<FILE_ENTRY> assets_get_files_by_ext_dir( const char** extensions, const char* dirname ) {
+ LIST<FILE_ENTRY> ret;
+ LIST<FILE_ENTRY> dir = dir_get_entries( dirname );
+
+ dir.each( fn( FILE_ENTRY* e ) {
+ char fullpath[256];
+ sprintf( fullpath, "%s/%s", dirname, e->name );
+ if( e->dir ) {
+ LIST<FILE_ENTRY> subdir = assets_get_files_by_ext_dir( extensions, fullpath );
+ subdir.each( fn( FILE_ENTRY* se ) { ret.push( *se ); } );
+ return;
+ }
+
+ U32 len = strlen( e->name );
+ for( U32 i = 0; !!extensions[i]; ++i ) {
+ const char* ext = extensions[i];
+ U32 extlen = strlen( ext );
+
+ if( extlen < len && e->name[len - extlen - 1] == '.' ) {
+ if( !strcmp( e->name + len - extlen, ext ) ) {
+ FILE_ENTRY ne{ .dir = 0 };
+ strcpy( ne.name, fullpath );
+ ret.push( ne );
+ }
+ }
+ }
+ } );
+
+ return ret;
+}
+
+LIST<FILE_ENTRY> assets_get_files_by_ext( const char** extensions ) {
+ return assets_get_files_by_ext_dir( extensions, "../assets" );
+}
+
+const char* assets_abspath( const char* filename ) {
+ while( !strncmp( filename, "../assets/", 10 ) )
+ filename += 10;
+
+ return filename;
+}
diff --git a/src/game/assets.h b/src/game/assets.h
new file mode 100644
index 0000000..d0fe270
--- /dev/null
+++ b/src/game/assets.h
@@ -0,0 +1,23 @@
+#pragma once
+#include "../util/typedef.h"
+#include "../util/file.h"
+
+
+typedef struct {
+ struct GL_FONT* font;
+ struct GL_FONT* jpn12;
+ struct GL_FONT* jpn16;
+ struct GL_TEX2D* test;
+
+ U8 fonts_init;
+} GAME_ASSETS;
+
+extern void assets_init( struct GAME_DATA* game );
+extern void assets_create_fonts( struct GAME_DATA* game );
+// extensions is a null-terminated array of null-terminated strings.
+extern LIST<FILE_ENTRY> assets_get_files_by_ext_dir( const char** extensions, const char* dir );
+// extensions is a null-terminated array of null-terminated strings.
+extern LIST<FILE_ENTRY> assets_get_files_by_ext( const char** extensions );
+extern const char* assets_abspath( const char* filepath );
+
+extern U8 assets_on_frame( struct GAME_DATA* game );
diff --git a/src/game/object.cpp b/src/game/object.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/game/object.cpp
diff --git a/src/game/object.h b/src/game/object.h
new file mode 100644
index 0000000..8191ab7
--- /dev/null
+++ b/src/game/object.h
@@ -0,0 +1,73 @@
+#pragma once
+#include "../util/allocator.h"
+#include "../util/vector.h"
+#include "../util/fnv.h"
+
+typedef void ( *OBJECT_DEINIT_FN )( struct OBJECT* obj );
+inline const char* obj_classid_to_name( U32 classid );
+
+enum ObjectClass_t {
+ OBJCLASS_NONE = 0,
+ OBJCLASS_WORLD,
+ OBJCLASS_TRIGGER,
+ OBJCLASS_PLAYER,
+ OBJCLASS_BASENPC,
+};
+
+static const char* OBJCLASS_NAMES[] = {
+ "none (invalid - FIXME)",
+ "world",
+ "trigger",
+ "player",
+ "base_npc",
+};
+
+// todo: come up with a good way to make these static
+struct OBJECT_PROP {
+ OBJECT_PROP( const char* name, void* value, U32 size, LIST<OBJECT_PROP*>* list, void* parent ) {
+ this->name = name;
+ this->size = size;
+ this->hash = fnv1a( name );
+ this->offset = (U32)( (U8*)value - (U8*)parent );
+ list->push( this );
+ }
+
+ const char* name;
+ FNV1A hash;
+ U32 offset;
+ U32 size;
+};
+
+#define OBJVAR( type, name ) \
+ type name; \
+ OBJECT_PROP __##name##_props{ #name, (void*)&name, sizeof(type), &this->props, this }
+
+#define OBJVAR_STR( name, len ) \
+ char name##[len]; \
+ OBJECT_PROP __##name##_props{ #name, (void*)name, len, &this->props, this }
+
+struct OBJECT {
+ static const U32 CLASSID = OBJCLASS_NONE;
+
+ U32 classid;
+ U8 keeponlevel{};
+ char name[64];
+
+ OBJECT* parent;
+ LIST<OBJECT*> children{};
+ LIST<OBJECT_PROP*> props{};
+ OBJECT_DEINIT_FN deinit_fn{};
+
+ OBJVAR( VEC3, pos );
+ OBJVAR( VEC3, rot );
+ OBJVAR( I32, idx );
+};
+
+inline const char* obj_classid_to_name( U32 classid ) {
+ if( classid > (sizeof(OBJCLASS_NAMES) / sizeof(OBJCLASS_NAMES[0])) ) {
+ dlog( "obj_classid_to_name() : invalid classid passed (%d)", classid );
+ return "INVALIDCLASSID";
+ }
+
+ return OBJCLASS_NAMES[classid];
+}
diff --git a/src/game/objlist.cpp b/src/game/objlist.cpp
new file mode 100644
index 0000000..01ce49b
--- /dev/null
+++ b/src/game/objlist.cpp
@@ -0,0 +1,44 @@
+#include "objlist.h"
+#include "world/world.h"
+#include "world/map.h"
+
+OBJECT_LIST* objl;
+
+OBJECT_LIST* objl_init() {
+ OBJECT_LIST* ret = new OBJECT_LIST;
+ ret->world = 0;
+ ret->pl = 0;
+
+ return ret;
+}
+
+void objl_clear( U8 delete_player ) {
+ objl->objects.each( fn( OBJECT** ptr ) {
+ OBJECT* obj = *ptr;
+
+ if( !delete_player && obj == objl->pl )
+ return;
+
+ if( obj->deinit_fn )
+ obj->deinit_fn( obj );
+ delete obj;
+ } );
+
+ objl->objects.clear();
+ objl->world = 0;
+ if( delete_player )
+ objl->pl = 0;
+}
+
+void objl_load_world( GAME_DATA* game, WORLD_MAP* map ) {
+ objl_clear();
+
+ objl->world = world_create( map );
+ if( !objl->pl )
+ objl->pl = player_create( map->startpos, map->startang );
+
+ if( !OK( world_populate_entities( objl->world ) ) ) {
+ objl_clear();
+ dlog( "objl_load_world() : error populating entities\n" );
+ }
+}
diff --git a/src/game/objlist.h b/src/game/objlist.h
new file mode 100644
index 0000000..f8a46d7
--- /dev/null
+++ b/src/game/objlist.h
@@ -0,0 +1,122 @@
+#pragma once
+
+#include "player.h"
+#include "world/world.h"
+
+const U32 OBJ_LIST_MAX = 8192;
+
+struct OBJECT_LIST {
+ template <typename T>
+ T* add( const char* name ) {
+ if( objects.size >= OBJ_LIST_MAX ) {
+ dlog( "OBJECT_LIST::add() : object list is full" );
+ return 0;
+ }
+
+ for( I32 i = 0; i < objects.size; ++i ) {
+ if( !strcmp( name, objects[i]->name ) ) {
+ dlog( "OBJECT_LIST::add() : object %s already exists", name );
+ return 0;
+ }
+ }
+
+ T* newt = new T;
+ newt->classid = T::CLASSID;
+ strcpy( newt->name, name );
+ objects.push( newt );
+ return newt;
+ }
+
+ template < typename T >
+ T* add( const T& obj ) {
+ for( I32 i = 0; i < objects.size; ++i ) {
+ if( !strcmp( obj.name, objects[i]->name ) ) {
+ dlog( "OBJECT_LIST::add() : object %s already exists", obj.name );
+ return 0;
+ }
+ }
+
+ T* newt = new T( obj );
+ newt->classid = T::CLASSID;
+ objects.push( newt );
+ return newt;
+ }
+
+ void remove( OBJECT* obj ) {
+ I32 idx = objects.idx_of( obj );
+ if( idx != -1 )
+ free( objects[idx] );
+ }
+
+ void remove( const char* obj ) {
+ for( I32 i = 0; i < objects.size; ++i ) {
+ if( !strcmp( obj, objects[i]->name ) ) {
+ free( objects[i] );
+ return;
+ }
+ }
+ }
+
+ template < typename T = OBJECT >
+ T* find( const char* name ) {
+ for( I32 i = 0; i < objects.size; ++i ) {
+ if( !strcmp( name, objects[i]->name ) )
+ return objects[i];
+ }
+
+ return 0;
+ }
+
+ void each( LIST<OBJECT*>::ON_EACH_FN func ) {
+ objects.each( func );
+ }
+
+ WORLD* world;
+ PLAYER* pl;
+ LIST<OBJECT*> objects;
+};
+
+extern OBJECT_LIST* objl;
+
+extern OBJECT_LIST* objl_init();
+extern void objl_clear( U8 delete_player = 0 );
+extern void objl_load_world( GAME_DATA* game, WORLD_MAP* map );
+
+
+template < typename T >
+inline T* obj_add( const char* name ) {
+ if( !objl ) {
+ dlog( "obj_add() : objl null\n" );
+ return 0;
+ }
+
+ return objl->add<T>( name );
+}
+
+template < typename T >
+inline T* obj_add( T obj ) {
+ T* newt = new T( obj );
+ newt = objl->add( newt );
+ return newt;
+}
+
+template < typename T >
+inline T* obj_add( OBJECT* obj, const char* name ) {
+ T* newt = objl->add<T>( name );
+ if( !newt )
+ return 0;
+
+ newt->parent = obj;
+ obj->children.push( newt );
+ return newt;
+}
+
+template < typename T >
+inline T* obj_add( OBJECT* obj, T newobj ) {
+ T* newt = objl->add( newobj );
+ if( !newt )
+ return 0;
+
+ newt->parent = obj;
+ obj->children.push( newt );
+}
diff --git a/src/game/physics/movement.h b/src/game/physics/movement.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/game/physics/movement.h
diff --git a/src/game/player.cpp b/src/game/player.cpp
new file mode 100644
index 0000000..87786df
--- /dev/null
+++ b/src/game/player.cpp
@@ -0,0 +1,82 @@
+#include "../game.h"
+#include "SDL_scancode.h"
+#include "objlist.h"
+#include "world/trace.h"
+#include "player.h"
+
+F32 PLAYER_HULL_HEIGHT = 50.f;
+F32 PLAYER_HULL_WIDTH = 32.f;
+
+PLAYER* player_create( VEC3 origin, F32 yaw ) {
+ PLAYER* p = obj_add<PLAYER>( "localplayer" );
+ p->pos = origin;
+ p->rot = { 0, yaw, 0 };
+ p->health = 100;
+ p->keeponlevel = 1;
+ p->velocity = {};
+
+ p->mins = {
+ -PLAYER_HULL_WIDTH * .5f,
+ -PLAYER_HULL_WIDTH * .5f,
+ 0
+ };
+
+ p->maxs = {
+ PLAYER_HULL_WIDTH * .5f,
+ PLAYER_HULL_WIDTH * .5f,
+ PLAYER_HULL_HEIGHT
+ };
+
+ return p;
+}
+
+void player_input( GAME_DATA* game, PLAYER* p ) {
+ F32* yaw = &p->rot.y;
+ VEC3* pos = &p->pos;
+
+ if( input.keys[(U8)'a'] )
+ *yaw -= 360.f * TICK_INTERVAL * 0.5f;
+ if( input.keys[(U8)'d'] )
+ *yaw += 360.f * TICK_INTERVAL * 0.5f;
+
+ if( input.keys[SDL_SCANCODE_UP] ) {
+ if( p->rot.x < 89.f )
+ p->rot.x += 360.f * TICK_INTERVAL * 0.5f;
+ }
+ if( input.keys[SDL_SCANCODE_DOWN] ) {
+ if( p->rot.x > -89.f )
+ p->rot.x -= 360.f * TICK_INTERVAL * 0.5f;
+ }
+
+ *yaw = remainderf( *yaw, 360.f );
+
+
+ VEC3 wishmove;
+ VEC3 dir = m_radial_offset( *yaw, 70.f );
+ if( input.keys[(U8)'w'] ) {
+ wishmove = dir * TICK_INTERVAL;
+ }
+ if( input.keys[(U8)'s'] ) {
+ wishmove = dir * -TICK_INTERVAL;
+ }
+
+ // todo : should never be false
+ if( vec_len( wishmove ) > BSP_TRACE_EPSILON && game->state.map->bsp ) {
+ BSP_TRACE tr{};
+ AABB aabb{};
+ tr.in_start = *pos;
+ tr.in_end = *pos + wishmove * 2.f;
+ aabb.min = p->mins;
+ aabb.max = p->maxs;
+
+ bsp_trace( &tr, game->state.map->bsp, aabb );
+ if( !tr.hit )
+ *pos += wishmove;
+ else
+ *pos += wishmove * tr.frac;
+ }
+}
+
+VEC3 player_get_view_pos( PLAYER* p ) {
+ return VEC3( p->pos.x, p->pos.y, p->pos.z + p->eyeoffset );
+}
diff --git a/src/game/player.h b/src/game/player.h
new file mode 100644
index 0000000..71b1c02
--- /dev/null
+++ b/src/game/player.h
@@ -0,0 +1,22 @@
+#pragma once
+#include "object.h"
+
+struct PLAYER_INPUT {
+
+};
+
+struct PLAYER : OBJECT {
+ static const U32 CLASSID = OBJCLASS_PLAYER;
+ U32 health;
+ F32 fov{72.f};
+ F32 eyeoffset{40.f};
+
+ VEC3 mins;
+ VEC3 maxs;
+
+ VEC3 velocity;
+};
+
+extern PLAYER* player_create( VEC3 origin, F32 yaw );
+extern void player_input( struct GAME_DATA* game, PLAYER* player );
+extern VEC3 player_get_view_pos( PLAYER* player );
diff --git a/src/game/raycast.cpp b/src/game/raycast.cpp
new file mode 100644
index 0000000..c82a3ed
--- /dev/null
+++ b/src/game/raycast.cpp
@@ -0,0 +1,162 @@
+#include <cmath>
+#include <math.h>
+
+#include "raycast.h"
+#include "../game.h"
+
+#include "../render/gl_2d.h"
+
+#include "objlist.h"
+#include "player.h"
+#include "world/world.h"
+#include "world/map.h"
+
+const U32 RAY_DEPTH_MAX = 8;
+
+U8 line_intersects( VEC3 ray_start, VEC2 ray_dir, MAP_WALL* s, VEC3* p, F32* t ) {
+ VEC2 v1 = VEC2{ ray_start.x, ray_start.y } - VEC2{ s->start.x, s->start.y };
+ VEC2 v2 = VEC2{ s->end.x, s->end.y } - VEC2{ s->start.x, s->start.y };
+ VEC2 v3 = { -ray_dir.y, ray_dir.x };
+
+ F32 dot = vec_dot( v2, v3 );
+ if( fabsf(dot) < 0.001f )
+ return 0;
+
+ F32 t1 = vec_cross( v2, v1 ) / dot;
+ F32 t2 = vec_dot( v1, v3 ) / dot;
+
+ if( t1 >= 0.0f && ( t2 >= 0.0f && t2 <= 1.0f ) ) {
+ *t = t1;
+ *p = ray_start + VEC3{ ray_dir } * (*t);
+ return 1;
+ }
+
+ return 0;
+}
+
+F32 ray_calc_wall_hit_dir( VEC3 start, VEC3 end, F32 ang ) {
+ VEC3 walld = start - end;
+ vec_normalize( &walld );
+ F32 wallang = atan2f( walld.y, walld.x );
+ wallang += ang;
+ if( wallang < 0.f ) wallang += 2.f * M_PI;
+ if( wallang > 2.f * M_PI ) wallang -= 2.f * M_PI;
+ return wallang;
+}
+
+LIST<RAY_HITDATA> ray_trace_list( VEC3 start, F32 ang, U32 max_iter ) {
+ WORLD* w = objl->world;
+ if( !w ) return {};
+ LIST<RAY_HITDATA> hits;
+
+ F32 rayang = m_deg2rad( ang );
+ VEC2 raydir = { cos( rayang ), sin( rayang ) };
+ VEC3 minp;
+
+ for( U32 i = 0; i < w->map->walls.size; ++i ) {
+ VEC3 p;
+ F32 dist;
+ if( line_intersects( start, raydir, &w->map->walls[i], &p, &dist ) ) {
+ dist = vec_dist( start, p );
+
+ VEC3 start = w->map->walls[i].start;
+ VEC3 end = w->map->walls[i].end;
+ F32 hitang = ray_calc_wall_hit_dir( start, end, ang );
+ hits.push( RAY_HITDATA{ p, dist, hitang, TH_WALL, i } );
+ if( hits.size > max_iter )
+ break;
+ }
+ }
+
+ return hits;
+}
+
+U32 ray_trace( TRACE* tr, F32 ang ) {
+ F32 rayang = m_deg2rad( ang );
+ VEC3 start = tr->startpos;
+ VEC3 raydir = { cos( rayang ), sin( rayang ), 0.f };
+ raydir *= RAY_DEPTH_MAX;
+
+ return ray_trace( tr, raydir );
+}
+
+U32 ray_trace( TRACE* tr, VEC3 end ) {
+ WORLD* w = objl->world;
+ if( !w ) return TRACE_HIT_NONE;
+ WORLD_MAP* map = w->map;
+
+ VEC3 start = tr->startpos;
+ VEC3 diff = end - start;
+ F32 trdist = vec_lensq( diff );
+ vec_normalize( &diff );
+ VEC2 raydir = { diff.x, diff.y };
+ F32 ang = atan2f( raydir.y, raydir.x );
+
+ F32 mindist = INFINITY;
+ VEC3 minp;
+ U32 hitwall = TRACE_HIT_NONE;
+
+ for( U32 i = 0; i < map->walls.size; ++i ) {
+ VEC3 p;
+ F32 dist;
+
+ if( line_intersects( start, raydir, &map->walls[i], &p, &dist ) ) {
+ if( dist <= trdist && dist < mindist ) {
+ mindist = dist;
+ minp = p;
+ hitwall = i;
+ }
+ }
+ }
+
+ if( hitwall != -1 ) {
+ tr->endpos = { minp.x, minp.y, 0.0f };
+ tr->hitwall = hitwall;
+
+ VEC3 start = map->walls[hitwall].start;
+ VEC3 end = map->walls[hitwall].end;
+ tr->hitang = ray_calc_wall_hit_dir( start, end, ang );
+ return hitwall;
+ }
+
+ tr->hitwall = -1;
+ tr->fract = -1.f;
+ tr->endpos = { start.x + RAY_DEPTH_MAX * raydir.x, start.y + RAY_DEPTH_MAX * raydir.y, 0.0f };
+ return TRACE_HIT_NONE;
+}
+
+void draw_player( GAME_DATA* game, PLAYER* pl ) {
+ GL_PROGRAM* gl2d = game->shaders.gl2d;
+ VEC2 xy = { pl->pos.x, pl->pos.y };
+
+ gl_2d_frect( gl2d, xy - VEC2{ 5.f, 5.f }, { 10.f, 10.f }, CLR::RED() );
+}
+
+void ray_draw_world2d( GAME_DATA* game, WORLD* world ) {
+ for( U32 i = 0; i < world->map->polygons.size; ++i ) {
+ MAP_POLYGON* p = &world->map->polygons[i];
+
+ LIST<VERTEX> verts{};
+ SURF_PROPS* props = polygon_get_props( world->map, p );
+ p->vertices.each( fn( MAP_VERTEX* mv ) {
+ VERTEX v{};
+ v.pos = VEC2{ mv->pos.x, mv->pos.y };
+ v.uv = mv->uv;
+ v.clr = props->clr;
+ verts.push( v );
+ } );
+
+ if( props->tex )
+ gl_textured_polygon( game->shaders.gl2d_texcoord, verts.data, verts.size, props->tex );
+ else
+ gl_polygon( game->shaders.gl2d, verts.data, verts.size );
+ }
+
+ for( U32 i = 0; i < world->map->walls.size; ++i ) {
+ MAP_WALL* s = &world->map->walls[i];
+ SURF_PROPS* p = &world->map->props[s->propid];
+ gl_2d_line( game->shaders.gl2d, VEC2( s->start.x, s->start.y ), VEC2( s->end.x, s->end.y ), p->clr );
+ }
+
+ draw_player( game, objl->pl );
+}
diff --git a/src/game/raycast.h b/src/game/raycast.h
new file mode 100644
index 0000000..5920e09
--- /dev/null
+++ b/src/game/raycast.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "../util.h"
+
+const static U32 RAY_ITER_MAX = 10;
+const static U32 TRACE_HIT_NONE = (U32)-1;
+
+enum TraceHitType_t {
+ TH_NONE,
+ TH_WALL,
+ TH_CEILING,
+ TH_FLOOR
+};
+
+enum TraceFilter_t {
+ TF_NONE = 0x0,
+ TF_WALLS = 0x1,
+ TF_SPRITES = 0x2,
+ TF_ENTITIES = 0x4,
+
+ TF_WORLD = TF_WALLS | TF_SPRITES,
+ TF_ALL = TF_WALLS | TF_SPRITES | TF_ENTITIES,
+};
+
+struct TRACE {
+ /* input */
+ VEC3 startpos;
+ /* output*/
+ VEC3 endpos;
+ /* output */
+ F32 fract;
+ /* output */
+ F32 hitang;
+ /* output */
+ U32 hitwall;
+ /* input */
+ U32 filter;
+};
+
+struct RAY_HITDATA {
+ VEC3 point;
+ F32 dist;
+ F32 ang;
+
+ U8 type;
+ U32 obj_idx;
+};
+
+extern LIST<RAY_HITDATA> ray_trace_list( VEC3 start, F32 ang, U32 max_iter = RAY_ITER_MAX );
+extern LIST<RAY_HITDATA> ray_trace_list( VEC3 start, VEC3 end, U32 max_iter = RAY_ITER_MAX );
+
+extern U32 ray_trace( TRACE* tr, F32 ang );
+extern U32 ray_trace( TRACE* tr, VEC3 end );
+extern U32 ray_trace( TRACE* tr, VEC3 start, F32 ang );
+extern U32 ray_trace( TRACE* tr, VEC3 start, VEC3 end );
+
+extern VEC2 project_vertex(
+ VEC3 vertex_pos,
+ VEC3 player_pos,
+ F32 player_angle_deg,
+ F32 fov_deg,
+ VEC2 window,
+ VEC2 winsize,
+ U8* in_view = 0
+);
diff --git a/src/game/vars.cpp b/src/game/vars.cpp
new file mode 100644
index 0000000..d7b1f9b
--- /dev/null
+++ b/src/game/vars.cpp
@@ -0,0 +1,115 @@
+#include "vars.h"
+#include "../util.h"
+#include <string.h>
+
+VAR_LIST* varl;
+
+VAR_LIST* vars_init() {
+ VAR_LIST* ret = new VAR_LIST;
+ return ret;
+}
+
+CVAR* var_new( const char* name, F32 v ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ CVAR* ret = new CVAR;
+ memset( ret, 0 , sizeof( CVAR ) );
+ ret->type = CVAR_TYPE_FLOAT;
+ strcpy( ret->name, name );
+ ret->fl_v = v;
+
+ varl->vars.push( ret );
+ return ret;
+}
+
+CVAR* var_new( const char* name, I32 i ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ CVAR* ret = new CVAR;
+ memset( ret, 0 , sizeof( CVAR ) );
+ ret->type = CVAR_TYPE_INT;
+ strcpy( ret->name, name );
+ ret->i_v = i;
+
+ varl->vars.push( ret );
+ return ret;
+}
+
+CVAR* var_new( const char* name, CLR c ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ CVAR* ret = new CVAR;
+ memset( ret, 0 , sizeof( CVAR ) );
+ ret->type = CVAR_TYPE_COLOR;
+ strcpy( ret->name, name );
+ ret->clr_v = c;
+
+ varl->vars.push( ret );
+ return ret;
+}
+
+CVAR* var_new( const char* name, CVAR_FUNC func ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ CVAR* ret = new CVAR;
+ memset( ret, 0 , sizeof( CVAR ) );
+ ret->type = CVAR_TYPE_FN;
+ strcpy( ret->name, name );
+ ret->fn_v = func;
+
+ varl->vars.push( ret );
+ return ret;
+}
+
+CVAR* var_new( const char* name, const char* v ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ CVAR* ret = new CVAR;
+ memset( ret, 0, sizeof( CVAR ) );
+ ret->type = CVAR_TYPE_STRING;
+ strcpy( ret->name, name );
+
+ varl->vars.push( ret );
+ return ret;
+}
+
+CVAR* var_find( const char* name ) {
+ if( !varl ) {
+ dlog( "var_new() : error: varl null\n" );
+ return 0;
+ }
+
+ U32 len = strlen( name );
+ for( U32 i = 0; i < varl->vars.size; ++i ) {
+ U32 len2 = strlen( varl->vars[i]->name );
+ if( strncmp( varl->vars[i]->name, name, min( len, len2 ) ) == 0 ) {
+ return varl->vars[i];
+ }
+ }
+
+ return 0;
+}
+
+I32 var_call( CVAR *cmdvar, const char *cmdline ) {
+ if( cmdvar->type != CVAR_TYPE_FN ) {
+ dlog( "var_call() : error: not a function\n" );
+ return -1;
+ }
+
+ return cmdvar->fn_v( cmdvar, cmdline );
+}
diff --git a/src/game/vars.h b/src/game/vars.h
new file mode 100644
index 0000000..395c470
--- /dev/null
+++ b/src/game/vars.h
@@ -0,0 +1,50 @@
+#pragma once
+#include "../util.h"
+
+enum CvarTypes_t {
+ CVAR_TYPE_INT,
+ CVAR_TYPE_FLOAT,
+ CVAR_TYPE_COLOR,
+ CVAR_TYPE_STRING,
+ CVAR_TYPE_FN,
+ CVAR_TYPE_LAST
+};
+
+typedef I32(*CVAR_FUNC)( struct CVAR* self, const char* cmdline );
+
+struct CVAR {
+ char name[100];
+ U8 type;
+ union {
+ F32 fl_v;
+ I32 i_v;
+ CLR clr_v;
+ CVAR_FUNC fn_v;
+ char str_v[64];
+ };
+
+ union {
+ F32 fl_v;
+ I32 i_v;
+ CLR clr_v;
+ CVAR_FUNC fn_v;
+ char str_v[64];
+ } default_v;
+};
+
+struct VAR_LIST {
+ LIST<CVAR*> vars;
+};
+
+extern VAR_LIST* vars_init();
+
+extern CVAR* var_new( const char* name, F32 v );
+extern CVAR* var_new( const char* name, I32 v );
+extern CVAR* var_new( const char* name, CLR v );
+extern CVAR* var_new( const char* name, const char* v );
+extern CVAR* var_new( const char* name, CVAR_FUNC fn );
+
+extern CVAR* var_find( const char* name );
+extern I32 var_call( CVAR* cmdvar, const char* cmdline );
+
+extern VAR_LIST* varl;
diff --git a/src/game/world/bsp.cpp b/src/game/world/bsp.cpp
new file mode 100644
index 0000000..9aa94d7
--- /dev/null
+++ b/src/game/world/bsp.cpp
@@ -0,0 +1,662 @@
+#include "bsp.h"
+#include "../../util/math.h"
+#include "map.h"
+#include <climits>
+
+const F32 BSP_PORTAL_PLANE_SIZE = 64000.f;
+
+struct MAP_TRI {
+ MAP_VERTEX p0, p1, p2;
+};
+
+void bsp_face_gen_render_verts( BSP* bsp, BSP_FACE* f ) {
+ LIST<VERTEX3D> vertices;
+ vertices.reserve( f->verts.size );
+
+ SURF_PROPS* props = map_get_props( bsp->map, f->propid );
+
+ f->verts.each( fn( MAP_VERTEX* v ) {
+ VERTEX3D v3d;
+ v3d.pos = { v->pos.x, v->pos.z, v->pos.y };
+ v3d.uv = v->uv;
+ v3d.clr = v->clr * props->clr;
+ v3d.sampler = 0;
+ vertices.push( v3d );
+ } );
+
+ f->render_verts = triangle_fan_to_list( vertices.data, vertices.size );
+}
+
+void bsp_gen_render_vertices( BSP* bsp ) {
+ bsp->faces.each( fn( BSP_FACE* f ) {
+ bsp_face_gen_render_verts( bsp, f );
+ } );
+}
+
+LIST<BSP_PLANE> bsp_map_get_wall_planes( WORLD_MAP* map ) {
+ LIST<BSP_PLANE> ret{};
+
+ auto calc_wall_plane = fn( MAP_WALL* w ) {
+ VEC3 dir = w->end - w->start;
+ VEC3 normal = vec_normalize( { -dir.y, dir.x, 0.0f } );
+ F32 dist = vec_dot( normal, w->start );
+
+ BSP_PLANE p = { normal, dist };
+ for( U32 i = 0; i < ret.size; ++i ) {
+ if( ret[i] == p )
+ return;
+ }
+
+ ret.push( p );
+ };
+
+ map->walls.each( calc_wall_plane );
+ for( U32 i = 0; i < 4; ++i )
+ calc_wall_plane( &map->skybox.walls[i] );
+
+ return ret;
+}
+
+
+LIST<MAP_TRI> bsp_map_triangulate_planes( WORLD_MAP* map ) {
+ LIST<MAP_TRI> ret;
+
+ auto triangulate_plane = fn( MAP_POLYGON* p ) {
+ MAP_VERTEX* start = &p->vertices[0];
+ MAP_VERTEX* last = &p->vertices[1];
+ for( U32 i = 2; i < p->vertices.size; ++i ) {
+ MAP_TRI tri = MAP_TRI{
+ *start,
+ *last,
+ p->vertices[i]
+ };
+
+ last = &p->vertices[i];
+ ret.push( tri );
+ }
+ };
+
+ map->polygons.each( triangulate_plane );
+ triangulate_plane( &map->skybox.polygons[0] );
+ triangulate_plane( &map->skybox.polygons[1] );
+
+ return ret;
+}
+
+LIST<BSP_PLANE> bsp_map_get_tri_planes( WORLD_MAP* map ) {
+ LIST<BSP_PLANE> ret;
+ LIST<MAP_TRI> tris = bsp_map_triangulate_planes( map );
+
+ tris.each( fn( MAP_TRI* tri ) {
+ VEC3 cross = vec_cross( tri->p1.pos - tri->p0.pos, tri->p2.pos - tri->p0.pos );
+ VEC3 normal = vec_normalize( cross );
+ F32 dist = vec_dot( normal, tri->p0.pos );
+
+ BSP_PLANE p = { normal, dist };
+ for( U32 i = 0; i < ret.size; ++i ) {
+ if( ret[i] == p )
+ return;
+ }
+
+ ret.push( p );
+ } );
+
+ return ret;
+}
+
+LIST<BSP_PLANE> bsp_map_get_planes( WORLD_MAP* map ) {
+ LIST<BSP_PLANE> ret;
+
+ LIST<BSP_PLANE> walls = bsp_map_get_wall_planes( map );
+ LIST<BSP_PLANE> floors = bsp_map_get_tri_planes( map );
+
+ ret.push_list( walls );
+ ret.push_list( floors );
+
+ // probably unnecessary, slow as fuck
+ // remove any dupes in case a polygon ends up the same as a wall
+ // unlikely but dont wanna run into the edge case
+ // this wont run that much anyway
+ // maybe remove later
+ for( U32 i = 0; i < ret.size; ++i ) {
+ BSP_PLANE* p = &ret[i];
+ for( U32 i2 = 0; i2 < ret.size; ++i2 ) {
+ if( i == i2 )
+ continue;
+
+ if( ret.data[i2] == *p ) {
+ ret.erase( i );
+ --i;
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+LIST<BSP_FACE> bsp_map_faces_from_walls( WORLD_MAP* map ) {
+ LIST<BSP_FACE> ret{};
+
+ auto triangulate_wall = fn( MAP_WALL* w ) {
+ SURF_PROPS* p = wall_get_props( map, w );
+ VEC3 points[] = {
+ { w->start.x, w->start.y, w->start.z },
+ { w->end.x, w->end.y, w->start.z },
+ { w->end.x, w->end.y, w->start.z + w->end.z },
+ { w->start.x, w->start.y, w->start.z + w->end.z },
+ };
+
+ VEC2 uvs[] = {
+ { w->uvstart.x, 1.f - w->uvend.y },
+ { 1.f - w->uvend.x, 1.f - w->uvend.y },
+ { 1.f - w->uvend.x, w->uvstart.y },
+ { w->uvstart.x, w->uvstart.y },
+ };
+
+ MAP_VERTEX vertices[4];
+ for( U32 i = 0; i < 4; ++i ) {
+ vertices[i] = {
+ .pos = points[i],
+ .uv = uvs[i],
+ .clr = p->clr,
+ };
+ }
+
+ BSP_FACE f1{ .propid = w->propid };
+ f1.verts.push( vertices[2] );
+ f1.verts.push( vertices[1] );
+ f1.verts.push( vertices[0] );
+
+ BSP_FACE f2{ .propid = w->propid };
+ f2.verts.push( vertices[3] );
+ f2.verts.push( vertices[2] );
+ f2.verts.push( vertices[0] );
+
+ ret.push( f1 );
+ ret.push( f2 );
+ };
+
+ map->walls.each( triangulate_wall );
+ for( U32 i = 0; i < 4; ++i ) triangulate_wall( &map->skybox.walls[i] );
+
+ return ret;
+}
+
+LIST<BSP_FACE> bsp_map_faces_from_planes( WORLD_MAP* map ) {
+ LIST<BSP_FACE> ret;
+
+ auto triangulate_plane = fn( MAP_POLYGON* p ) {
+ MAP_VERTEX* start = &p->vertices[0];
+ MAP_VERTEX* last = &p->vertices[1];
+ for( U32 i = 2; i < p->vertices.size; ++i ) {
+ BSP_FACE f = { .propid = p->propid };
+ f.verts.push( *start );
+ f.verts.push( *last );
+ f.verts.push( p->vertices[i] );
+
+ ret.push( f );
+ last = &p->vertices[i];
+ }
+ };
+
+ map->polygons.each( triangulate_plane );
+ triangulate_plane( &map->skybox.polygons[0] );
+ triangulate_plane( &map->skybox.polygons[1] );
+
+ return ret;
+}
+
+LIST<BSP_FACE> bsp_map_get_faces( WORLD_MAP* map ) {
+ LIST<BSP_FACE> ret;
+
+ LIST<BSP_FACE> walls = bsp_map_faces_from_walls( map );
+ LIST<BSP_FACE> floors = bsp_map_faces_from_planes( map );
+
+ ret.emplace_list( walls );
+ ret.emplace_list( floors );
+
+ return ret;
+}
+
+inline I32 point_classify( const BSP_PLANE& plane, const VEC3& point ) {
+ F32 side = bsp_plane_dist( plane, point );
+ if( side > BSP_NORM_EPSILON )
+ return BSP_SIDE_FRONT;
+ else if( side < -BSP_NORM_EPSILON )
+ return BSP_SIDE_BACK;
+ else
+ return BSP_SIDE_ON;
+}
+
+inline I32 polygon_classify(
+ const BSP_PLANE& plane,
+ const BSP_FACE* face,
+ I32* out_fr = 0,
+ I32* out_bk = 0
+) {
+ I32 cf = 0, cb = 0;
+ for( U32 i = 0; i < face->verts.size; ++i ) {
+ I32 side = point_classify( plane, face->verts.data[i].pos );
+ if( side & BSP_SIDE_FRONT )
+ ++cf;
+ else if( side & BSP_SIDE_BACK )
+ ++cb;
+ }
+
+ if( out_fr )
+ *out_fr = cf;
+ if( out_bk )
+ *out_bk = cb;
+
+ if( cf && cb ) return BSP_SIDE_SPAN;
+ if( cf ) return BSP_SIDE_FRONT;
+ if( cb ) return BSP_SIDE_BACK;
+ return BSP_SIDE_ON;
+}
+
+STAT polygon_split( const BSP_PLANE& plane, const BSP_FACE* in, BSP_FACE* out_fr, BSP_FACE* out_bk ) {
+ LIST<MAP_VERTEX> front, back;
+ STAT stat = STAT_ERR;
+
+ for( U32 i = 0; i < in->verts.size; ++i ) {
+ U32 i2 = (i+1) % in->verts.size;
+ MAP_VERTEX* a = &in->verts.data[i];
+ MAP_VERTEX* c = &in->verts.data[i2];
+
+ F32 da = bsp_plane_dist( plane, a->pos );
+ F32 dc = bsp_plane_dist( plane, c->pos );
+
+ U8 afront = da > BSP_NORM_EPSILON;
+ U8 aback = da < -BSP_NORM_EPSILON;
+ U8 cfront = dc > BSP_NORM_EPSILON;
+ U8 cback = dc < -BSP_NORM_EPSILON;
+
+ if( !aback ) front.push( *a );
+ if( !afront ) back.push( *a );
+
+ if( (afront && cback) || (aback && cfront) ) {
+ F32 t = m_clamp( da / (da - dc), 0.f, 1.f );
+ VEC3 isec = a->pos + (c->pos - a->pos) * t;
+ VEC2 uv = a->uv + (c->uv - a->uv) * t;
+ CLR clr = a->clr + (c->clr - a->clr) * t;
+ MAP_VERTEX v = { .pos = isec, .uv = uv, .clr = clr };
+ front.push( v );
+ back.push( v );
+ }
+ }
+
+ if( front.size > 2 ) {
+ out_fr->propid = in->propid;
+ out_fr->verts = front;
+ stat = STAT_OK;
+ }
+ if( back.size > 2 ) {
+ out_bk->propid = in->propid;
+ out_bk->verts = back;
+ stat = STAT_OK;
+ }
+
+ return stat;
+}
+
+I32 bsp_splitter_cost( const BSP_PLANE& plane, const LIST<BSP_FACE>& faces ) {
+ I32 splits = 0, front = 0, back = 0;
+ for( U32 i = 0; i < faces.size; ++i ) {
+ I32 side = polygon_classify( plane, &faces.data[i], 0, 0 );
+ if( side == BSP_SIDE_SPAN ) ++splits;
+ else if( side == BSP_SIDE_FRONT ) ++front;
+ else if( side == BSP_SIDE_BACK ) ++back;
+ else { ++front; ++back; }
+ }
+
+ I32 diff = ( front > back ) ? front - back : back - front;
+ return splits * 8 + diff;
+}
+
+I32 bsp_calc_splitter_index( const LIST<BSP_PLANE>& planes, const LIST<BSP_FACE>& faces ) {
+ I32 best = -1;
+ I32 bestc = INT_MAX;
+
+ for( U32 i = 0; i < planes.size; ++i ) {
+ I32 cost = bsp_splitter_cost( planes.data[i], faces );
+ if( cost < bestc ) {
+ best = i;
+ bestc = cost;
+ }
+ }
+
+ return best;
+}
+
+U8 bsp_is_convex_cluster( const LIST<BSP_FACE>& faces ) {
+ for( U32 i = 0; i < faces.size; ++i ) {
+ VEC3 e1 = faces.data[i].verts[1].pos - faces.data[i].verts[0].pos;
+ VEC3 e2 = faces.data[i].verts[2].pos - faces.data[i].verts[0].pos;
+ VEC3 norm = vec_normalize( vec_cross( e1, e2 ) );
+ F32 dist = vec_dot( norm, faces.data[i].verts[0].pos );
+ BSP_PLANE plane{ norm, dist };
+ for( U32 i2 = 0; i2 < faces.size; ++i2 ) {
+ if( i == i2 )
+ continue;
+
+ if( polygon_classify( plane, &faces.data[i2], 0, 0 ) == BSP_SIDE_SPAN )
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+I32 bsp_insert_leaf( BSP* bsp, LIST<BSP_FACE>& faces ) {
+ BSP_LEAF leaf{};
+ leaf.first = bsp->faces.size;
+ leaf.count = faces.size;
+ leaf.cluster = -1; // todo: pvs
+ bsp->faces.push_list( faces );
+ bsp->leaves.push( leaf );
+ return ~( (I32)bsp->leaves.size - 1 );
+}
+
+I32 bsp_build_nodes(
+ BSP* bsp,
+ LIST<BSP_PLANE>& planes,
+ LIST<BSP_FACE>& faces,
+ I32 depth ) {
+ if( faces.size == 0 )
+ return bsp_insert_leaf( bsp, faces );
+ if( depth > BSP_DEPTH_MAX || bsp_is_convex_cluster( faces ) )
+ return bsp_insert_leaf( bsp, faces );
+
+ I32 splitter_idx = bsp_calc_splitter_index( planes, faces );
+ if( splitter_idx < 0 ) return bsp_insert_leaf( bsp, faces );
+
+ BSP_PLANE split = planes.data[splitter_idx];
+
+ LIST<BSP_FACE> front, back, coplane;
+ for( U32 i = 0; i < faces.size; ++i ) {
+ BSP_FACE* f = &faces.data[i];
+ I32 frontc = 0, backc = 0;
+ I32 side = polygon_classify( split, f, &frontc, &backc );
+ if( side == BSP_SIDE_FRONT )
+ front.push( *f );
+ else if( side == BSP_SIDE_BACK )
+ back.push( *f );
+ else if( side == BSP_SIDE_ON )
+ coplane.push( *f );
+ else {
+ BSP_FACE fr{}, bk{};
+ if( OK( polygon_split( split, f, &fr, &bk ) ) ) {
+ if( fr.verts.size > 2 ) front.push( fr );
+ if( bk.verts.size > 2 ) back.push( bk );
+ }
+ }
+ }
+
+ BSP_NODE node{};
+ node.plane = split;
+ I32 nidx = bsp->nodes.size;
+ bsp->nodes.push( node );
+
+ LIST<BSP_PLANE> child_planes;
+ child_planes.reserve( planes.size > 0 ? planes.size - 1 : 0 );
+ for( I32 i = 0; i < planes.size; ++i ) {
+ if( i != splitter_idx ) child_planes.push( planes.data[i] );
+ }
+
+ I32 front_child = bsp_build_nodes( bsp, child_planes, front, depth + 1 );
+ if( coplane.size ) {
+ I32 coplane_leaf = bsp_insert_leaf( bsp, coplane );
+ BSP_NODE glue{};
+ glue.plane = split;
+ glue.front = front_child;
+ glue.back = coplane_leaf;
+ front_child = bsp->nodes.size;
+ bsp->nodes.push( glue );
+ }
+
+ I32 back_child = bsp_build_nodes( bsp, child_planes, back, depth + 1 );
+
+ bsp->nodes.data[nidx].front = front_child;
+ bsp->nodes.data[nidx].back = back_child;
+ return nidx;
+}
+
+inline void bsp_plane_basis( const VEC3& norm, VEC3* u, VEC3* v ) {
+ VEC3 a = fabsf( norm.x ) > 0.9f ? VEC3{ 0, 1, 0 } : VEC3{ 1, 0, 0 };
+ *u = vec_normalize( vec_cross( a, norm ) );
+ *v = vec_normalize( vec_cross( norm, *v ) );
+}
+
+BSP_WINDING bsp_winding_create( BSP_PLANE* plane ) {
+ BSP_WINDING w{};
+ VEC3 n = plane->normal, nu = n * -1.f;
+ VEC3 u, v;
+ bsp_plane_basis( n, &u, &v );
+
+ VEC3 c = n * plane->dist;
+
+ F32 s = BSP_PORTAL_PLANE_SIZE;
+ w.points.reserve( 4 );
+ w.points.push( c + ( u + v ) * s );
+ w.points.push( c + ( nu + v ) * s );
+ w.points.push( c + ( nu - v ) * s );
+ w.points.push( c + ( u - v ) * s );
+
+ return w;
+}
+
+
+F32 signed_dist_point_to_plane( const BSP_PLANE& plane, const VEC3& point ) {
+ return vec_dot( plane.normal, point ) - plane.dist;
+}
+
+U8 bsp_clip_winding( BSP_WINDING* w, const BSP_PLANE& plane, U8 keep_front ) {
+ if( w->points.size < 3 )
+ return 0;
+
+ LIST<VEC3> out;
+ out.reserve( w->points.size + 2 );
+
+ for( U32 i = 0; i < w->points.size; ++i ) {
+ VEC3* a = &w->points.data[i];
+ VEC3* b = &w->points.data[(i + 1) % w->points.size];
+
+ F32 da = signed_dist_point_to_plane( plane, *a );
+ F32 db = signed_dist_point_to_plane( plane, *b );
+
+ U8 ain = keep_front ? ( da >= -BSP_NORM_EPSILON ) : ( da <= BSP_NORM_EPSILON );
+ U8 bin = keep_front ? ( db >= -BSP_NORM_EPSILON ) : ( db <= BSP_NORM_EPSILON );
+
+ if( ain )
+ out.push( *a );
+
+ if( ain != bin ) {
+ F32 t = da / (da - db);
+ t = m_clamp( t, 0.f, 1.f );
+ VEC3 p = *a + (*b - *a) * t;
+ out.push( p );
+ }
+ }
+
+ if( out.size < 3 ) {
+ w->points.clear();
+ return 0;
+ }
+
+ w->points = out;
+ return 1;
+}
+
+U8 bsp_clip_winding_to_set( BSP_WINDING* w, LIST<BSP_PLANE>* planes ) {
+ for( U32 i = 0; i < planes->size; ++i ) {
+ if( !bsp_clip_winding( w, planes->data[i], 1 ) )
+ return 0;
+ }
+
+ return 1;
+}
+
+U8 bsp_winding_intersect_plane( BSP_PLANE* plane, BSP_WINDING* wa, BSP_WINDING* wb, BSP_WINDING* out ) {
+ if( wa->points.size < 3 || wb->points.size < 3 )
+ return 0;
+
+ BSP_WINDING w = *wa;
+
+ for( U32 i = 0; i < wb->points.size; ++i ) {
+ VEC3* b1 = &wb->points[i];
+ VEC3* b2 = &wb->points[(i + 1) % wb->points.size];
+
+ VEC3 edge = *b2 - *b1;
+
+ VEC3 inward_norm = vec_normalize( vec_cross( plane->normal, edge ) );
+ F32 d = vec_dot( inward_norm, *b1 );
+
+ BSP_PLANE horip{ inward_norm, d };
+ if( !bsp_clip_winding( &w, horip, 1 ) )
+ return 0;
+ }
+
+ *out = w;
+ return 1;
+}
+
+void bsp_gather_volumes_for_node(
+ BSP* bsp,
+ I32 node_idx,
+ const LIST<BSP_PLANE>& parents,
+ U8 go_front,
+ LIST<BSP_PLANE>* out
+) {
+ *out = parents;
+ BSP_NODE* n = &bsp->nodes.data[node_idx];
+ BSP_PLANE newp = n->plane;
+ if( !go_front ) {
+ newp.normal = newp.normal * -1;
+ newp.dist = -newp.dist;
+ }
+
+ out->push( newp );
+}
+
+void bsp_clip_portal_to_subtree(
+ BSP* bsp,
+ I32 idx,
+ BSP_WINDING w,
+ U8 in_front,
+ LIST<BSP_PORTAL_NODE>* nodes
+) {
+ if( bsp_is_leaf( idx ) ) {
+ BSP_PORTAL_NODE newn{ .leaf = bsp_leaf_index( idx ), .w = w };
+ nodes->push( newn );
+ return;
+ }
+
+ BSP_NODE* n = &bsp->nodes.data[idx];
+ BSP_WINDING wf = w, wb = w;
+
+ if( !bsp_clip_winding( in_front ? &wf : &wb, n->plane, in_front ) )
+ return;
+
+ if( in_front ) {
+ bsp_clip_portal_to_subtree( bsp, n->front, wf, 1, nodes );
+ } else {
+ bsp_clip_portal_to_subtree( bsp, n->back, wb, 0, nodes );
+ }
+}
+
+void bsp_build_portals( BSP* bsp ) {
+ if( bsp->root < 0 )
+ return;
+
+ LIST<BSP_PORTAL_STACK_FRAME> stack;
+ stack.push( BSP_PORTAL_STACK_FRAME{ bsp->root, LIST<BSP_PLANE>{} } );
+
+ // todo: multithread this
+ while( stack.size ) {
+ BSP_PORTAL_STACK_FRAME frame = stack.data[stack.size - 1];
+ stack.pop();
+
+ if( bsp_is_leaf( frame.idx ) )
+ continue;
+
+ BSP_NODE* n = &bsp->nodes.data[frame.idx];
+
+ BSP_WINDING wind = bsp_winding_create( &n->plane );
+ BSP_WINDING bounded = wind;
+ if( bsp_clip_winding_to_set( &bounded, &frame.planes ) ) {
+ LIST<BSP_PORTAL_NODE> fr_nodes, bk_nodes;
+
+ bsp_clip_portal_to_subtree( bsp, n->front, bounded, 1, &fr_nodes );
+ bsp_clip_portal_to_subtree( bsp, n->back, bounded, 1, &bk_nodes );
+
+ for( U32 ifr = 0; ifr < fr_nodes.size; ++ifr ) {
+ for( U32 ibk = 0; ibk < bk_nodes.size; ++ibk ) {
+ BSP_WINDING intersect;
+ if( bsp_winding_intersect_plane( &n->plane, &fr_nodes.data[ifr].w, &bk_nodes.data[ibk].w, &intersect ) ) {
+ if( intersect.points.size > 3 ) {
+ BSP_PORTAL p;
+ p.w = intersect;
+ p.plane = n->plane;
+ p.front = bsp_leaf_index( fr_nodes.data[ifr].leaf );
+ p.back = bsp_leaf_index( bk_nodes.data[ibk].leaf );
+ bsp->portals.push( p );
+ }
+ }
+ }
+ }
+ }
+
+ LIST<BSP_PLANE> fr, bk;
+ bsp_gather_volumes_for_node( bsp, frame.idx, frame.planes, 1, &fr );
+ bsp_gather_volumes_for_node( bsp, frame.idx, frame.planes, 0, &bk );
+
+ BSP_NODE* n1 = &bsp->nodes.data[frame.idx];
+ if( !bsp_is_leaf( n1->front ) ) stack.push( BSP_PORTAL_STACK_FRAME{ n1->front, fr } );
+ if( !bsp_is_leaf( n1->back ) ) stack.push( BSP_PORTAL_STACK_FRAME{ n1->back, bk } );
+ }
+
+ for( U32 i = 0; i < bsp->leaves.size; ++i ) {
+ bsp->leaves.data[i].cluster = (I32)i;
+ }
+}
+
+void pvs_set( BSP_BITSET* pvs, U32 count, U32 cluster, U32 seen ) {
+ U32 wp = (count + 31) / 32;
+ U32 base = cluster * wp;
+
+ U32 w = seen / 32, b = seen % 32;
+ while( base + w >= pvs->size ) pvs->push( 0 );
+ pvs->data[base + w] |= (1u << b);
+}
+
+U8 pvs_get( BSP_BITSET* pvs, U32 count, U32 cluster, U32 seen ) {
+ U32 wp = (count + 31) / 32;
+ U32 base = cluster * wp;
+
+ U32 w = seen / 32, b = seen % 32;
+ if( base + w >= pvs->size ) return 0;
+
+ return ( pvs->data[base + w] >> b) & 1u;
+}
+
+BSP* bsp_build_map( WORLD_MAP* m ) {
+ BSP* bsp = new BSP;
+ bsp->map = m;
+ LIST<BSP_FACE> faces = bsp_map_get_faces( m );
+ LIST<BSP_PLANE> planes = bsp_map_get_planes( m );
+
+ bsp->root = bsp_build_nodes( bsp, planes, faces, 0 );
+ U32 vertc = 0;
+ m->polygons.each( fn( MAP_POLYGON* p ) { vertc += p->vertices.size; } );
+ for( U32 i = 0; i < bsp->faces.size; ++i ) {
+ bsp->faces.data[i].id = i;
+ }
+ bsp_gen_render_vertices( bsp );
+ return bsp;
+}
+
+void bsp_free( BSP* bsp ) {
+ delete bsp;
+}
+
diff --git a/src/game/world/bsp.h b/src/game/world/bsp.h
new file mode 100644
index 0000000..439fce1
--- /dev/null
+++ b/src/game/world/bsp.h
@@ -0,0 +1,103 @@
+#pragma once
+
+#include "map.h"
+#include "../../render/gl_3d.h"
+
+const F32 BSP_NORM_EPSILON = 0.0001f;
+const F32 BSP_DIST_EPSILON = 0.01f;
+const I32 BSP_DEPTH_MAX = 1024;
+struct BSP_PLANE {
+ VEC3 normal;
+ F32 dist;
+
+ const bool operator==( const BSP_PLANE& other ) const {
+ return fabsf( normal.x - other.normal.x ) < BSP_NORM_EPSILON &&
+ fabsf( normal.y - other.normal.y ) < BSP_NORM_EPSILON &&
+ fabsf( normal.z - other.normal.z ) < BSP_NORM_EPSILON &&
+ fabsf( dist - other.dist ) < BSP_DIST_EPSILON;
+ }
+};
+
+enum BspSide_t {
+ BSP_SIDE_FRONT = 1,
+ BSP_SIDE_BACK = 2,
+ BSP_SIDE_ON = 4,
+ BSP_SIDE_SPAN = 8,
+};
+
+struct BSP_FACE {
+ I32 propid;
+ I32 id;
+ LIST<MAP_VERTEX> verts{};
+ LIST<VERTEX3D> render_verts{};
+};
+
+struct BSP_NODE {
+ BSP_PLANE plane;
+ I32 front;
+ I32 back;
+};
+
+struct BSP_LEAF {
+ U32 first;
+ U32 count;
+ I32 cluster;
+};
+
+struct BSP_WINDING {
+ LIST<VEC3> points;
+};
+
+struct BSP_PORTAL {
+ BSP_WINDING w; // winding
+ BSP_PLANE plane;
+ I32 front;
+ I32 back;
+};
+
+struct BSP_CLUSTER {
+ I32 first;
+ I32 count;
+ I32 pvs_off;
+};
+
+struct BSP_PORTAL_STACK_FRAME {
+ I32 idx;
+ LIST<BSP_PLANE> planes;
+};
+
+struct BSP_PORTAL_NODE {
+ I32 leaf;
+ BSP_WINDING w;
+};
+
+struct BSP_BITSET : public LIST<U32> {};
+
+struct BSP {
+ LIST<BSP_NODE> nodes;
+ LIST<BSP_LEAF> leaves;
+ LIST<BSP_FACE> faces;
+
+ // >= 0 = node, < 0 = leaf
+ I32 root;
+ WORLD_MAP* map;
+
+ LIST<BSP_PORTAL> portals;
+ LIST<BSP_CLUSTER> clusters;
+ BSP_BITSET pvs_bits;
+};
+
+extern LIST<BSP_PLANE> bsp_map_get_planes( WORLD_MAP* map );
+extern LIST<BSP_FACE> bsp_map_get_faces( WORLD_MAP* map );
+extern BSP* bsp_build_map( WORLD_MAP* map );
+extern void bsp_free( BSP* bsp );
+
+
+inline U8 bsp_is_leaf( I32 child ) { return child < 0; }
+inline I32 bsp_leaf_index( I32 child ) { return ~child; }
+inline F32 bsp_plane_dist( const BSP_PLANE& p, const VEC3& v ) { return vec_dot( p.normal, v ) - p.dist; }
+inline SURF_PROPS* bsp_face_get_props( WORLD_MAP* w, BSP_FACE* s ) {
+ return s->propid < 0 ?
+ map_props_get_special( w, s->propid ) :
+ &w->props[s->propid];
+}
diff --git a/src/game/world/bsp_draw.cpp b/src/game/world/bsp_draw.cpp
new file mode 100644
index 0000000..30e6731
--- /dev/null
+++ b/src/game/world/bsp_draw.cpp
@@ -0,0 +1,124 @@
+#include "bsp_draw.h"
+#include "../../game.h"
+#include "../../render/gl_3d.h"
+#include "../objlist.h"
+#include "map.h"
+#include "trace.h"
+
+
+void bsp_draw_leaf( BSP* bsp, GAME_DATA* game, I32 leafidx ) {
+ BSP_LEAF* leaf = &bsp->leaves.data[leafidx];
+ // todo : handle transparency
+
+ for( U32 i = 0; i < leaf->count; ++i ) {
+ BSP_FACE* f = &bsp->faces.data[leaf->first + i];
+ U8 face_transparent = 0;
+ if( !face_transparent ) {
+ LIST<VERTEX3D> verts = f->render_verts;
+ SURF_PROPS* props = bsp_face_get_props( game->state.map, f );
+ gl_3d_polygon( game->render.batch_3d, verts.data, verts.size, props->tex );
+ }
+ }
+
+ // todo : sort by depth
+ for( U32 i = 0; i < leaf->count; ++i ) {
+ BSP_FACE* f = &bsp->faces.data[leaf->first + i];
+ U8 face_transparent = 0;
+ if( face_transparent ) {
+ LIST<VERTEX3D> verts = f->render_verts;
+ SURF_PROPS* props = bsp_face_get_props( game->state.map, f );
+ gl_3d_polygon( game->render.batch_3d, verts.data, verts.size, props->tex );
+ }
+ }
+}
+
+U8 leaf_visible( I32 leaf_idx ) {
+ // todo : pvs
+ return 1;
+}
+
+I32 bsp_point_leaf( BSP* bsp, const VEC3& p ) {
+ I32 n = bsp->root;
+ for( ;; ) {
+ if( bsp_is_leaf( n ) ) return bsp_leaf_index( n );
+ BSP_NODE* node = &bsp->nodes.data[n];
+ F32 dist = bsp_plane_dist( node->plane, p );
+ n = (dist >= 0.f)? node->front : node->back;
+ }
+}
+
+void bsp_traverse_draw( BSP* bsp, GAME_DATA* game, I32 node, VEC3 campos ) {
+ if( bsp_is_leaf( node ) ) {
+ I32 i = bsp_leaf_index( node );
+ return bsp_draw_leaf( bsp, game, i );
+ }
+
+ BSP_NODE* n = &bsp->nodes.data[node];
+ F32 dist = bsp_plane_dist( n->plane, campos );
+
+ I32 near = (dist >= 0.f)? n->front : n->back;
+ I32 far = (dist >= 0.f)? n->back : n->front;
+
+ // todo : set up vertex buffer, draw all in one call
+ // todo todo : batch different shader calls separately
+
+ bsp_traverse_draw( bsp, game, near, campos );
+ bsp_traverse_draw( bsp, game, far, campos );
+}
+
+void bsp_draw_sprites( BSP* bsp, GAME_DATA* game ) {
+ LIST<MAP_SPRITE>* sprites = &game->state.map->sprites;
+ for( U32 i = 0; i < sprites->size; ++i ) {
+ MAP_SPRITE* sprite = &sprites->data[i];
+ BSP_TRACE t;
+ t.in_start = player_get_view_pos( objl->pl );
+ t.in_end = sprite->pos;
+
+ bsp_trace( &t, bsp );
+ if( !t.hit ) {
+ VEC3 angle = vec_angles( sprite->pos, t.in_start );
+ gl_3d_plane(
+ game->shaders.gl3d,
+ sprite->pos,
+ sprite->size,
+ { -angle.x + 90.f, angle.y - 90.f },
+ sprite->tex,
+ sprite->clr
+ );
+ }
+ }
+}
+
+void bsp_draw( BSP* bsp, GAME_DATA* game, VEC2 window, VEC2 winsize ) {
+ gl_set_viewport( game->gl, window, winsize );
+ PLAYER* pl = objl->pl;
+ F32 fov = pl->fov;
+ gl_3d_projection_setup(
+ game->shaders.gl3d,
+ { pl->pos.x, pl->pos.y, pl->pos.z + pl->eyeoffset },
+ fov,
+ pl->rot.y,
+ pl->rot.x,
+ 1.f,
+ 9999.f,
+ winsize
+ );
+
+ gl_set_clip( game->gl, window, winsize );
+ gl_batch_empty( game->render.batch_3d );
+ glEnable( GL_DEPTH_TEST );
+ glEnable( GL_CULL_FACE );
+ // ???
+ glFrontFace( GL_CW );
+ glDepthFunc( GL_LESS );
+ bsp_traverse_draw( bsp, game, bsp->root, objl->pl->pos );
+ gl_batch_draw( game->render.batch_3d );
+
+ glDisable( GL_DEPTH_TEST );
+ glDisable( GL_CULL_FACE );
+ bsp_draw_sprites( bsp, game );
+ gl_reset_clip( game->gl );
+
+ VEC2 canvas = { (F32)game->gl->canvas_size[0], (F32)game->gl->canvas_size[1] };
+ gl_set_viewport( game->gl, {}, canvas );
+}
diff --git a/src/game/world/bsp_draw.h b/src/game/world/bsp_draw.h
new file mode 100644
index 0000000..048fa61
--- /dev/null
+++ b/src/game/world/bsp_draw.h
@@ -0,0 +1,4 @@
+#include "bsp.h"
+
+extern void bsp_draw_leaf( BSP* bsp, I32 leafidx );
+extern void bsp_draw( BSP* bsp, GAME_DATA* game, VEC2 window, VEC2 winsize );
diff --git a/src/game/world/draw.cpp b/src/game/world/draw.cpp
new file mode 100644
index 0000000..9269a21
--- /dev/null
+++ b/src/game/world/draw.cpp
@@ -0,0 +1,216 @@
+#include "draw.h"
+#include "map.h"
+
+#include "../../game.h"
+#include "../../render/gl_3d.h"
+
+#include "../objlist.h"
+#include "../raycast.h"
+
+VEC2 world_project_pos(
+ GAME_DATA* game,
+ VEC3 pos
+) {
+ GL_3D* gl3d = (GL_3D*)game->shaders.gl3d;
+
+ VEC4 clip = { pos.x, pos.z, pos.y, 1.f };
+ VEC4 ndc;
+
+ mat4_mul_vec4( &gl3d->projmat, &clip, &ndc );
+
+ if( ndc.w <= 0 )
+ return { -INFINITY, -INFINITY };
+
+ ndc.x /= ndc.w;
+ ndc.y /= ndc.w;
+
+ VEC2 screen = {
+ ( ndc.x * 0.5f + 0.5f ) * gl3d->winsize.x,
+ ( -( ndc.y * 0.5f ) + 0.5f ) * gl3d->winsize.y
+ };
+
+ return screen;
+}
+
+void world_draw_wall(
+ GAME_DATA* game,
+ WORLD* world,
+ VERTEX3D* verts,
+ MAP_WALL* s
+) {
+ SURF_PROPS* p = wall_get_props( world->map, s );
+ gl_3d_polygon( game->shaders.gl3d, verts, 4, p->tex );
+}
+
+void world_draw_walls( GAME_DATA* game, WORLD* world ) {
+ world->map->walls.each( fn( MAP_WALL* s ) {
+ SURF_PROPS* props = wall_get_props( world->map, s );
+ VEC3 start = s->start;
+ VEC3 end = s->end;
+
+ VERTEX3D verts[4]{};
+ verts[0].pos = { start.x, start.z, start.y };
+ verts[1].pos = { start.x, start.z + end.z, start.y };
+ verts[2].pos = { end.x, start.z + end.z, end.y };
+ verts[3].pos = { end.x, start.z, end.y };
+
+ verts[0].uv = { 0, 1 };
+ verts[1].uv = { 0, 0 };
+ verts[2].uv = { 1, 0 };
+ verts[3].uv = { 1, 1 };
+
+ verts[0].clr = verts[1].clr = verts[2].clr = verts[3].clr = props->clr;
+
+ world_draw_wall( game, world, verts, s );
+ } );
+}
+
+void world_draw_polygon(
+ GAME_DATA* game,
+ WORLD* world,
+ MAP_POLYGON* p,
+ LIST<VERTEX3D>* verts
+) {
+ SURF_PROPS* props = polygon_get_props( world->map, p );
+
+ gl_3d_polygon(
+ game->shaders.gl3d,
+ verts->data,
+ verts->size,
+ props->tex
+ );
+}
+
+void world_draw_polygons( GAME_DATA* game, WORLD* world ) {
+ for( U32 i = 0; i < world->map->polygons.size; ++i ) {
+ MAP_POLYGON* p = &world->map->polygons[i];
+ SURF_PROPS* props = polygon_get_props( world->map, p );
+
+ LIST<VERTEX3D> proj_verts;
+ U8 visc = p->vertices.size; // todo: visc
+ p->vertices.each( fn( MAP_VERTEX* mv ) {
+ VERTEX3D v;
+ v.pos = { mv->pos.x, mv->pos.z, mv->pos.y };
+ v.uv = mv->uv;
+ v.clr = mv->clr * props->clr;
+ v.sampler = 0;
+ proj_verts.push( v );
+ } );
+
+ if( proj_verts.size > 2 && visc > 0 ) {
+ world_draw_polygon( game, world, p, &proj_verts );
+ }
+ }
+}
+
+U8 sprite_is_occluded(
+ VEC3 player_pos,
+ MAP_SPRITE* sprite,
+ WORLD* world
+) {
+ VEC3 ray_dir = sprite->pos - player_pos;
+ F32 dist_to_sprite = vec_len2d( ray_dir );
+
+ vec_normalize( &ray_dir );
+
+ TRACE trace;
+ trace.startpos = player_pos;
+ U32 hit = ray_trace( &trace, sprite->pos );
+
+ F32 dist = vec_len( trace.endpos - player_pos );
+
+ return hit != TRACE_HIT_NONE && dist < dist_to_sprite;
+}
+
+void world_draw_sprite(
+ GAME_DATA* game,
+ MAP_SPRITE* sprite,
+ VEC3 player_pos,
+ WORLD* world
+ ) {
+ if( sprite_is_occluded( player_pos, sprite, world ) )
+ return;
+
+ VEC3 angle = vec_angles( sprite->pos, player_pos );
+ gl_3d_plane(
+ game->shaders.gl3d,
+ sprite->pos,
+ sprite->size,
+ { -angle.x + 90.f, angle.y - 90.f },
+ sprite->tex,
+ sprite->clr
+ );
+}
+
+void world_draw_sprites(
+ GAME_DATA* game,
+ WORLD* world
+) {
+ VEC3 player_pos = player_get_view_pos( objl->pl );
+ for( U32 i = 0; i < world->map->sprites.size; ++i ) {
+ MAP_SPRITE* sprite = &world->map->sprites[i];
+ world_draw_sprite( game, sprite, player_pos, world );
+ }
+}
+
+void world_draw( GAME_DATA* game, WORLD* world, VEC2 window, VEC2 winsize ) {
+ PLAYER* pl = objl->pl;
+ VEC2 start = { pl->pos.x, pl->pos.y };
+
+ F32 fov = pl->fov;
+
+ gl_set_viewport( game->gl, window, winsize );
+ gl_3d_projection_setup(
+ game->shaders.gl3d,
+ { pl->pos.x, pl->pos.y, pl->pos.z + pl->eyeoffset },
+ fov,
+ pl->rot.y,
+ pl->rot.x,
+ 1.f,
+ 9999.f,
+ winsize
+ );
+
+ gl_set_clip( game->gl, window, winsize );
+ glEnable( GL_DEPTH_TEST );
+ glDepthFunc( GL_LESS );
+ world_draw_polygons( game, world );
+ world_draw_walls( game, world );
+ glDisable( GL_DEPTH_TEST );
+ world_draw_sprites( game, world );
+ gl_reset_clip( game->gl );
+
+ VEC2 canvas = { (F32)game->gl->canvas_size[0], (F32)game->gl->canvas_size[1] };
+ gl_set_viewport( game->gl, {}, canvas );
+}
+
+VEC2 world_draw_project_point(
+ VEC3 vertex_pos,
+ VEC3 player_pos,
+ F32 player_angle_deg,
+ F32 fov_deg,
+ VEC2 window,
+ VEC2 winsize,
+ U8* in_view
+) {
+ F32 rx = vertex_pos.x;
+ F32 ry = vertex_pos.y;
+
+ F32 dist = sqrtf( rx*rx + ry*ry );
+ if( dist < 0.001f )
+ return { -99999.0f, -99999.0f };
+
+ F32 ang = atan2f( ry, rx );
+ F32 half_fov_rad = m_deg2rad( fov_deg * 0.5f );
+ if( in_view ) *in_view = (ang >= -half_fov_rad && ang <= half_fov_rad);
+
+ F32 cosang = cosf( ang );
+ F32 normalized_x = (ang + half_fov_rad) / (2.f * half_fov_rad);
+ F32 screen_x = window.x + normalized_x * winsize.x;
+
+ F32 screen_y_center = window.y + winsize.y * 0.5f;
+ F32 vz = ( vertex_pos.z - player_pos.z );
+ F32 screen_y = screen_y_center + (vz * winsize.y) / (dist * -cosang);
+
+ return { screen_x, screen_y };
+}
diff --git a/src/game/world/draw.h b/src/game/world/draw.h
new file mode 100644
index 0000000..6297a5e
--- /dev/null
+++ b/src/game/world/draw.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "../../util/vector.h"
+
+extern VEC2 world_project_pos( struct GAME_DATA* game, VEC3 pos );
+
+extern void world_draw_walls( struct GAME_DATA* game, struct WORLD* world );
+extern void world_draw_sprites( struct GAME_DATA* game, struct WORLD* world );
+extern void world_draw_polygons( struct GAME_DATA* game, struct WORLD* world );
+extern void world_draw( struct GAME_DATA* game, struct WORLD* world, VEC2 window, VEC2 winsize );
+extern VEC2 world_draw_project_point(
+ VEC3 vertex_pos,
+ VEC3 player_pos,
+ F32 player_angle_deg,
+ F32 fov_deg,
+ VEC2 window,
+ VEC2 winsize,
+ U8* in_view = 0
+);
diff --git a/src/game/world/map.cpp b/src/game/world/map.cpp
new file mode 100644
index 0000000..aba283d
--- /dev/null
+++ b/src/game/world/map.cpp
@@ -0,0 +1,640 @@
+#include <cfloat>
+
+#include "map.h"
+#include "../../game.h"
+#include "../assets.h"
+#include "bsp.h"
+
+const F32 SKYBOX_OFFSET = 24.f;
+
+MAP_TEXTURE_ENTRY* map_load_texture( WORLD_MAP* m, GAME_DATA* game, const char* name ) {
+ FNV1A hash = fnv1a( name );
+ MAP_TEXTURE_ENTRY** pentry = m->textures.where( fn( MAP_TEXTURE_ENTRY** t ) { return (*t)->hash == hash; } );
+ if( pentry ) {
+ return *pentry;
+ }
+
+ GL_TEX2D* tex = gl_texture_from_file( game->gl, name );
+ if( !tex ) {
+ dlog( "map_load_texture() : failed to load texture %s\n", name );
+ return 0;
+ }
+
+ MAP_TEXTURE_ENTRY* entry = new MAP_TEXTURE_ENTRY;
+ entry->tex = tex;
+ entry->hash = fnv1a( name );
+ strcpy( entry->name, name );
+
+ m->textures.push( entry );
+ return entry;
+}
+
+STAT map_add_texture_ref( WORLD_MAP* map, GL_TEX2D *tex ) {
+ const char* name = assets_abspath( tex->name );
+ FNV1A hash = fnv1a( name );
+
+ I32 idx = map->textures.idx_where( fn( MAP_TEXTURE_ENTRY** e ) {
+ return (*e)->hash == hash;
+ } );
+
+ if( idx != -1 )
+ return STAT_ALREADYEXISTS;
+
+ MAP_TEXTURE_ENTRY* entry = new MAP_TEXTURE_ENTRY;
+ entry->tex = tex;
+ entry->hash = hash;
+ strcpy( entry->name, name );
+
+ return STAT_OK;
+}
+
+GL_TEX2D* map_find_texture( WORLD_MAP* map, const char* name ) {
+ FNV1A hash = fnv1a( name );
+ MAP_TEXTURE_ENTRY** entry = map->textures.where( fn( MAP_TEXTURE_ENTRY** e ) {
+ return (*e)->hash == hash;
+ } );
+
+ if( entry )
+ return (*entry)->tex;
+
+ return 0;
+}
+
+void map_polygon_calc_vertex_normals( MAP_POLYGON* p ) {
+ for( U32 i = 0; i < p->vertices.size; i += 3 ) {
+ MAP_VERTEX* v1 = &p->vertices[i];
+ MAP_VERTEX* v2 = &p->vertices[(i + 1) % p->vertices.size];
+ MAP_VERTEX* v3 = &p->vertices[(i + 2) % p->vertices.size];
+
+ VEC3 d1 = v2->pos - v1->pos;
+ VEC3 d2 = v3->pos - v1->pos;
+ VEC3 n = vec_cross( d1, d2 );
+ vec_normalize( &n );
+
+ v1->normal = v2->normal = v3->normal = n;
+ }
+}
+
+STAT map_polygon_verts_from_section( WORLD_MAP* m, MAP_POLYGON* p, CFG_SECTION* vertices, U32 vertc ) {
+ char vertsec[256] = { 0 };
+ for( U32 i = 0; i < vertc; ++i ) {
+ MAP_VERTEX vert;
+
+ sprintf( vertsec, "%d", i );
+ CFG_SECTION* v = cfg_section( vertices, vertsec );
+ if( !v ) {
+ dlog( "map_polygon_verts_from_section() : missing vertex %d in polygon with propid %d in map %s\n", i, p->propid, m->name );
+ continue;
+ }
+
+ CFG_VEC3* pos = cfg_vec3( v, "pos" );
+ CFG_VEC2* uv = cfg_vec2( v, "uv" );
+ CFG_CLR* clr = cfg_clr( v, "clr" );
+
+ if( !pos ) {
+ dlog( "map_polygon_verts_from_section() : missing pos for vertex %d in polygon with propid %d in map %s\n", i, p->propid, m->name );
+ return STAT_ERR;
+ }
+
+ vert.pos = pos->value;
+ if( uv ) vert.uv = uv->value;
+ if( clr ) vert.clr = clr->value;
+
+ p->vertices.push( vert );
+ }
+
+ if( !p->vertices.size ) {
+ dlog( "map_polygon_verts_from_section() : invalid vertex count for polygon with propid %d in map %s\n", p->propid, m->name );
+ return STAT_ERR;
+ }
+
+ map_polygon_calc_vertex_normals( p );
+ return STAT_OK;
+}
+
+void map_polygon_calc_bounds( MAP_POLYGON* p ) {
+ p->vertices.each( fn( MAP_VERTEX* v ) {
+ p->mins.x = min( v->pos.x, p->mins.x );
+ p->mins.y = min( v->pos.y, p->mins.y );
+ p->mins.z = min( v->pos.z, p->mins.z );
+
+ p->maxs.x = max( v->pos.x, p->maxs.x );
+ p->maxs.y = max( v->pos.y, p->maxs.y );
+ p->maxs.z = max( v->pos.z, p->maxs.z );
+ } );
+}
+
+STAT map_polygons_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* polygons, U32 polyc ) {
+ if( !polyc ) {
+ dlog( "map_polygons_from_section() : no polygons in %s\n", m->name );
+ return STAT_OK;
+ }
+
+ U32 parsec = 0;
+ char polysec[256] = { 0 };
+ for( U32 i = 0; i < polyc; ++i ) {
+ MAP_POLYGON poly;
+
+ sprintf( polysec, "%d", i );
+ CFG_SECTION* p = cfg_section( polygons, polysec );
+ if( !p ) {
+ dlog( "map_polygons_from_section() : missing polygon %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CFG_INT* propid = cfg_int( p, "propid" );
+ if( !propid ) {
+ dlog( "map_polygons_from_section() : missing propid for polygon %d in map %s\n", i, m->name );
+ continue;
+ }
+
+ poly.propid = propid->value;
+ CFG_INT* polytype = cfg_int( p, "type" );
+ if( !polytype ) {
+ dlog( "map_polygons_from_section() : missing polygon type for polygon %d in %s\n", i, m->name );
+ continue;
+ }
+
+ poly.type = (U8)polytype->value;
+
+ CFG_INT* vertc = cfg_int( p, "vertcount" );
+ if( !vertc || !vertc->value ) {
+ dlog( "map_polygons_from_section() : missing vertex count for polygon %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CFG_SECTION* vertices = cfg_section( p, "vertices" );
+ if( !vertices ) {
+ dlog( "map_polygons_from_section() : missing vertices for polygon %d in %s", i, m->name );
+ continue;
+ }
+
+ if( !OK( map_polygon_verts_from_section( m, &poly, vertices, vertc->value ) ) )
+ continue;
+
+ map_polygon_calc_bounds( &poly );
+ m->polygons.push( poly );
+ ++parsec;
+ }
+
+ return parsec > 0 ? STAT_OK : STAT_ERR;
+}
+
+STAT map_walls_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* walls, U32 wallc ) {
+ if( !wallc ) {
+ dlog( "map_walls_from_section() : no walls in %s\n", m->name );
+ return STAT_OK;
+ }
+
+ U32 parsec = 0;
+ char wallsec[256] = { 0 };
+ for( U32 i = 0; i < wallc; i++ ) {
+ MAP_WALL seg;
+
+ sprintf( wallsec, "%d", i );
+ CFG_SECTION* w = cfg_section( walls, wallsec );
+ if( !w ) {
+ dlog( "map_walls_from_section() : missing wall %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CFG_VEC3* start = cfg_vec3( w, "start" );
+ CFG_VEC3* end = cfg_vec3( w, "end" );
+ CFG_INT* prop = cfg_int( w, "propid" );
+
+ if( !start || !end || !prop ) {
+ dlog( "map_walls_from_section() : bad wall definition in %s idx %d [%p %p %p]\n", m->name, i, start, end, prop );
+ continue;
+ }
+
+ seg.start = start->value;
+ seg.end = end->value;
+ seg.propid = prop->value;
+ m->walls.push( seg );
+
+ ++parsec;
+ }
+
+ return parsec > 0 ? STAT_OK : STAT_ERR;
+}
+
+
+STAT map_props_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* props, U32 propc ) {
+ if( !propc ) {
+ dlog( "map_props_from_section() : no props in %s\n", m->name );
+ return STAT_ERR;
+ }
+
+ U32 parsec = 0;
+ char propsec[256] = { 0 };
+ for( U32 i = 0; i < propc; ++i ) {
+ SURF_PROPS prop;
+
+ sprintf( propsec, "%d", i );
+ CFG_SECTION* p = cfg_section( props, propsec );
+ if( !p ) {
+ dlog( "map_props_from_section() : missing prop %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CFG_CLR* clr = cfg_clr( p, "clr" );
+ CFG_STR* tex = cfg_str( p, "tex" );
+
+ CLR propclr = (!!clr)? clr->value : CLR{ 1.f, 1.f, 1.f, 1.f };
+ GL_TEX2D* proptex = 0;
+ if( tex ) {
+ MAP_TEXTURE_ENTRY* entry = map_load_texture( m, g, tex->str );
+ if( !entry ) continue;
+
+ proptex = entry->tex;
+ }
+
+ prop.clr = propclr;
+ prop.tex = proptex;
+ m->props.push( prop );
+
+ ++parsec;
+ }
+
+ return parsec > 0 ? STAT_OK : STAT_ERR;
+}
+
+STAT map_sprites_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* sprites, U32 spritec ) {
+ if( !spritec ) {
+ dlog( "map_sprites_from_section() : no sprites in %s\n", m->name );
+ return STAT_OK;
+ }
+
+ U32 parsec = 0;
+ char spritesec[256] = { 0 };
+ for( U32 i = 0; i < spritec; ++i ) {
+ MAP_SPRITE sprite;
+
+ sprintf( spritesec, "%d", i );
+ CFG_SECTION* s = cfg_section( sprites, spritesec );
+ if( !s ) {
+ dlog( "map_sprites_from_section() : missing sprite %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CFG_CLR* clr = cfg_clr( s, "clr" );
+ CFG_STR* tex = cfg_str( s, "tex" );
+ CFG_VEC3* pos = cfg_vec3( s, "pos" );
+ CFG_VEC2* size = cfg_vec2( s, "size" );
+
+ if( !tex || !pos || !size ) {
+ dlog( "map_sprites_from_section() : invalid sprite %d in %s\n", i, m->name );
+ continue;
+ }
+
+ CLR spriteclr = (!!clr)? clr->value : CLR{ 1.f, 1.f, 1.f, 1.f };
+ MAP_TEXTURE_ENTRY* texentry = map_load_texture( m, g, tex->str );
+ GL_TEX2D* spritetex = texentry->tex;
+
+ sprite.tex = spritetex;
+ sprite.pos = pos->value;
+ sprite.size = size->value;
+ sprite.clr = spriteclr;
+ m->sprites.push( sprite );
+
+ ++parsec;
+ }
+
+ return parsec > 0 ? STAT_OK : STAT_ERR;
+}
+
+STAT map_skybox_from_section( CFG_SECTION* s, GAME_DATA* g, WORLD_MAP* m ) {
+ CFG_SECTION* props = cfg_section( s, "props" );
+ if( !props )
+ return STAT_ERR;
+
+ CFG_CLR* clr = cfg_clr( props, "clr" );
+ CFG_STR* tex = cfg_str( props, "tex" );
+
+ CLR skyclr;
+ GL_TEX2D* skytex;
+ if( !tex ) {
+ skyclr = (!!clr)? clr->value : CLR::BLACK();
+ skytex = 0;
+ }
+ else {
+ MAP_TEXTURE_ENTRY* texentry = map_load_texture( m, g, tex->str );
+ skyclr = (!!clr)? clr->value : CLR::WHITE();
+ if( !texentry || !texentry->tex ) {
+ dlog( "map_skybox_from_section() : could not load skybox texture '%s'", tex->str );
+ return STAT_ERR;
+ }
+ else {
+ skytex = texentry->tex;
+ }
+ }
+
+
+ m->skybox.props = SURF_PROPS{ .tex = skytex, .clr = skyclr };
+ return STAT_OK;
+}
+
+MAP_SKYBOX map_default_skybox() {
+ return {
+ .props = { .tex = 0, .clr = CLR::BLACK() }
+ };
+}
+
+// generates 4 walls and 2 polygons to enclose the map.
+void map_gen_skybox( WORLD_MAP* m ) {
+ VEC3 mins = m->mins - VEC3( SKYBOX_OFFSET, SKYBOX_OFFSET, SKYBOX_OFFSET );
+ VEC3 maxs = m->maxs + VEC3( SKYBOX_OFFSET, SKYBOX_OFFSET, SKYBOX_OFFSET );
+
+ m->skybox.walls[0] = {
+ .start = { mins.x, mins.y, mins.z },
+ .end = { maxs.x, mins.y, maxs.z - mins.z },
+ .propid = MAPPROP_SKYBOX
+ };
+ m->skybox.walls[1] = {
+ .start = { maxs.x, mins.y, mins.z },
+ .end = { maxs.x, maxs.y, maxs.z - mins.z },
+ .propid = MAPPROP_SKYBOX
+ };
+ m->skybox.walls[2] = {
+ .start = { maxs.x, maxs.y, mins.z },
+ .end = { mins.x, maxs.y, maxs.z - mins.z },
+ .propid = MAPPROP_SKYBOX
+ };
+ m->skybox.walls[3] = {
+ .start = { mins.x, maxs.y, mins.z },
+ .end = { mins.x, mins.y, maxs.z - mins.z },
+ .propid = MAPPROP_SKYBOX
+ };
+
+ VEC2 floor[] = {
+ { mins.x, mins.y },
+ { maxs.x, mins.y },
+ { maxs.x, maxs.y },
+ { mins.x, maxs.y }
+ };
+ VEC2 floor_uv[] = {
+ { 0, 0 },
+ { 1, 0 },
+ { 1, 1 },
+ { 0, 1 }
+ };
+
+ m->skybox.polygons[0] = m->skybox.polygons[1] = {
+ .propid = MAPPROP_SKYBOX
+ };
+
+ for( U32 i = 0; i < 4; ++i ) {
+ m->skybox.polygons[0].vertices.push( {
+ .pos = { floor[i].x, floor[i].y, mins.z },
+ .uv = floor_uv[i], // todo: fix
+ .clr = CLR::WHITE()
+ } );
+
+ m->skybox.polygons[1].vertices.push( {
+ .pos = { floor[i].x, floor[i].y, maxs.z },
+ .uv = floor_uv[i], // todo: fix
+ .clr = CLR::WHITE()
+ } );
+ }
+}
+
+void map_calc_bounds( WORLD_MAP* m ) {
+ m->mins = { FLT_MAX, FLT_MAX, FLT_MAX };
+ m->maxs = { -FLT_MAX, -FLT_MAX, -FLT_MAX };
+ m->walls.each( fn( MAP_WALL* l ) {
+ m->mins.x = min( min( l->start.x, l->end.x ), m->mins.x );
+ m->mins.y = min( min( l->start.y, l->end.y ), m->mins.y );
+ m->mins.z = min( min( l->start.z, l->start.x + l->end.z ), m->mins.z );
+
+ m->maxs.x = max( max( l->start.x, l->end.x ), m->maxs.x );
+ m->maxs.y = max( max( l->start.y, l->end.y ), m->maxs.y );
+ m->maxs.z = max( max( l->start.z, l->start.x + l->end.z ), m->maxs.z );
+ } );
+
+ m->polygons.each( fn( MAP_POLYGON* p ) {
+ m->mins.x = min( p->mins.x, m->mins.x );
+ m->mins.y = min( p->mins.y, m->mins.y );
+ m->mins.z = min( p->mins.z, m->mins.z );
+
+ m->maxs.x = max( p->maxs.x, m->maxs.x );
+ m->maxs.y = max( p->maxs.y, m->maxs.y );
+ m->maxs.z = max( p->maxs.z, m->maxs.z );
+ } );
+}
+
+void map_check_bounds( WORLD_MAP* m ) {
+ VEC3 mins = { FLT_MAX, FLT_MAX, FLT_MAX };
+ VEC3 maxs = { -FLT_MAX, -FLT_MAX, -FLT_MAX };
+ m->walls.each( fn( MAP_WALL* l ) {
+ mins.x = min( min( l->start.x, l->end.x ), mins.x );
+ mins.y = min( min( l->start.y, l->end.y ), mins.y );
+ mins.z = min( min( l->start.z, l->start.x + l->end.z ), mins.z );
+
+ maxs.x = max( max( l->start.x, l->end.x ), maxs.x );
+ maxs.y = max( max( l->start.y, l->end.y ), maxs.y );
+ maxs.z = max( max( l->start.z, l->start.x + l->end.z ), maxs.z );
+ } );
+
+ m->polygons.each( fn( MAP_POLYGON* p ) {
+ mins.x = min( p->mins.x, mins.x );
+ mins.y = min( p->mins.y, mins.y );
+ mins.z = min( p->mins.z, mins.z );
+
+ maxs.x = max( p->maxs.x, maxs.x );
+ maxs.y = max( p->maxs.y, maxs.y );
+ maxs.z = max( p->maxs.z, maxs.z );
+ } );
+
+ if( vec_dist( mins, m->mins ) > 0.01f || vec_dist( maxs, m->maxs ) > 0.01f ) {
+ m->mins = mins;
+ m->maxs = maxs;
+ map_gen_skybox( m );
+ }
+}
+
+// todo: too long -- extract parts into funcs
+WORLD_MAP* map_from_file( GAME_DATA* game, const char* path ) {
+ CFG_SECTION* root = cfg_load( path );
+
+ ddef( const char* errstr = "map_from_file() : %s - missing %s section\n" );
+ if( !root ) { dlog( "map_from_file() : error opening file %s\n", path ); return 0; }
+ defer( cfg_free( root ); );
+
+ WORLD_MAP* m = new WORLD_MAP;
+ const char* name = file_path_last_of( path );
+ strcpy( m->name, name );
+
+ CFG_SECTION* s = cfg_section( root, "map" );
+ if( !s ) { dlog( errstr, "map", path ); delete m; return 0; }
+
+ CFG_SECTION* props = cfg_section( s, "props" );
+ if( !props ) { dlog( errstr, path, "props" ); delete m; return 0; }
+
+ CFG_SECTION* walls = cfg_section( s, "walls" );
+ if( !walls ) { dlog( errstr, path, "walls" ); delete m; return 0; }
+
+ CFG_SECTION* polys = cfg_section( s, "polygons" );
+ if( !polys ) { dlog( errstr, path, "polygons" ); delete m; return 0; }
+
+ CFG_INT* propc = cfg_int( s, "propcount" );
+ if( !propc ) { dlog( errstr, path, "propcount" ); delete m; return 0; }
+
+ CFG_INT* wallc = cfg_int( s, "wallcount" );
+ if( !wallc ) { dlog( errstr, path, "wallcount" ); delete m; return 0; }
+
+ CFG_INT* polyc = cfg_int( s, "polycount" );
+ if( !polyc ) { dlog( errstr, path, "polycount" ); delete m; return 0; }
+
+ if( !OK( map_props_from_section( m, game, props, propc->value ) ) ) { delete m; return 0; }
+ if( !OK( map_walls_from_section( m, game, walls, wallc->value ) ) ) { delete m; return 0; }
+ if( !OK( map_polygons_from_section( m, game, polys, polyc->value ) ) ) { delete m; return 0; }
+
+ CFG_INT* spritec = cfg_int( s, "spritecount" );
+ if( spritec ) {
+ CFG_SECTION* sprites = cfg_section( s, "sprites" );
+ if( !sprites ) { dlog( errstr, path, "sprites" ); delete m; return 0; }
+ if( !OK( map_sprites_from_section( m, game, sprites, spritec->value ) ) ) { delete m; return 0; }
+ }
+
+ CFG_VEC3* startpos = cfg_vec3( s, "startpos" );
+ if( !startpos ) {
+ dlog( errstr, path, "startpos" );
+ m->startpos = { 0, 0, 0 };
+ }
+ else {
+ m->startpos = startpos->value;
+ }
+
+ CFG_FLOAT* startang = cfg_float( s, "startang" );
+ if( !startang ) {
+ dlog( errstr, path, "startang" );
+ m->startang = 0;
+ }
+ else {
+ m->startang = startang->value;
+ }
+
+ map_calc_bounds( m );
+ CFG_SECTION* skybox = cfg_section( s, "skybox" );
+ if( !skybox ) {
+ dlog( errstr, path, "skybox" );
+ dlog( "using default skybox" );
+ m->skybox = map_default_skybox();
+ }
+ else {
+ if( !OK( map_skybox_from_section( skybox, game, m ) ) ) {
+ dlog( errstr, path, "skybox" );
+ dlog( "using default skybox" );
+ m->skybox = map_default_skybox();
+ }
+ }
+ map_gen_skybox( m );
+ return m;
+}
+
+void map_serialize_info( CFG_SECTION* s, WORLD_MAP* map ) {
+ cfg_int( "wallcount", s, map->walls.size );
+ cfg_int( "polycount", s, map->polygons.size );
+ cfg_int( "propcount", s, map->props.size );
+ cfg_int( "spritecount", s, map->sprites.size );
+ cfg_vec3( "startpos", s, map->startpos );
+ cfg_float( "startang", s, map->startang );
+}
+
+void map_serialize_walls( CFG_SECTION* s, WORLD_MAP* map ) {
+ CFG_SECTION* walls = cfg_section_new( "walls", s );
+ char name[64];
+
+ U32 i = 0;
+ map->walls.each( fn( MAP_WALL* seg ) {
+ sprintf( name, "%d", i++ );
+ CFG_SECTION* wallsec = cfg_section_new( name, walls );
+ cfg_vec3( "start", wallsec, seg->start );
+ cfg_vec3( "end", wallsec, seg->end );
+ cfg_int( "propid", wallsec, seg->propid );
+ } );
+}
+
+void map_serialize_polygons( CFG_SECTION* s, WORLD_MAP* map ) {
+ CFG_SECTION* polygons = cfg_section_new( "polygons", s );
+ char name[64];
+
+ U32 i = 0;
+ map->polygons.each( fn( MAP_POLYGON* p ) {
+ sprintf( name, "%d", i++ );
+ CFG_SECTION* polygonsec = cfg_section_new( name, polygons );
+ cfg_int( "vertcount", polygonsec, p->vertices.size );
+ cfg_int( "propid", polygonsec, p->propid );
+ cfg_int( "type", polygonsec, p->type );
+
+ CFG_SECTION* vertices = cfg_section_new( "vertices", polygonsec );
+ U32 i2 = 0;
+ p->vertices.each( fn( MAP_VERTEX* v ) {
+ sprintf( name, "%d", i2++ );
+ CFG_SECTION* vertsec = cfg_section_new( name, vertices );
+ cfg_vec3( "pos", vertsec, v->pos );
+ cfg_vec2( "uv", vertsec, v->uv );
+ cfg_clr( "clr", vertsec, v->clr );
+ } );
+ } );
+}
+
+void map_serialize_sprites( CFG_SECTION* s, WORLD_MAP* map ) {
+ CFG_SECTION* sprites = cfg_section_new( "sprites", s );
+ char name[64];
+
+ U32 i = 0;
+ map->sprites.each( fn( MAP_SPRITE* s ) {
+ sprintf( name, "%d", i++ );
+ CFG_SECTION* spritesec = cfg_section_new( name, sprites );
+ cfg_vec3( "pos", spritesec, s->pos );
+ cfg_vec2( "size", spritesec, s->size );
+ cfg_clr( "clr", spritesec, s->clr );
+ if( s->tex ) {
+ const char* endp = assets_abspath( s->tex->name );
+ cfg_str( "tex", spritesec, endp, 256 );
+ }
+ } );
+}
+
+void map_serialize_props( CFG_SECTION* s, WORLD_MAP* map ) {
+ CFG_SECTION* props = cfg_section_new( "props", s );
+ char name[64];
+
+ U32 i = 0;
+ map->props.each( fn( SURF_PROPS* p ) {
+ sprintf( name, "%d", i++ );
+ CFG_SECTION* propsec = cfg_section_new( name, props );
+ cfg_clr( "clr", propsec, p->clr );
+ if( p->tex ) {
+ const char* endp = assets_abspath( p->tex->name );
+ cfg_str( "tex", propsec, endp, 256 );
+ }
+ } );
+}
+
+
+CFG_SECTION* map_serialize( WORLD_MAP* map ) {
+ CFG_SECTION* root = cfg_section_new( "root", 0 );
+ CFG_SECTION* s = cfg_section_new( "map", root );
+
+ map_serialize_info( s, map );
+ map_serialize_props( s, map );
+ map_serialize_walls( s, map );
+ map_serialize_polygons( s, map );
+ map_serialize_sprites( s, map );
+
+ return s;
+}
+
+void map_free( GAME_DATA* game, WORLD_MAP* m ) {
+ if( !m ) return;
+ if( m->bsp ) bsp_free( m->bsp );
+
+ m->textures.each( fn( MAP_TEXTURE_ENTRY** e ) {
+ gl_texture_destroy( game->gl, (*e)->tex );
+ delete *e;
+ } );
+
+ delete m;
+}
diff --git a/src/game/world/map.h b/src/game/world/map.h
new file mode 100644
index 0000000..29022db
--- /dev/null
+++ b/src/game/world/map.h
@@ -0,0 +1,137 @@
+#pragma once
+
+#include "../../util/allocator.h"
+#include "../../util/vector.h"
+#include "../../util/color.h"
+#include "../../util/fnv.h"
+
+#include <string.h>
+
+enum MapPropId_t {
+ MAPPROP_SKYBOX = -1,
+};
+
+struct SURF_PROPS {
+ struct GL_TEX2D* tex;
+ CLR clr;
+};
+
+struct MAP_VERTEX {
+ VEC3 pos;
+ VEC3 normal;
+ VEC2 uv;
+ CLR clr;
+};
+
+enum MapPolygonType_t {
+ MPT_FLOOR,
+ MPT_CEILING
+};
+
+struct MAP_POLYGON {
+ MAP_POLYGON& operator=( const MAP_POLYGON& other ) {
+ memcpy( this, &other, sizeof(*this) );
+ vertices = other.vertices;
+
+ return *this;
+ }
+
+ LIST<MAP_VERTEX> vertices;
+
+ VEC3 mins;
+ VEC3 maxs;
+ U8 type;
+ I32 propid;
+};
+
+struct MAP_WALL {
+ VEC3 start;
+ VEC3 end;
+
+ VEC2 uvstart;
+ VEC2 uvend;
+
+ I32 propid;
+};
+
+struct MAP_TEXTURE_ENTRY {
+ char name[256];
+ struct GL_TEX2D* tex;
+ FNV1A hash;
+};
+
+struct MAP_SPRITE {
+ VEC3 pos;
+ VEC2 size;
+ CLR clr;
+ GL_TEX2D* tex;
+};
+
+struct MAP_ENTITY {
+ U32 classid;
+ LIST<struct OBJECT_PROP*> props;
+};
+
+struct MAP_SKYBOX {
+ SURF_PROPS props;
+ MAP_WALL walls[4];
+ MAP_POLYGON polygons[2];
+};
+
+struct WORLD_MAP {
+ LIST<MAP_WALL> walls;
+ LIST<MAP_POLYGON> polygons;
+ LIST<MAP_SPRITE> sprites;
+ LIST<SURF_PROPS> props;
+ MAP_SKYBOX skybox;
+ F32 w;
+ F32 h;
+
+ VEC3 mins;
+ VEC3 maxs;
+ char name[256];
+
+ VEC3 startpos;
+ F32 startang;
+
+ struct BSP* bsp{};
+
+ LIST<MAP_TEXTURE_ENTRY*> textures;
+};
+
+extern WORLD_MAP* map_from_file( struct GAME_DATA* game, const char* filename );
+extern struct CFG_SECTION* map_serialize( WORLD_MAP* map );
+extern STAT map_add_texture_ref( WORLD_MAP* map, GL_TEX2D* tex );
+extern GL_TEX2D* map_find_texture( WORLD_MAP* map, const char* name );
+extern void map_free( struct GAME_DATA* game, WORLD_MAP* map );
+
+// checks mins/maxs and recreates skybox if needed
+extern void map_check_bounds( WORLD_MAP* map );
+extern void map_calc_bounds( WORLD_MAP* map );
+extern void map_polygon_calc_bounds( MAP_POLYGON* p );
+
+
+inline SURF_PROPS* map_props_get_special( WORLD_MAP* w, I32 prop ) {
+ switch( prop ) {
+ case MAPPROP_SKYBOX: return &w->skybox.props;
+ };
+ dlog( "map_props_get_special(): unknown special prop %d\n", prop );
+ return 0;
+}
+
+inline SURF_PROPS* map_get_props( WORLD_MAP* m, I32 idx ) {
+ return idx < 0 ?
+ map_props_get_special( m, idx ) :
+ &m->props[idx];
+}
+
+inline SURF_PROPS* wall_get_props( WORLD_MAP* w, MAP_WALL* s ) {
+ return s->propid < 0 ?
+ map_props_get_special( w, s->propid ) :
+ &w->props[s->propid];
+}
+inline SURF_PROPS* polygon_get_props( WORLD_MAP* w, MAP_POLYGON* s ) {
+ return s->propid < 0 ?
+ map_props_get_special( w, s->propid ) :
+ &w->props[s->propid];
+}
diff --git a/src/game/world/trace.cpp b/src/game/world/trace.cpp
new file mode 100644
index 0000000..b3a0041
--- /dev/null
+++ b/src/game/world/trace.cpp
@@ -0,0 +1,393 @@
+#include <cfloat>
+
+#include "trace.h"
+#include "../../util/math.h"
+#include "bsp.h"
+
+// i have no idea how this works tbh i pasted this
+// todo : backface culling (return 0 if d < BSP_TRACE_EPSILON)
+U8 bsp_segment_intersects_tri(
+ const VEC3& va, const VEC3& vb,
+ const VEC3& v0, const VEC3& v1, const VEC3& v2,
+ F32* fract,
+ VEC2* out_uv = 0
+) {
+ VEC3 dir = vb - va;
+ VEC3 e1 = v1 - v0,
+ e2 = v2 - v0;
+
+ VEC3 p = vec_cross( dir, e2 );
+ F32 d = vec_dot( e1, p );
+
+ if( fabsf( d ) < BSP_TRACE_EPSILON ) {
+ // parallel
+ return 0;
+ }
+
+ F32 inv = 1.0f / d;
+ VEC3 ivec = va - v0;
+ F32 u = vec_dot( ivec, p ) * inv;
+ if( u < -BSP_TRACE_EPSILON || u > 1.0f + BSP_TRACE_EPSILON ) {
+ // outside
+ return 0;
+ }
+
+ VEC3 q = vec_cross( ivec, e1 );
+ F32 v = vec_dot( dir, q ) * inv;
+ if( v < -BSP_TRACE_EPSILON || u + v > 1.0f + BSP_TRACE_EPSILON ) {
+ // outside
+ return 0;
+ }
+
+ F32 t = vec_dot( e2, q ) * inv;
+ if( t < -BSP_TRACE_EPSILON || t > 1.0f + BSP_TRACE_EPSILON ) {
+ // outside
+ return 0;
+ }
+
+ if( fract ) *fract = m_clamp( t, 0.f, 1.f );
+ if( out_uv ) {
+ *out_uv = { u, v };
+ }
+
+ return 1;
+}
+
+U8 bsp_trace_leaf_segment( BSP_TRACE* trace, BSP* bsp, I32 leafidx, const VEC3& va, const VEC3& vb ) {
+ BSP_LEAF* leaf = &bsp->leaves.data[leafidx];
+ U8 didhit = 0;
+ F32 best = FLT_MAX;
+
+ for( U32 i = 0; i < leaf->count; ++i ) {
+ BSP_FACE* face = &bsp->faces.data[leaf->first + i];
+
+ // shouldnt happen
+ if( face->verts.size < 3 )
+ continue;
+
+ for( U32 t0 = 1; t0 + 1 < face->verts.size; ++t0 ) {
+ VEC3 v0 = face->verts[0].pos;
+ VEC3 v1 = face->verts[t0].pos;
+ VEC3 v2 = face->verts[t0 + 1].pos;
+
+ // todo : skip func for props
+
+ F32 t;
+ VEC2 uv;
+
+ if( !bsp_segment_intersects_tri( va, vb, v0, v1, v2, &t, &uv ) )
+ continue;
+
+ if( t < best ) {
+ best = t;
+ trace->hit = didhit = 1;
+ trace->frac = t;
+ trace->point = va + (vb - va) * t;
+ trace->normal = vec_normalize( vec_cross( v1 - v0, v2 - v0 ) );
+ trace->propid = face->propid;
+ trace->leafid = leafidx;
+ trace->faceid = leaf->first + i;
+ }
+ }
+ }
+
+ return didhit;
+}
+
+U8 bsp_trace_segment( BSP_TRACE* trace, BSP* bsp, I32 nodeidx, const VEC3& va, const VEC3& vb ) {
+ if( bsp_is_leaf( nodeidx ) ) {
+ I32 i = bsp_leaf_index( nodeidx );
+ return bsp_trace_leaf_segment( trace, bsp, i, va, vb );
+ }
+
+ BSP_NODE* node = &bsp->nodes.data[nodeidx];
+ F32 da = bsp_plane_dist( node->plane, va );
+ F32 db = bsp_plane_dist( node->plane, vb );
+
+ if( da > BSP_TRACE_EPSILON && db > BSP_TRACE_EPSILON )
+ return bsp_trace_segment( trace, bsp, node->front, va, vb );
+ if( da < -BSP_TRACE_EPSILON && db < -BSP_TRACE_EPSILON )
+ return bsp_trace_segment( trace, bsp, node->back, va, vb );
+
+ VEC3 dir = vb - va;
+ F32 denom = vec_dot( node->plane.normal, dir );
+ I32 near = (da >= 0.f) ? node->front : node->back;
+ I32 far = (da >= 0.f) ? node->back : node->front;
+ if( fabsf( denom ) < BSP_TRACE_EPSILON ) {
+ if( bsp_trace_segment( trace, bsp, near, va, vb ) )
+ return 1;
+ return bsp_trace_segment( trace, bsp, far, va, vb );
+ }
+
+ F32 t = (node->plane.dist - vec_dot( node->plane.normal, va ) ) / denom;
+ t = m_clamp( t, 0.f, 1.f );
+
+ VEC3 iplane = va + dir * t;
+ if( bsp_trace_segment( trace, bsp, near, va, iplane ) )
+ return 1;
+
+ if( t <= BSP_TRACE_EPSILON || t >= 1.0f - BSP_TRACE_EPSILON ) {
+ return bsp_trace_segment( trace, bsp, far, iplane, vb );
+ }
+
+ return bsp_trace_segment( trace, bsp, far, iplane, vb );
+}
+
+// nudge the trace ends slightly to prevent exact-coplanar infinite recursion.
+// quake does this every recursion divided up by the length of the trace
+// should not matter in the end
+void bsp_trace_nudge( BSP_TRACE* trace ) {
+ VEC3 dir = trace->in_end - trace->in_start;
+ VEC3 nudge = dir * BSP_TRACE_EPSILON;
+ trace->in_start += nudge;
+ trace->in_end -= nudge;
+}
+
+U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, VEC3 start, VEC3 end ) {
+ if( !trace ) {
+ dlog( "bsp_trace() : null trace\n" );
+ abort();
+ }
+
+ trace->in_start = start;
+ trace->in_end = end;
+
+ return bsp_trace( trace, bsp );
+}
+
+U8 bsp_trace( BSP_TRACE* trace, BSP* bsp ) {
+ if( !trace ) {
+ dlog( "bsp_trace() : null trace\n" );
+ abort();
+ }
+
+ trace->hit = 0;
+
+ bsp_trace_nudge( trace );
+ VEC3 start = trace->in_start;
+ VEC3 end = trace->in_end;
+
+ return bsp_trace_segment( trace, bsp, bsp->root, start, end );
+}
+
+inline VEC3 bsp_face_get_normal( const BSP_FACE* f ){
+ const VEC3 v0 = f->verts.data[0].pos;
+ const VEC3 v1 = f->verts.data[1].pos;
+ const VEC3 v2 = f->verts.data[2].pos;
+ return vec_normalize( vec_cross( v1 - v0, v2 - v0 ) );
+}
+
+inline U8 point_in_inflated_poly(
+ const VEC3& point,
+ const BSP_FACE* face,
+ const AABB& hull,
+ const VEC3& norm
+){
+ VEC3 center{};
+ U32 c = face->verts.size;
+ for( U32 i = 0; i < c; ++i )
+ center = center + face->verts.data[i].pos;
+ if( c ) center = center * (1.0f / c);
+
+ for( U32 i = 0; i < c; ++i ) {
+ VEC3 a = face->verts.data[i].pos;
+ VEC3 b = face->verts.data[(i+1)%c].pos;
+ VEC3 in = vec_cross( norm, b - a );
+ F32 len = vec_len( in );
+ if( len <= 0.00001f ) continue;
+ in *= (1.0f/ len);
+ if( vec_dot( center - a, in ) < 0.0f )
+ in = in * -1.0f;
+
+ F32 dist = vec_dot( point - a, in );
+ VEC2 feet = aabb_extend_at_feet( hull, in );
+ F32 pos_r = feet.x, neg_r = feet.y;
+ F32 expand = norm.z >= 0 ? neg_r : pos_r;
+ if( dist < -expand - BSP_TRACE_EPSILON )
+ return 0;
+ }
+ return 1;
+}
+
+
+// big fat ass
+U8 bsp_trace_sweep_aabb_to_face(
+ BSP_TRACE* trace,
+ const AABB& hull,
+ BSP_FACE* face,
+ const VEC3& start,
+ const VEC3& end,
+ U32 face_idx
+) {
+ if( face->verts.size < 3 )
+ return 0;
+
+ VEC3 norm = bsp_face_get_normal( face );
+ F32 d = vec_dot( norm, face->verts[0].pos );
+ VEC2 feet = aabb_extend_at_feet( hull, norm );
+ F32 pos_r = feet.x, neg_r = feet.y;
+ F32 radius = norm.z >= 0 ? neg_r : pos_r;
+
+ VEC3 dir = end - start;
+ F32 s0 = vec_dot( norm, trace->in_start ) - d;
+ F32 ndir = vec_dot( norm, trace->in_end - trace->in_start );
+
+ if( fabsf( s0 ) <= radius + BSP_TRACE_EPSILON ) {
+ if( point_in_inflated_poly( start, face, hull, norm ) ) {
+ trace->hit = 1;
+ trace->frac = 0.f;
+ trace->point = start;
+ trace->normal = norm;
+ trace->propid = face->propid;
+ trace->faceid = face_idx;
+ return 1;
+ }
+ }
+
+ if( fabsf( ndir ) < BSP_TRACE_EPSILON )
+ return 0;
+
+ F32 t_enter = (-radius - s0) / ndir;
+ F32 t_exit = ( radius - s0) / ndir;
+ if( t_enter > t_exit ) { F32 tmp=t_enter; t_enter=t_exit; t_exit=tmp; }
+
+ if( t_exit < 0.f - BSP_TRACE_EPSILON || t_enter > 1.f + BSP_TRACE_EPSILON )
+ return 0;
+
+ F32 t = m_clamp( t_enter, 0.f, 1.f );
+ VEC3 step = start + dir * t;
+
+ if( !point_in_inflated_poly( step, face, hull, norm ) )
+ return 0;
+
+ trace->hit = 1;
+ trace->frac = t;
+ trace->point = step;
+ trace->normal = norm;
+ trace->propid = face->propid;
+ trace->faceid = face_idx;
+ return 1;
+}
+
+U8 bsp_trace_sweep_aabb_to_leaf(
+ BSP* bsp,
+ BSP_TRACE* trace,
+ const AABB& hull,
+ const VEC3& start,
+ const VEC3& end,
+ I32 leaf_idx
+) {
+ BSP_LEAF* leaf = &bsp->leaves.data[leaf_idx];
+ U8 hit = 0;
+ F32 best = FLT_MAX;
+
+ BSP_TRACE besthit = *trace;
+
+ for( U32 i = 0; i < leaf->count; ++i ) {
+ U32 face_idx = leaf->first + i;
+ BSP_FACE* f = &bsp->faces.data[face_idx];
+
+ if( f->propid == MAPPROP_SKYBOX )
+ continue;
+
+ BSP_TRACE tr = *trace;
+ if( !bsp_trace_sweep_aabb_to_face( &tr, hull, f, start, end, face_idx ) )
+ continue;
+
+ if( tr.frac < best ) {
+ best = tr.frac;
+ besthit = tr;
+ besthit.leafid = leaf_idx;
+ hit = 1;
+ }
+ }
+
+ if( hit ) {
+ *trace = besthit;
+ trace->hit = 1;
+ }
+ return hit;
+}
+
+U8 bsp_trace_sweep_aabb(
+ BSP_TRACE* trace,
+ BSP* bsp,
+ const AABB& hull,
+ const VEC3& start,
+ const VEC3& end,
+ I32 node_idx
+) {
+ if( bsp_is_leaf( node_idx ) ) {
+ I32 i = bsp_leaf_index( node_idx );
+ return bsp_trace_sweep_aabb_to_leaf( bsp, trace, hull, start, end, i );
+ }
+
+ BSP_NODE* node = &bsp->nodes.data[node_idx];
+ VEC3 n = node->plane.normal;
+ F32 d = node->plane.dist;
+
+ F32 s_dist = vec_dot( n, start ) - d;
+ F32 e_dist = vec_dot( n, end ) - d;
+ F32 offset = aabb_support_radius( hull, n );
+
+ if( s_dist > offset && e_dist > offset )
+ return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, node->front );
+
+ if( s_dist < -offset && e_dist < -offset )
+ return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, node->back );
+
+ VEC3 dir = end - start;
+ F32 denom = ( e_dist - s_dist );
+
+ if( fabsf(denom) < BSP_TRACE_EPSILON ) {
+ I32 near = (s_dist >= 0.f) ? node->front : node->back;
+ I32 far = (s_dist >= 0.f) ? node->back : node->front;
+ if( bsp_trace_sweep_aabb( trace, bsp, hull, start, end, near ) )
+ return 1;
+ return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, far );
+ }
+
+ F32 tin = ( offset - s_dist) / denom;
+ F32 tout = (-offset - s_dist) / denom;
+ if( tin > tout ) { F32 tmp = tin; tin = tout; tout = tmp; }
+
+ tin = m_clamp( tin, 0.f, 1.f );
+ tout = m_clamp( tout , 0.f, 1.f );
+
+ VEC3 mid = start + dir * tin;
+ I32 near = (s_dist > 0.f) ? node->front : node->back;
+ I32 far = (s_dist > 0.f) ? node->back : node->front;
+
+ if( bsp_trace_sweep_aabb( trace, bsp, hull, start, mid, near ) )
+ return 1;
+
+ if( tin <= BSP_TRACE_EPSILON || tin >= 1.f - BSP_TRACE_EPSILON ) {
+ F32 nudge = (denom >= 0.f ? 1.f : -1.f) * BSP_TRACE_EPSILON * 4.f;
+ VEC3 nudged = mid + dir * nudge;
+ return bsp_trace_sweep_aabb( trace, bsp, hull, nudged, end, far );
+ }
+
+ return bsp_trace_sweep_aabb(trace, bsp, hull, mid, end, far );
+}
+
+U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB hull ) {
+ if( !trace || !bsp )
+ return 0;
+
+ trace->hit = 0;
+ bsp_trace_nudge( trace );
+ VEC3 start = trace->in_start;
+ VEC3 end = trace->in_end;
+
+ U8 ret = bsp_trace_sweep_aabb( trace, bsp, hull, start, end, bsp->root );
+ return ret;
+}
+
+U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB hull, VEC3 start, VEC3 end ) {
+ if( !trace || !bsp )
+ return 0;
+
+ trace->in_start = start;
+ trace->in_end = end;
+ return bsp_trace( trace, bsp, hull );
+}
diff --git a/src/game/world/trace.h b/src/game/world/trace.h
new file mode 100644
index 0000000..8a0f4eb
--- /dev/null
+++ b/src/game/world/trace.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "bsp.h"
+#include "../../util/aabb.h"
+
+// quake uses 1 / 32
+const F32 BSP_TRACE_EPSILON = 1.f / 64.f;
+
+struct BSP_TRACE {
+ VEC3 in_start;
+ VEC3 in_end;
+
+ U8 hit;
+ F32 frac;
+ VEC3 point;
+ VEC3 normal;
+ U32 propid;
+ I32 leafid;
+ U32 faceid;
+};
+
+extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp );
+extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, VEC3 start, VEC3 end );
+
+extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB aabb );
+extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB aabb, VEC3 start, VEC3 end );
+
+extern U8 bsp_trace( BSP_TRACE* trace );
+extern U8 bsp_trace( BSP_TRACE* trace, VEC3 start, VEC3 end );
diff --git a/src/game/world/world.cpp b/src/game/world/world.cpp
new file mode 100644
index 0000000..337b496
--- /dev/null
+++ b/src/game/world/world.cpp
@@ -0,0 +1,14 @@
+#include "world.h"
+#include "../objlist.h"
+
+WORLD* world_create( WORLD_MAP* map ) {
+ WORLD* w = obj_add<WORLD>( "root" );
+ w->map = map;
+
+ return w;
+}
+
+STAT world_populate_entities( WORLD *world ) {
+
+ return STAT_OK;
+}
diff --git a/src/game/world/world.h b/src/game/world/world.h
new file mode 100644
index 0000000..229f235
--- /dev/null
+++ b/src/game/world/world.h
@@ -0,0 +1,11 @@
+#pragma once
+#include "../object.h"
+
+struct WORLD : OBJECT {
+ static const U32 CLASSID = OBJCLASS_WORLD;
+
+ struct WORLD_MAP* map;
+};
+
+extern WORLD* world_create( WORLD_MAP* map );
+extern STAT world_populate_entities( WORLD* world );