move files to src folder
authorhgn <hgodden00@gmail.com>
Sun, 8 Sep 2024 15:36:01 +0000 (16:36 +0100)
committerhgn <hgodden00@gmail.com>
Sun, 8 Sep 2024 15:36:01 +0000 (16:36 +0100)
276 files changed:
addon.c [deleted file]
addon.h [deleted file]
addon_types.c [deleted file]
addon_types.h [deleted file]
audio.c [deleted file]
audio.h [deleted file]
build.c
build.sh
build_control_overlay.c [deleted file]
client.c [deleted file]
client.h [deleted file]
common.h [deleted file]
control_overlay.c [deleted file]
control_overlay.h [deleted file]
depth_compare.h [deleted file]
ent_camera.c [deleted file]
ent_camera.h [deleted file]
ent_challenge.c [deleted file]
ent_challenge.h [deleted file]
ent_glider.c [deleted file]
ent_glider.h [deleted file]
ent_miniworld.c [deleted file]
ent_miniworld.h [deleted file]
ent_npc.c [deleted file]
ent_npc.h [deleted file]
ent_objective.c [deleted file]
ent_objective.h [deleted file]
ent_region.c [deleted file]
ent_region.h [deleted file]
ent_relay.c [deleted file]
ent_relay.h [deleted file]
ent_route.c [deleted file]
ent_route.h [deleted file]
ent_skateshop.c [deleted file]
ent_skateshop.h [deleted file]
ent_tornado.c [deleted file]
ent_tornado.h [deleted file]
ent_traffic.c [deleted file]
ent_traffic.h [deleted file]
entity.c [deleted file]
entity.h [deleted file]
font.h [deleted file]
freecam.c [deleted file]
freecam.h [deleted file]
gameserver.c [deleted file]
gameserver.h [deleted file]
gameserver_db.h [deleted file]
gui.h [deleted file]
input.h [deleted file]
menu.c [deleted file]
menu.h [deleted file]
model.c [deleted file]
model.h [deleted file]
network.c [deleted file]
network.h [deleted file]
network_common.h [deleted file]
network_compression.h [deleted file]
network_msg.h [deleted file]
particle.c [deleted file]
particle.h [deleted file]
physics_test.h [deleted file]
player.c [deleted file]
player.h [deleted file]
player_api.h [deleted file]
player_basic_info.c [deleted file]
player_basic_info.h [deleted file]
player_common.c [deleted file]
player_common.h [deleted file]
player_dead.c [deleted file]
player_dead.h [deleted file]
player_drive.c [deleted file]
player_drive.h [deleted file]
player_effects.c [deleted file]
player_effects.h [deleted file]
player_glide.c [deleted file]
player_glide.h [deleted file]
player_model.h [deleted file]
player_ragdoll.c [deleted file]
player_ragdoll.h [deleted file]
player_remote.c [deleted file]
player_remote.h [deleted file]
player_render.c [deleted file]
player_render.h [deleted file]
player_replay.c [deleted file]
player_replay.h [deleted file]
player_skate.c [deleted file]
player_skate.h [deleted file]
player_walk.c [deleted file]
player_walk.h [deleted file]
render.c [deleted file]
render.h [deleted file]
save.c [deleted file]
save.h [deleted file]
scene.c [deleted file]
scene.h [deleted file]
scene_rigidbody.h [deleted file]
shader_props.h [deleted file]
skaterift.c [deleted file]
skaterift.h [deleted file]
skaterift_lib.c [deleted file]
skeleton.h [deleted file]
src/addon.c [new file with mode: 0644]
src/addon.h [new file with mode: 0644]
src/addon_types.c [new file with mode: 0644]
src/addon_types.h [new file with mode: 0644]
src/audio.c [new file with mode: 0644]
src/audio.h [new file with mode: 0644]
src/build_control_overlay.c [new file with mode: 0644]
src/client.c [new file with mode: 0644]
src/client.h [new file with mode: 0644]
src/common.h [new file with mode: 0644]
src/control_overlay.c [new file with mode: 0644]
src/control_overlay.h [new file with mode: 0644]
src/depth_compare.h [new file with mode: 0644]
src/ent_camera.c [new file with mode: 0644]
src/ent_camera.h [new file with mode: 0644]
src/ent_challenge.c [new file with mode: 0644]
src/ent_challenge.h [new file with mode: 0644]
src/ent_glider.c [new file with mode: 0644]
src/ent_glider.h [new file with mode: 0644]
src/ent_miniworld.c [new file with mode: 0644]
src/ent_miniworld.h [new file with mode: 0644]
src/ent_npc.c [new file with mode: 0644]
src/ent_npc.h [new file with mode: 0644]
src/ent_objective.c [new file with mode: 0644]
src/ent_objective.h [new file with mode: 0644]
src/ent_region.c [new file with mode: 0644]
src/ent_region.h [new file with mode: 0644]
src/ent_relay.c [new file with mode: 0644]
src/ent_relay.h [new file with mode: 0644]
src/ent_route.c [new file with mode: 0644]
src/ent_route.h [new file with mode: 0644]
src/ent_skateshop.c [new file with mode: 0644]
src/ent_skateshop.h [new file with mode: 0644]
src/ent_tornado.c [new file with mode: 0644]
src/ent_tornado.h [new file with mode: 0644]
src/ent_traffic.c [new file with mode: 0644]
src/ent_traffic.h [new file with mode: 0644]
src/entity.c [new file with mode: 0644]
src/entity.h [new file with mode: 0644]
src/font.h [new file with mode: 0644]
src/freecam.c [new file with mode: 0644]
src/freecam.h [new file with mode: 0644]
src/gameserver.c [new file with mode: 0644]
src/gameserver.h [new file with mode: 0644]
src/gameserver_db.h [new file with mode: 0644]
src/gui.h [new file with mode: 0644]
src/input.h [new file with mode: 0644]
src/menu.c [new file with mode: 0644]
src/menu.h [new file with mode: 0644]
src/model.c [new file with mode: 0644]
src/model.h [new file with mode: 0644]
src/network.c [new file with mode: 0644]
src/network.h [new file with mode: 0644]
src/network_common.h [new file with mode: 0644]
src/network_compression.h [new file with mode: 0644]
src/network_msg.h [new file with mode: 0644]
src/particle.c [new file with mode: 0644]
src/particle.h [new file with mode: 0644]
src/physics_test.h [new file with mode: 0644]
src/player.c [new file with mode: 0644]
src/player.h [new file with mode: 0644]
src/player_api.h [new file with mode: 0644]
src/player_basic_info.c [new file with mode: 0644]
src/player_basic_info.h [new file with mode: 0644]
src/player_common.c [new file with mode: 0644]
src/player_common.h [new file with mode: 0644]
src/player_dead.c [new file with mode: 0644]
src/player_dead.h [new file with mode: 0644]
src/player_drive.c [new file with mode: 0644]
src/player_drive.h [new file with mode: 0644]
src/player_effects.c [new file with mode: 0644]
src/player_effects.h [new file with mode: 0644]
src/player_glide.c [new file with mode: 0644]
src/player_glide.h [new file with mode: 0644]
src/player_model.h [new file with mode: 0644]
src/player_ragdoll.c [new file with mode: 0644]
src/player_ragdoll.h [new file with mode: 0644]
src/player_remote.c [new file with mode: 0644]
src/player_remote.h [new file with mode: 0644]
src/player_render.c [new file with mode: 0644]
src/player_render.h [new file with mode: 0644]
src/player_replay.c [new file with mode: 0644]
src/player_replay.h [new file with mode: 0644]
src/player_skate.c [new file with mode: 0644]
src/player_skate.h [new file with mode: 0644]
src/player_walk.c [new file with mode: 0644]
src/player_walk.h [new file with mode: 0644]
src/render.c [new file with mode: 0644]
src/render.h [new file with mode: 0644]
src/save.c [new file with mode: 0644]
src/save.h [new file with mode: 0644]
src/scene.c [new file with mode: 0644]
src/scene.h [new file with mode: 0644]
src/scene_rigidbody.h [new file with mode: 0644]
src/shader_props.h [new file with mode: 0644]
src/skaterift.c [new file with mode: 0644]
src/skaterift.h [new file with mode: 0644]
src/skaterift_lib.c [new file with mode: 0644]
src/skeleton.h [new file with mode: 0644]
src/steam.c [new file with mode: 0644]
src/steam.h [new file with mode: 0644]
src/traffic.h [new file with mode: 0644]
src/trail.c [new file with mode: 0644]
src/trail.h [new file with mode: 0644]
src/vehicle.c [new file with mode: 0644]
src/vehicle.h [new file with mode: 0644]
src/workshop.c [new file with mode: 0644]
src/workshop.h [new file with mode: 0644]
src/world.c [new file with mode: 0644]
src/world.h [new file with mode: 0644]
src/world_audio.c [new file with mode: 0644]
src/world_audio.h [new file with mode: 0644]
src/world_entity.c [new file with mode: 0644]
src/world_entity.h [new file with mode: 0644]
src/world_gate.c [new file with mode: 0644]
src/world_gate.h [new file with mode: 0644]
src/world_gen.c [new file with mode: 0644]
src/world_gen.h [new file with mode: 0644]
src/world_info.h [new file with mode: 0644]
src/world_load.c [new file with mode: 0644]
src/world_load.h [new file with mode: 0644]
src/world_map.c [new file with mode: 0644]
src/world_map.h [new file with mode: 0644]
src/world_physics.c [new file with mode: 0644]
src/world_physics.h [new file with mode: 0644]
src/world_render.c [new file with mode: 0644]
src/world_render.h [new file with mode: 0644]
src/world_routes.c [new file with mode: 0644]
src/world_routes.h [new file with mode: 0644]
src/world_routes_ui.c [new file with mode: 0644]
src/world_routes_ui.h [new file with mode: 0644]
src/world_sfd.c [new file with mode: 0644]
src/world_sfd.h [new file with mode: 0644]
src/world_volumes.c [new file with mode: 0644]
src/world_volumes.h [new file with mode: 0644]
src/world_water.c [new file with mode: 0644]
src/world_water.h [new file with mode: 0644]
steam.c [deleted file]
steam.h [deleted file]
traffic.h [deleted file]
trail.c [deleted file]
trail.h [deleted file]
vehicle.c [deleted file]
vehicle.h [deleted file]
workshop.c [deleted file]
workshop.h [deleted file]
world.c [deleted file]
world.h [deleted file]
world_audio.c [deleted file]
world_audio.h [deleted file]
world_entity.c [deleted file]
world_entity.h [deleted file]
world_gate.c [deleted file]
world_gate.h [deleted file]
world_gen.c [deleted file]
world_gen.h [deleted file]
world_info.h [deleted file]
world_load.c [deleted file]
world_load.h [deleted file]
world_map.c [deleted file]
world_map.h [deleted file]
world_physics.c [deleted file]
world_physics.h [deleted file]
world_render.c [deleted file]
world_render.h [deleted file]
world_routes.c [deleted file]
world_routes.h [deleted file]
world_routes_ui.c [deleted file]
world_routes_ui.h [deleted file]
world_sfd.c [deleted file]
world_sfd.h [deleted file]
world_volumes.c [deleted file]
world_volumes.h [deleted file]
world_water.c [deleted file]
world_water.h [deleted file]

diff --git a/addon.c b/addon.c
deleted file mode 100644 (file)
index 6769b5a..0000000
--- a/addon.c
+++ /dev/null
@@ -1,881 +0,0 @@
-#include "vg/vg_engine.h"
-#include "vg/vg_io.h"
-#include "vg/vg_loader.h"
-#include "addon.h"
-#include "addon_types.h"
-#include "vg/vg_msg.h"
-#include "steam.h"
-#include "workshop.h"
-#include <string.h>
-
-struct addon_system addon_system;
-
-u32 addon_count( enum addon_type type, u32 ignoreflags )
-{
-   if( ignoreflags ){
-      u32 typecount = 0, count = 0;
-      for( u32 i=0; typecount<addon_count( type, 0 ); i++ ){
-         addon_reg *reg = &addon_system.registry[i];
-         if( reg->alias.type == type ){
-            typecount ++;
-
-            if( reg->flags & ignoreflags )
-               continue;
-            
-            count ++;
-         }
-      }
-
-      return count;
-   }
-   else
-      return addon_system.registry_type_counts[ type ];
-}
-
-
-/* these kind of suck, oh well. */
-addon_reg *get_addon_from_index( enum addon_type type, u32 index, 
-                                 u32 ignoreflags )
-{
-   u32 typecount = 0, count = 0;
-   for( u32 i=0; typecount<addon_count(type,0); i++ ){
-      addon_reg *reg = &addon_system.registry[i];
-      if( reg->alias.type == type ){
-         typecount ++;
-
-         if( reg->flags & ignoreflags )
-            continue;
-
-         if( index == count )
-            return reg;
-
-         count ++;
-      }
-   }
-
-   return NULL;
-}
-
-u32 get_index_from_addon( enum addon_type type, addon_reg *a )
-{
-   u32 count = 0;
-   for( u32 i=0; count<addon_system.registry_type_counts[type]; i++ ){
-      addon_reg *reg = &addon_system.registry[i];
-      if( reg->alias.type == type ){
-         if( reg == a )
-            return count;
-
-         count ++;
-      }
-   }
-
-   return 0xffffffff;
-}
-
-u32 addon_match( addon_alias *alias )
-{
-   if( alias->type == k_addon_type_none ) return 0xffffffff;
-
-   u32 foldername_djb2 = 0;
-   if( !alias->workshop_id )
-      foldername_djb2 = vg_strdjb2( alias->foldername );
-
-   u32 count = 0;
-   for( u32 i=0; count<addon_system.registry_type_counts[alias->type]; i++ ){
-      addon_reg *reg = &addon_system.registry[i];
-      if( reg->alias.type == alias->type ){
-         
-         if( alias->workshop_id ){
-            if( alias->workshop_id == reg->alias.workshop_id )
-               return count;
-         }
-         else{
-            if( reg->foldername_hash == foldername_djb2 ){
-               if( !strcmp( reg->alias.foldername, alias->foldername ) ){
-                  return count;
-               }
-            }
-         }
-
-         count ++;
-      }
-   }
-
-   return 0xffffffff;
-}
-
-/*
- * Create a string version of addon alias in buf
- */
-void addon_alias_uid( addon_alias *alias, char buf[ADDON_UID_MAX] )
-{
-   if( alias->workshop_id ){
-      snprintf( buf, 128, "sr%03d-steam-"PRINTF_U64, 
-            alias->type, alias->workshop_id );
-   }
-   else {
-      snprintf( buf, 128, "sr%03d-local-%s",
-            alias->type, alias->foldername );
-   }
-}
-
-/*
- * equality check
- */
-int addon_alias_eq( addon_alias *a, addon_alias *b )
-{
-   if( a->type == b->type ){
-      if( a->workshop_id == b->workshop_id ){
-         if( a->workshop_id )
-            return 1;
-         else
-            return !strcmp( a->foldername, b->foldername );
-      }
-      else
-         return 0;
-   }
-   else return 0;
-}
-
-/*
- * make alias represent NULL.
- */
-void invalidate_addon_alias( addon_alias *alias )
-{
-   alias->type = k_addon_type_none;
-   alias->workshop_id = 0;
-   alias->foldername[0] = '\0';
-}
-
-/*
- * parse uid to alias. returns 1 if successful
- */
-int addon_uid_to_alias( const char *uid, addon_alias *alias )
-{
-/*           1
- * 01234567890123
- * sr&&&-@@@@@-#*
- *    |    |   |
- *  type   |   id
- *         |
- *     location
- */
-   if( strlen(uid) < 13 ){
-      invalidate_addon_alias( alias );
-      return 0;
-   }
-   if( !((uid[0] == 's') && (uid[1] == 'r')) ){
-      invalidate_addon_alias( alias );
-      return 0;
-   }
-
-   char type[4];
-   memcpy( type, uid+2, 3 );
-   type[3] = '\0';
-   alias->type = atoi(type);
-
-   char location[6];
-   memcpy( location, uid+6, 5 );
-   location[5] = '\0';
-
-   if( !strcmp(location,"steam") )
-      alias->workshop_id = atoll( uid+12 );
-   else if( !strcmp(location,"local") ){
-      alias->workshop_id = 0;
-      vg_strncpy( uid+12, alias->foldername, 64, k_strncpy_always_add_null );
-   }
-   else{
-      invalidate_addon_alias( alias );
-      return 0;
-   }
-
-   return 1;
-}
-
-void addon_system_init( void )
-{
-   u32 reg_size   = sizeof(addon_reg)*ADDON_MOUNTED_MAX;
-   addon_system.registry = vg_linear_alloc( vg_mem.rtmemory, reg_size );
-
-   for( u32 type=0; type<k_addon_type_max; type++ ){
-      struct addon_type_info *inf = &addon_type_infos[type];
-      struct addon_cache *cache = &addon_system.cache[type];
-
-      if( inf->cache_count ){
-         /* create the allocations pool */
-         u32 alloc_size = sizeof(struct addon_cache_entry)*inf->cache_count;
-         cache->allocs = vg_linear_alloc( vg_mem.rtmemory, alloc_size );
-         memset( cache->allocs, 0, alloc_size );
-
-         cache->pool.buffer = cache->allocs;
-         cache->pool.count  = inf->cache_count;
-         cache->pool.stride = sizeof( struct addon_cache_entry );
-         cache->pool.offset = offsetof( struct addon_cache_entry, poolnode );
-         vg_pool_init( &cache->pool );
-
-         /* create the real memory */
-         u32 cache_size = inf->cache_stride*inf->cache_count;
-         cache->items = vg_linear_alloc( vg_mem.rtmemory, cache_size );
-         cache->stride = inf->cache_stride;
-         memset( cache->items, 0, cache_size );
-
-         for( i32 j=0; j<inf->cache_count; j++ ){
-            struct addon_cache_entry *alloc = &cache->allocs[j];
-            alloc->reg_ptr   = NULL;
-            alloc->reg_index = 0xffffffff;
-         }
-      }
-   }
-}
-
-/*
- * Scanning routines
- * -----------------------------------------------------------------------------
- */
-
-/*
- * Reciever for scan completion. copies the registry counts back into main fred
- */
-void async_addon_reg_update( void *data, u32 size )
-{
-   vg_info( "Registry update notify\n" );
-   
-   for( u32 i=0; i<k_addon_type_max; i++ ){
-      addon_system.registry_type_counts[i] = 0;
-   }
-
-   for( u32 i=0; i<addon_system.registry_count; i++ ){
-      enum addon_type type = addon_system.registry[i].alias.type;
-      addon_system.registry_type_counts[ type ] ++;
-   }
-}
-
-static void addon_set_foldername( addon_reg *reg, const char name[64] ){
-   vg_strncpy( name, reg->alias.foldername, 64, k_strncpy_always_add_null );
-   reg->foldername_hash = vg_strdjb2( reg->alias.foldername );
-}
-
-/*
- * Create a new registry 
- */
-static addon_reg *addon_alloc_reg( PublishedFileId_t workshop_id,
-                                      enum addon_type type ){
-   if( addon_system.registry_count == ADDON_MOUNTED_MAX ){
-      vg_error( "You have too many addons installed!\n" );
-      return NULL;
-   }
-
-   addon_reg *reg = &addon_system.registry[ addon_system.registry_count ];
-   reg->flags = 0;
-   reg->metadata_len = 0;
-   reg->cache_id = 0;
-   reg->state = k_addon_state_indexed;
-   reg->alias.workshop_id = workshop_id;
-   reg->alias.foldername[0] = '\0';
-   reg->alias.type = type;
-
-   if( workshop_id ){
-      char foldername[64];
-      snprintf( foldername, 64, PRINTF_U64, workshop_id );
-      addon_set_foldername( reg, foldername );
-   }
-   return reg;
-}
-
-/*
- * If the addon.inf exists int the folder, load into the reg
- */
-static int addon_try_load_metadata( addon_reg *reg, vg_str folder_path ){
-   vg_str meta_path = folder_path;
-   vg_strcat( &meta_path, "/addon.inf" );
-   if( !vg_strgood( &meta_path ) ){
-      vg_error( "The metadata path is too long\n" );
-      return 0;
-   }
-
-   FILE *fp = fopen( meta_path.buffer, "rb" );
-   if( !fp ){
-      vg_error( "Could not open the '%s'\n", meta_path.buffer );
-      return 0;
-   }
-
-   reg->metadata_len = fread( reg->metadata, 1, 512, fp );
-   if( reg->metadata_len != 512 ){
-      if( !feof(fp) ){
-         fclose(fp);
-         vg_error( "unknown error codition" );
-         reg->metadata_len = 0;
-         return 0;
-      }
-   }
-   fclose(fp);
-   return 1;
-}
-
-static void addon_print_info( addon_reg *reg ){
-   vg_info( "addon_reg #%u{\n", addon_system.registry_count );
-   vg_info( "  type: %d\n", reg->alias.type );
-   vg_info( "  workshop_id: " PRINTF_U64 "\n", reg->alias.workshop_id );
-   vg_info( "  folder: [%u]%s\n", reg->foldername_hash, reg->alias.foldername );
-   vg_info( "  metadata_len: %u\n", reg->metadata_len );
-   vg_info( "  cache_id: %hu\n", reg->cache_id );
-   vg_info( "}\n" );
-}
-
-static void addon_mount_finish( addon_reg *reg ){
-#if 0
-   addon_print_info( reg );
-#endif
-   addon_system.registry_count ++;
-}
-
-/*
- * Mount a fully packaged addon, one that certainly has a addon.inf
- */
-static addon_reg *addon_mount_workshop_folder( PublishedFileId_t workshop_id,
-                                                  vg_str folder_path )
-{
-   addon_reg *reg = addon_alloc_reg( workshop_id, k_addon_type_none );
-   if( !reg ) return NULL;
-
-   if( !addon_try_load_metadata( reg, folder_path ) ){
-      return NULL;
-   }
-
-   enum addon_type type = k_addon_type_none;
-   vg_msg msg;
-   vg_msg_init( &msg, reg->metadata, reg->metadata_len );
-
-   if( vg_msg_seekframe( &msg, "workshop" ))
-   {
-      vg_msg_getkvintg( &msg, "type", k_vg_msg_u32, &type, NULL );
-   }
-
-   if( type == k_addon_type_none )
-   {
-      vg_error( "Cannot determine addon type\n" );
-      return NULL;
-   }
-
-   reg->alias.type = type;
-   addon_mount_finish( reg );
-   return reg;
-}
-
-/*
- * Mount a local folder. may or may not have addon.inf
- */
-addon_reg *addon_mount_local_addon( const char *folder,
-                                    enum addon_type type,
-                                    const char *content_ext )
-{
-   char folder_path_buf[4096];
-   vg_str folder_path;
-   vg_strnull( &folder_path, folder_path_buf, 4096 );
-   vg_strcat( &folder_path, folder );
-
-   const char *folder_name = vg_strch( &folder_path, '/' )+1;
-   u32 folder_hash = vg_strdjb2(folder_name);
-   for( u32 i=0; i<addon_system.registry_count; i++ ){
-      addon_reg *reg = &addon_system.registry[i];
-
-      if( (reg->alias.type == type) && (reg->foldername_hash == folder_hash) ){
-         if( !strcmp( reg->alias.foldername, folder_name ) ){
-            reg->state = k_addon_state_indexed;
-            return reg;
-         }
-      }
-   }
-
-   addon_reg *reg = addon_alloc_reg( 0, type );
-   if( !reg ) return NULL;
-   addon_set_foldername( reg, folder_name );
-   addon_try_load_metadata( reg, folder_path );
-
-   if( reg->metadata_len == 0 ){
-      /* create our own content commands */
-      vg_msg msg;
-      vg_msg_init( &msg, reg->metadata, sizeof(reg->metadata) );
-
-      u32 content_count = 0;
-
-      vg_strcat( &folder_path, "" );
-      vg_warn( "Creating own metadata for: %s\n", folder_path.buffer );
-
-      vg_dir subdir;
-      if( !vg_dir_open(&subdir, folder_path.buffer) ){
-         vg_error( "Failed to open '%s'\n", folder_path.buffer );
-         return NULL;
-      }
-
-      while( vg_dir_next_entry(&subdir) ){
-         if( vg_dir_entry_type(&subdir) == k_vg_entry_type_file ){
-            const char *fname = vg_dir_entry_name(&subdir);
-            vg_str file = folder_path;
-            vg_strcat( &file, "/" );
-            vg_strcat( &file, fname );
-            if( !vg_strgood( &file ) ) continue;
-
-            char *ext = vg_strch( &file, '.' );
-            if( !ext ) continue;
-            if( strcmp(ext,content_ext) ) continue;
-            
-            vg_msg_wkvstr( &msg, "content", fname );
-            content_count ++;
-         }
-      }
-      vg_dir_close(&subdir);
-
-      if( !content_count ) return NULL;
-      if( msg.error == k_vg_msg_error_OK )
-         reg->metadata_len = msg.cur.co;
-      else{
-         vg_error( "Error creating metadata: %d\n", msg.error );
-         return NULL;
-      }
-   }
-
-   addon_mount_finish( reg );
-   return reg;
-}
-
-/*
- * Check all subscribed items
- */
-void addon_mount_workshop_items(void)
-{
-   if( skaterift.demo_mode ){
-      vg_info( "Won't load workshop items in demo mode\n" );
-      return;
-   }
-   if( !steam_ready ) return;
-
-   /*
-    * Steam workshop scan
-    */
-   vg_info( "Mounting steam workshop subscriptions\n" );
-   PublishedFileId_t workshop_ids[ ADDON_MOUNTED_MAX ];
-   u32 workshop_count = ADDON_MOUNTED_MAX;
-
-   vg_async_item *call = vg_async_alloc(
-                           sizeof(struct async_workshop_installed_files_info));
-   struct async_workshop_installed_files_info *info = call->payload;
-   info->buffer = workshop_ids;
-   info->len = &workshop_count;
-   vg_async_dispatch( call, async_workshop_get_installed_files );
-   vg_async_stall();
-
-   for( u32 j=0; j<workshop_count; j++ ){
-      /* check for existance in both our caches
-       * ----------------------------------------------------------*/
-      PublishedFileId_t id = workshop_ids[j];
-      for( u32 i=0; i<addon_system.registry_count; i++ ){
-         addon_reg *reg = &addon_system.registry[i];
-
-         if( reg->alias.workshop_id == id ){
-            reg->state = k_addon_state_indexed;
-            goto next_file_workshop;
-         }
-      }
-
-      vg_async_item *call1 = 
-         vg_async_alloc( sizeof(struct async_workshop_filepath_info) );
-
-      char path[ 4096 ];
-
-      struct async_workshop_filepath_info *info = call1->payload;
-      info->buf = path;
-      info->id = id;
-      info->len = VG_ARRAY_LEN(path);
-      vg_async_dispatch( call1, async_workshop_get_filepath );
-      vg_async_stall(); /* too bad! */
-
-      vg_str folder = {.buffer = path, .i=strlen(path), .len=4096};
-      addon_mount_workshop_folder( id, folder );
-next_file_workshop:;
-   }
-}
-
-/*
- * Scan a local content folder for addons. It must find at least one file with 
- * the specified content_ext to be considered.
- */
-void addon_mount_content_folder( enum addon_type type,
-                                 const char *base_folder, 
-                                 const char *content_ext )
-{
-   vg_info( "Mounting addons(type:%d) matching skaterift/%s/*/*%s\n", 
-                  type, base_folder, content_ext );
-
-   char path_buf[4096];
-   vg_str path;
-   vg_strnull( &path, path_buf, 4096 );
-   vg_strcat( &path, base_folder );
-
-   vg_dir dir;
-   if( !vg_dir_open(&dir,path.buffer) ){
-      vg_error( "vg_dir_open('%s') failed\n", path.buffer );
-      return;
-   }
-
-   vg_strcat(&path,"/");
-
-   while( vg_dir_next_entry(&dir) ){
-      if( vg_dir_entry_type(&dir) == k_vg_entry_type_dir ){
-         const char *d_name = vg_dir_entry_name(&dir);
-
-         vg_str folder = path;
-         if( strlen( d_name ) > ADDON_FOLDERNAME_MAX ){
-            vg_warn( "folder too long: %s\n", d_name );
-            continue;
-         }
-
-         vg_strcat( &folder, d_name );
-         if( !vg_strgood( &folder ) ) continue;
-
-         addon_mount_local_addon( folder.buffer, type, content_ext );
-      }
-   }
-   vg_dir_close(&dir);
-}
-
-/*
- * write the full path of the addon's folder into the vg_str
- */
-int addon_get_content_folder( addon_reg *reg, vg_str *folder, int async)
-{
-   if( reg->alias.workshop_id ){
-      struct async_workshop_filepath_info *info = NULL;
-      vg_async_item *call = NULL;
-
-      if( async ){
-         call = vg_async_alloc( sizeof(struct async_workshop_filepath_info) );
-         info = call->payload;
-      }
-      else 
-         info = alloca( sizeof(struct async_workshop_filepath_info) );
-
-      info->buf = folder->buffer;
-      info->id = reg->alias.workshop_id;
-      info->len = folder->len;
-
-      if( async ){
-         vg_async_dispatch( call, async_workshop_get_filepath );
-         vg_async_stall(); /* too bad! */
-      }
-      else {
-         async_workshop_get_filepath( info, 0 );
-      }
-
-      if( info->buf[0] == '\0' ){
-         vg_error( "Failed SteamAPI_GetItemInstallInfo(" PRINTF_U64 ")\n",
-                     reg->alias.workshop_id );
-         return 0;
-      }
-      folder->i = strlen( folder->buffer );
-      return 1;
-   }
-   else{
-      folder->i = 0;
-      
-      const char *local_folder = 
-         addon_type_infos[reg->alias.type].local_content_folder;
-
-      if( !local_folder ) return 0;
-      vg_strcat( folder, local_folder );
-      vg_strcat( folder, reg->alias.foldername );
-      return 1;
-   }
-}
-
-/*
- * Return existing cache id if reg_index points to a registry with its cache
- * already set.
- */
-u16 addon_cache_fetch( enum addon_type type, u32 reg_index )
-{
-   addon_reg *reg = NULL;
-
-   if( reg_index < addon_count( type, 0 ) ){
-      reg = get_addon_from_index( type, reg_index, 0 );
-      if( reg->cache_id ) 
-         return reg->cache_id;
-   }
-
-   return 0;
-}
-
-/*
- * Allocate a new cache item from the pool
- */
-u16 addon_cache_alloc( enum addon_type type, u32 reg_index )
-{
-   struct addon_cache *cache = &addon_system.cache[ type ];
-
-   u16 new_id = vg_pool_lru( &cache->pool );
-   struct addon_cache_entry *new_entry = vg_pool_item( &cache->pool, new_id );
-
-   addon_reg *reg = NULL;
-   if( reg_index < addon_count( type, 0 ) )
-      reg = get_addon_from_index( type, reg_index, 0 );
-
-   if( new_entry ){
-      if( new_entry->reg_ptr )
-         new_entry->reg_ptr->cache_id = 0;
-
-      if( reg )
-         reg->cache_id = new_id;
-
-      new_entry->reg_ptr = reg;
-      new_entry->reg_index = reg_index;
-      return new_id;
-   }
-   else{
-      vg_error( "cache full (type: %u)!\n", type );
-      return 0;
-   }
-}
-
-/*
- * Get the real item data for cache id 
- */
-void *addon_cache_item( enum addon_type type, u16 id )
-{
-   if( !id ) return NULL;
-
-   struct addon_cache *cache = &addon_system.cache[type];
-   return cache->items + ((size_t)(id-1) * cache->stride);
-}
-
-/*
- * Get the real item data for cache id ONLY if the item is completely loaded.
- */
-void *addon_cache_item_if_loaded( enum addon_type type, u16 id )
-{
-   if( !id ) return NULL;
-
-   struct addon_cache *cache = &addon_system.cache[type];
-   struct addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
-
-   if( entry->state == k_addon_cache_state_loaded )
-      return addon_cache_item( type, id );
-   else return NULL;
-}
-
-/* 
- * Updates the item state from the main thread
- */
-void async_addon_setstate( void *_entry, u32 _state )
-{
-   addon_cache_entry *entry = _entry;
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-   entry->state = _state;
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-   vg_success( "   loaded (%s)\n", entry->reg_ptr->alias.foldername );
-}
-
-/*
- * Handles the loading of an individual item
- */
-static int addon_cache_load_request( enum addon_type type, u16 id,
-                                     addon_reg *reg, vg_str folder ){
-   
-   /* load content files
-    * --------------------------------- */
-   vg_str content_path = folder;
-
-   vg_msg msg;
-   vg_msg_init( &msg, reg->metadata, reg->metadata_len );
-
-   const char *kv_content = vg_msg_getkvstr( &msg, "content" );
-   if( kv_content ){
-      vg_strcat( &content_path, "/" );
-      vg_strcat( &content_path, kv_content );
-   }
-   else{
-      vg_error( "   No content paths in metadata\n" );
-      return 0;
-   }
-
-   if( !vg_strgood( &content_path ) ) {
-      vg_error( "   Metadata path too long\n" );
-      return 0;
-   }
-   
-   if( type == k_addon_type_board ){
-      struct player_board *board = addon_cache_item( type, id );
-      player_board_load( board, content_path.buffer );
-      return 1;
-   }
-   else if( type == k_addon_type_player ){
-      struct player_model *model = addon_cache_item( type, id );
-      player_model_load( model, content_path.buffer );
-      return 1;
-   }
-   else {
-      return 0;
-   }
-
-   return 0;
-}
-
-static void addon_cache_free_item( enum addon_type type, u16 id ){
-   if( type == k_addon_type_board ){
-      struct player_board *board = addon_cache_item( type, id );
-      player_board_unload( board );
-   }
-   else if( type == k_addon_type_player ){
-      struct player_model *model = addon_cache_item( type, id );
-      player_model_unload( model );
-   }
-}
-
-/*
- * Goes over cache item load requests and calls the above ^
- */
-static void T1_addon_cache_load_loop(void *_)
-{
-   vg_info( "Running load loop\n" );
-   char path_buf[4096];
-
-   for( u32 type=0; type<k_addon_type_max; type++ )
-   {
-      struct addon_cache *cache = &addon_system.cache[type];
-
-      for( u32 id=1; id<=cache->pool.count; id++ )
-      {
-         addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
-
-         SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-         if( entry->state == k_addon_cache_state_load_request )
-         {
-            vg_info( "process cache load request (%u#%u, reg:%u)\n",
-                        type, id, entry->reg_index );
-
-            if( entry->reg_index >= addon_count(type,0) )
-            {
-               /* should maybe have a different value for this case */
-               entry->state = k_addon_cache_state_none;
-               SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-               continue;
-            }
-
-            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-
-            /* continue with the request */
-            addon_reg *reg = get_addon_from_index( type, entry->reg_index, 0 );
-            entry->reg_ptr = reg;
-
-            vg_str folder;
-            vg_strnull( &folder, path_buf, 4096 );
-            if( addon_get_content_folder( reg, &folder, 1 ) )
-            {
-               if( addon_cache_load_request( type, id, reg, folder ) )
-               {
-                  vg_async_call( async_addon_setstate, 
-                                 entry, k_addon_cache_state_loaded );
-                  continue;
-               }
-            }
-            
-            vg_warn( "cache item did not load (%u#%u)\n", type, id );
-            SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-            entry->state = k_addon_cache_state_none;
-            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-         }
-         else
-            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-      }
-   }
-}
-
-void addon_system_pre_update(void)
-{
-   if( !vg_loader_availible() ) return;
-
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-   for( u32 type=0; type<k_addon_type_max; type++ )
-   {
-      struct addon_cache *cache = &addon_system.cache[type];
-
-      for( u32 id=1; id<=cache->pool.count; id++ )
-      {
-         addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
-         if( entry->state == k_addon_cache_state_load_request )
-         {
-            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-            vg_loader_start( T1_addon_cache_load_loop, NULL );
-            return;
-         }
-      }
-   }
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-}
-
-/*
- * Perform the cache interactions required to create a viewslot which will
- * eventually be loaded by other parts of the system.
- */
-u16 addon_cache_create_viewer( enum addon_type type, u16 reg_id )
-{
-   struct addon_cache *cache = &addon_system.cache[type];
-   vg_pool *pool = &cache->pool;
-
-   u16 cache_id = addon_cache_fetch( type, reg_id );
-   if( !cache_id ){
-      cache_id = addon_cache_alloc( type, reg_id );
-
-      if( cache_id ){
-         SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-         addon_cache_entry *entry = vg_pool_item( pool, cache_id );
-
-         if( entry->state == k_addon_cache_state_loaded ){
-            addon_cache_free_item( type, cache_id );
-         }
-
-         entry->state = k_addon_cache_state_load_request;
-         SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-      }
-   }
-
-   if( cache_id )
-      vg_pool_watch( pool, cache_id );
-
-   return cache_id;
-}
-
-u16 addon_cache_create_viewer_from_uid( enum addon_type type,
-                                        char uid[ADDON_UID_MAX] )
-{
-   addon_alias q;
-   if( !addon_uid_to_alias( uid, &q ) ) return 0;
-   if( q.type != type ) return 0;
-
-   u32 reg_id = addon_match( &q );
-
-   if( reg_id == 0xffffffff ){
-      vg_warn( "We dont have the addon '%s' installed.\n", uid );
-      return 0;
-   }
-   else {
-      return addon_cache_create_viewer( type, reg_id );
-   }
-}
-
-void addon_cache_watch( enum addon_type type, u16 cache_id )
-{
-   if( !cache_id ) return;
-
-   struct addon_cache *cache = &addon_system.cache[type];
-   vg_pool *pool = &cache->pool;
-   vg_pool_watch( pool, cache_id );
-}
-
-void addon_cache_unwatch( enum addon_type type, u16 cache_id )
-{
-   if( !cache_id ) return;
-
-   struct addon_cache *cache = &addon_system.cache[type];
-   vg_pool *pool = &cache->pool;
-   vg_pool_unwatch( pool, cache_id );
-}
diff --git a/addon.h b/addon.h
deleted file mode 100644 (file)
index c43277c..0000000
--- a/addon.h
+++ /dev/null
@@ -1,108 +0,0 @@
-#pragma once
-#include "vg/vg_steam_ugc.h"
-#include "vg/vg_mem_pool.h"
-#include "vg/vg_string.h"
-#include "addon_types.h"
-
-typedef struct addon_reg addon_reg;
-typedef struct addon_cache_entry addon_cache_entry;
-typedef struct addon_alias addon_alias;
-
-struct addon_alias 
-{
-   enum addon_type type;
-   PublishedFileId_t workshop_id;
-   char foldername[ ADDON_FOLDERNAME_MAX ];
-};
-
-#define ADDON_REG_HIDDEN   0x1
-#define ADDON_REG_MTZERO   0x2
-#define ADDON_REG_CITY     0x4
-#define ADDON_REG_PREMIUM  0x8
-
-struct addon_system
-{
-   struct addon_reg
-   {
-      addon_alias alias;
-      u32 foldername_hash;
-      u8 metadata[512];  /* vg_msg buffer */
-      u32 metadata_len;
-      u32 flags;
-
-      u16 cache_id;
-
-      enum addon_state{
-         k_addon_state_none,
-         k_addon_state_indexed,
-         k_addon_state_indexed_absent /* gone but not forgotten */
-      }
-      state;
-   }
-   *registry;
-   u32 registry_count;
-
-   /* deffered: updates in main thread */
-   u32 registry_type_counts[k_addon_type_max];
-
-   struct addon_cache
-   {
-      struct addon_cache_entry
-      {
-         u32 reg_index;
-         addon_reg *reg_ptr;     /* TODO: only use reg_index? */
-
-         vg_pool_node poolnode;
-
-         enum addon_cache_state{
-            k_addon_cache_state_none,
-            k_addon_cache_state_loaded,
-            k_addon_cache_state_load_request
-         }
-         state;
-      }
-      *allocs;
-      vg_pool pool;
-
-      void *items;  /* the real data */
-      size_t stride;
-   }
-   cache[k_addon_type_max];
-   SDL_SpinLock sl_cache_using_resources;
-}
-extern addon_system;
-
-void addon_system_init( void );
-u32 addon_count( enum addon_type type, u32 ignoreflags );
-addon_reg *get_addon_from_index( enum addon_type type, u32 index, 
-                                 u32 ignoreflags );
-u32 get_index_from_addon( enum addon_type type, addon_reg *a );
-int addon_get_content_folder( addon_reg *reg, vg_str *folder, int async);
-
-/* scanning routines */
-u32 addon_match( addon_alias *alias );
-int addon_alias_eq( addon_alias *a, addon_alias *b );
-void addon_alias_uid( addon_alias *alias, char buf[ADDON_UID_MAX] );
-int addon_uid_to_alias( const char *uid, addon_alias *alias );
-void invalidate_addon_alias( addon_alias *alias );
-void addon_mount_content_folder( enum addon_type type,
-                                    const char *base_folder, 
-                                    const char *content_ext );
-void addon_mount_workshop_items(void);
-void async_addon_reg_update( void *data, u32 size );
-addon_reg *addon_mount_local_addon( const char *folder,
-                                       enum addon_type type,
-                                       const char *content_ext );
-u16 addon_cache_fetch( enum addon_type type, u32 reg_index );
-u16 addon_cache_alloc( enum addon_type type, u32 reg_index );
-void *addon_cache_item( enum addon_type type, u16 id );
-void *addon_cache_item_if_loaded( enum addon_type type, u16 id );
-void async_addon_setstate( void *data, u32 size );
-
-void addon_system_pre_update(void);
-u16 addon_cache_create_viewer( enum addon_type type, u16 reg_id);
-
-void addon_cache_watch( enum addon_type type, u16 cache_id );
-void addon_cache_unwatch( enum addon_type type, u16 cache_id );
-u16 addon_cache_create_viewer_from_uid( enum addon_type type,
-                                        char uid[ADDON_UID_MAX] );
diff --git a/addon_types.c b/addon_types.c
deleted file mode 100644 (file)
index b10a23c..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-#include "player.h"
-#include "player_render.h"
-#include "player_api.h"
-
-struct addon_type_info addon_type_infos[] = 
-{
-   [k_addon_type_board] = { 
-      .local_content_folder = "boards/",
-      .cache_stride = sizeof(struct player_board),
-      .cache_count  = 20
-   },
-   [k_addon_type_player] = {
-      .local_content_folder = "playermodels/",
-      .cache_stride = sizeof(struct player_model),
-      .cache_count  = 20
-   },
-   [k_addon_type_world] = {
-      .local_content_folder = "maps/"
-   }
-};
diff --git a/addon_types.h b/addon_types.h
deleted file mode 100644 (file)
index a244fc0..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-enum addon_type{
-   k_addon_type_none   = 0,
-   k_addon_type_board  = 1,
-   k_addon_type_world  = 2,
-   k_addon_type_player = 3,
-   k_addon_type_max
-};
-
-#define ADDON_FOLDERNAME_MAX 64
-#define ADDON_MOUNTED_MAX 128 /* total count that we have knowledge of */
-#define ADDON_UID_MAX 76
-
-#ifdef VG_ENGINE
-
-struct addon_type_info {
-   size_t cache_stride;
-   u16 cache_count;
-   const char *local_content_folder;
-}
-extern addon_type_infos[];
-
-#endif
diff --git a/audio.c b/audio.c
deleted file mode 100644 (file)
index dda8a2d..0000000
--- a/audio.c
+++ /dev/null
@@ -1,259 +0,0 @@
-#include "world.h"
-#include "audio.h"
-#include "vg/vg_audio_dsp.h"
-
-audio_clip audio_board[] =
-{
-   { .path="sound/skate_hpf.ogg" },
-   { .path="sound/wheel.ogg" },
-   { .path="sound/slide.ogg" },
-   { .path="sound/grind_enter.ogg" },
-   { .path="sound/grind_exit.ogg" },
-   { .path="sound/grind_loop.ogg" },
-   { .path="sound/woodslide.ogg" },
-   { .path="sound/metalscrape.ogg" },
-   { .path="sound/slidetap.ogg" }
-};
-
-audio_clip audio_taps[] =
-{
-   { .path="sound/tap0.ogg" },
-   { .path="sound/tap1.ogg" },
-   { .path="sound/tap2.ogg" },
-   { .path="sound/tap3.ogg" }
-};
-
-audio_clip audio_flips[] =
-{
-   { .path="sound/lf0.ogg" },
-   { .path="sound/lf1.ogg" },
-   { .path="sound/lf2.ogg" },
-   { .path="sound/lf3.ogg" },
-};
-
-audio_clip audio_hits[] =
-{
-   { .path="sound/hit0.ogg" },
-   { .path="sound/hit1.ogg" },
-   { .path="sound/hit2.ogg" },
-   { .path="sound/hit3.ogg" },
-   { .path="sound/hit4.ogg" }
-};
-
-audio_clip audio_splash =
-{ .path = "sound/splash.ogg" };
-
-audio_clip audio_jumps[] = {
-   { .path = "sound/jump0.ogg" },
-   { .path = "sound/jump1.ogg" },
-};
-
-audio_clip audio_footsteps[] = {
- {.path = "sound/step_concrete0.ogg" },
- {.path = "sound/step_concrete1.ogg" },
- {.path = "sound/step_concrete2.ogg" },
- {.path = "sound/step_concrete3.ogg" }
-};
-
-audio_clip audio_footsteps_grass[] = {
- {.path = "sound/step_bush0.ogg" },
- {.path = "sound/step_bush1.ogg" },
- {.path = "sound/step_bush2.ogg" },
- {.path = "sound/step_bush3.ogg" },
- {.path = "sound/step_bush4.ogg" },
- {.path = "sound/step_bush5.ogg" }
-};
-
-audio_clip audio_footsteps_wood[] = {
- {.path = "sound/step_wood0.ogg" },
- {.path = "sound/step_wood1.ogg" },
- {.path = "sound/step_wood2.ogg" },
- {.path = "sound/step_wood3.ogg" },
- {.path = "sound/step_wood4.ogg" },
- {.path = "sound/step_wood5.ogg" }
-};
-
-audio_clip audio_lands[] = {
-   { .path = "sound/land0.ogg" },
-   { .path = "sound/land1.ogg" },
-   { .path = "sound/land2.ogg" },
-   { .path = "sound/landsk0.ogg" },
-   { .path = "sound/landsk1.ogg" },
-   { .path = "sound/onto.ogg" },
-   { .path = "sound/outo.ogg" },
-};
-
-audio_clip audio_water[] = {
-   { .path = "sound/wave0.ogg" },
-   { .path = "sound/wave1.ogg" },
-   { .path = "sound/wave2.ogg" },
-   { .path = "sound/wave3.ogg" },
-   { .path = "sound/wave4.ogg" },
-   { .path = "sound/wave5.ogg" }
-};
-
-audio_clip audio_grass[] = {
-   { .path = "sound/grass0.ogg" },
-   { .path = "sound/grass1.ogg" },
-   { .path = "sound/grass2.ogg" },
-   { .path = "sound/grass3.ogg" },
-};
-
-audio_clip audio_ambience[] =
-{
-   { .path="sound/town_generic.ogg" }
-};
-
-audio_clip audio_gate_pass = {
-   .path = "sound/gate_pass.ogg"
-};
-
-audio_clip audio_gate_lap = {
-   .path = "sound/gate_lap.ogg"
-};
-
-audio_clip audio_gate_ambient = {
-.path = "sound/gate_ambient.ogg"
-};
-
-audio_clip audio_rewind[] = {
-{ .path = "sound/rewind_start.ogg" },
-{ .path = "sound/rewind_end_1.5.ogg" },
-{ .path = "sound/rewind_end_2.5.ogg" },
-{ .path = "sound/rewind_end_6.5.ogg" },
-{ .path = "sound/rewind_clack.ogg" },
-};
-
-audio_clip audio_ui[] = {
-   { .path = "sound/ui_click.ogg" },
-   { .path = "sound/ui_ding.ogg" },
-   { .path = "sound/teleport.ogg" },
-   { .path = "sound/ui_move.ogg" }
-};
-
-audio_clip audio_challenge[] = {
-   { .path = "sound/objective0.ogg" },
-   { .path = "sound/objective1.ogg" },
-   { .path = "sound/objective_win.ogg" },
-   { .path = "sound/ui_good.ogg" },
-   { .path = "sound/ui_inf.ogg" },
-   { .path = "sound/ui_ok.ogg" },
-   { .path = "sound/objective_fail.ogg" }
-};
-
-struct air_synth_data air_audio_data;
-
-static void audio_air_synth_get_samples( void *_data, f32 *buf, u32 count ){
-   struct air_synth_data *data = _data;
-
-   SDL_AtomicLock( &data->sl );
-   f32 spd = data->speed;
-   SDL_AtomicUnlock( &data->sl );
-
-   f32 s0  = sinf(data->t*2.0f),
-       s1  = sinf(data->t*0.43f),
-       s2  = sinf(data->t*1.333f),
-       sm  = vg_clampf( data->speed / 45.0f, 0, 1 ),
-       ft  = (s0*s1*s2)*0.5f+0.5f,
-       f   = vg_lerpf( 200.0f, 1200.0f, sm*0.7f + ft*0.3f ),
-       vol = 0.25f * sm;
-
-   dsp_init_biquad_butterworth_lpf( &data->lpf, f );
-
-   for( u32 i=0; i<count; i ++ ){
-      f32 v = (vg_randf64(&vg_dsp.rand) * 2.0f - 1.0f) * vol;
-      v = dsp_biquad_process( &data->lpf, v );
-
-      buf[i*2+0] = v;
-      buf[i*2+1] = v;
-   }
-
-   data->t += (f32)(count)/44100.0f;
-};
-
-static audio_clip air_synth = {
-   .flags = k_audio_format_gen,
-   .size = 0,
-   .func = audio_air_synth_get_samples,
-   .data = &air_audio_data
-};
-
-void audio_init(void)
-{
-   audio_clip_loadn( audio_board, VG_ARRAY_LEN(audio_board), NULL );
-   audio_clip_loadn( audio_taps, VG_ARRAY_LEN(audio_taps), NULL );
-   audio_clip_loadn( audio_flips, VG_ARRAY_LEN(audio_flips), NULL );
-   audio_clip_loadn( audio_hits, VG_ARRAY_LEN(audio_hits), NULL );
-   audio_clip_loadn( audio_ambience, VG_ARRAY_LEN(audio_ambience), NULL );
-   audio_clip_loadn( &audio_splash, 1, NULL );
-   audio_clip_loadn( &audio_gate_pass, 1, NULL );
-   audio_clip_loadn( &audio_gate_lap, 1, NULL );
-   audio_clip_loadn( &audio_gate_ambient, 1, NULL );
-
-   audio_clip_loadn( audio_jumps, VG_ARRAY_LEN(audio_jumps), NULL );
-   audio_clip_loadn( audio_lands, VG_ARRAY_LEN(audio_lands), NULL );
-   audio_clip_loadn( audio_water, VG_ARRAY_LEN(audio_water), NULL );
-   audio_clip_loadn( audio_grass, VG_ARRAY_LEN(audio_grass), NULL );
-   audio_clip_loadn( audio_footsteps, VG_ARRAY_LEN(audio_footsteps), NULL );
-   audio_clip_loadn( audio_footsteps_grass, 
-                     VG_ARRAY_LEN(audio_footsteps_grass), NULL );
-   audio_clip_loadn( audio_footsteps_wood, 
-                     VG_ARRAY_LEN(audio_footsteps_wood), NULL );
-   audio_clip_loadn( audio_rewind, VG_ARRAY_LEN(audio_rewind), NULL );
-   audio_clip_loadn( audio_ui, VG_ARRAY_LEN(audio_ui), NULL );
-   audio_clip_loadn( audio_challenge, VG_ARRAY_LEN(audio_challenge), NULL );
-
-   audio_lock();
-   audio_set_lfo_wave( 0, k_lfo_polynomial_bipolar, 80.0f );
-   audio_set_lfo_frequency( 0, 20.0f );
-
-   air_audio_data.channel = audio_get_first_idle_channel();
-   if( air_audio_data.channel )
-      audio_channel_init( air_audio_data.channel, &air_synth, 0 );
-
-   audio_unlock();
-}
-
-void audio_ambient_sprite_play( v3f co, audio_clip *clip )
-{
-   audio_lock();
-   u16 group_id = 0xfff0;
-   audio_channel *ch = audio_get_group_idle_channel( group_id, 4 );
-
-   if( ch ){
-      audio_channel_init( ch, clip, AUDIO_FLAG_SPACIAL_3D );
-      audio_channel_group( ch, group_id );
-      audio_channel_set_spacial( ch, co, 80.0f );
-      audio_channel_edit_volume( ch, 1.0f, 1 );
-      ch = audio_relinquish_channel( ch );
-   }
-   audio_unlock();
-}
-
-enum audio_sprite_type world_audio_sample_sprite_random(v3f origin, v3f output);
-void audio_ambient_sprites_update( world_instance *world, v3f co )
-{
-   static float accum = 0.0f;
-   accum += vg.time_delta;
-
-   if( accum > 0.1f )
-      accum -= 0.1f;
-   else return;
-
-   v3f sprite_pos;
-   enum audio_sprite_type sprite_type = 
-      world_audio_sample_sprite_random( co, sprite_pos );
-   
-   if( sprite_type != k_audio_sprite_type_none ){
-      if( sprite_type == k_audio_sprite_type_grass ){
-         audio_ambient_sprite_play( sprite_pos, 
-                                    &audio_grass[vg_randu32(&vg.rand)%4] );
-      }
-      else if( sprite_type == k_audio_sprite_type_water ){
-         if( world->water.enabled ){
-            audio_ambient_sprite_play( sprite_pos, 
-                                       &audio_water[vg_randu32(&vg.rand)%6] );
-         }
-      }
-   }
-}
diff --git a/audio.h b/audio.h
deleted file mode 100644 (file)
index 95894f1..0000000
--- a/audio.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-
-#include "vg/vg_engine.h"
-#include "vg/vg_audio.h"
-#include "vg/vg_audio_dsp.h"
-#include "world.h"
-
-struct air_synth_data {
-   f32 speed;
-
-   /* internal */
-   f32 t;
-   struct dsp_biquad lpf;
-   SDL_SpinLock sl;
-
-   /* not used in locking */
-   audio_channel *channel;
-}
-extern air_audio_data;
-
-void audio_init(void);
-void audio_ambient_sprite_play( v3f co, audio_clip *clip );
-void audio_ambient_sprites_update( world_instance *world, v3f co );
-
-/* TODO(ASSETS): 
- * Have these as asignable ID's and not a bunch of different arrays. 
- */
-extern audio_clip audio_board[];
-extern audio_clip audio_taps[];
-extern audio_clip audio_flips[];
-extern audio_clip audio_hits[];
-extern audio_clip audio_splash;
-extern audio_clip audio_jumps[];
-extern audio_clip audio_footsteps[];
-extern audio_clip audio_footsteps_grass[];
-extern audio_clip audio_footsteps_wood[];
-extern audio_clip audio_lands[];
-extern audio_clip audio_water[];
-extern audio_clip audio_grass[];
-extern audio_clip audio_ambience[];
-extern audio_clip audio_gate_pass;
-extern audio_clip audio_gate_lap;
-extern audio_clip audio_gate_ambient;
-extern audio_clip audio_rewind[];
-extern audio_clip audio_ui[];
-extern audio_clip audio_challenge[];
-
-enum audio_sprite_type 
-{
-   k_audio_sprite_type_none,
-   k_audio_sprite_type_grass,
-   k_audio_sprite_type_water
-};
diff --git a/build.c b/build.c
index f772fd2868483691a77db33321520fd9230f6317..692e94104e3485d5e0ecfd63422b3a71af350a72 100644 (file)
--- a/build.c
+++ b/build.c
@@ -5,11 +5,11 @@
 #include "vg/vg_build.h"
 #include "vg/vg_build_utils_shader.h"
 #include "vg/vg_msg.h"
-#include "addon_types.h"
+#include "src/addon_types.h"
 
 #include "vg/vg_m.h"
-#include "model.h"
-#include "model.c"
+#include "src/model.h"
+#include "src/model.c"
 
 /* 
  * Addon metadata utilities 
@@ -149,7 +149,7 @@ void build_game_content( struct vg_project *proj )
    vg_syscall( "cp bin/skaterift_blender.zip bin/%s/tools/", proj->uid.buffer );
 }
    
-#include "build_control_overlay.c"
+#include "src/build_control_overlay.c"
 
 void build_game_bin( struct vg_project *proj, struct vg_compiler_env *env )
 {
@@ -171,9 +171,6 @@ void build_game_bin( struct vg_project *proj, struct vg_compiler_env *env )
       vg_strcat( &conf.link, "-lws2_32 " );
    }
 
-   //vg_strcat( &conf.defines, "-DSKATERIFT " );
-   vg_strcat( &conf.defines, "-DHGATE " );
-
    vg_make_app( proj, &(struct vg_engine_config ) 
          {
             .fixed_update_hz = 60,
@@ -184,7 +181,7 @@ void build_game_bin( struct vg_project *proj, struct vg_compiler_env *env )
             .custom_game_settings = 0,
             .custom_shaders = 1
          }, 
-         env, &conf, "client.c", "skaterift" );
+         env, &conf, "src/client.c", "skaterift" );
 
    vg_add_controller_database( proj );
 }
@@ -216,9 +213,10 @@ void compile_server( struct vg_project *proj, struct vg_compiler_env *env )
    vg_strcat( &sources, sqlite.path.buffer );
 
    struct vg_compiler_conf conf = {0};
-   vg_strcat( &conf.include, "-I. -I./dep " );
+   vg_strcat( &conf.include, "-Isrc -I./dep " );
    vg_strcat( &conf.library, "-L./vg/dep/steam " );
-   vg_strcat( &conf.link, "-ldl -lpthread -lm -lsdkencryptedappticket -lsteam_api " );
+   vg_strcat( &conf.link, "-ldl -lpthread -lm "
+                          "-lsdkencryptedappticket -lsteam_api " );
 
    vg_add_blob( proj, "vg/dep/steam/libsteam_api.so", "" );
    vg_add_blob( proj, "vg/dep/steam/libsdkencryptedappticket.so", "" );
index f5175ea5c3f07c82faa0ea13608506a7bce68558..4ad323cc9caea13a6106945dcb708258df0f7ef9 100755 (executable)
--- a/build.sh
+++ b/build.sh
@@ -1 +1 @@
-clang -fsanitize=address -O0 -I./vg build.c vg/vg_tool.c -o /tmp/tmpsr && /tmp/tmpsr $@
+clang -fsanitize=address -O0 -I. -I./vg build.c vg/vg_tool.c -o /tmp/tmpsr && /tmp/tmpsr $@
diff --git a/build_control_overlay.c b/build_control_overlay.c
deleted file mode 100644 (file)
index 1b67b37..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Script to load the overlay model and generate an enum referencing each 
- * submesh by its name.
- */
-void build_control_overlay(void)
-{
-   FILE *hdr = fopen( "control_overlay.h.generated", "w" );
-   mdl_context ctx;
-   mdl_open( &ctx, "content_skaterift/models/rs_overlay.mdl", NULL );
-   mdl_load_metadata_block( &ctx, NULL );
-   mdl_close( &ctx );
-
-   for( u32 i=0; i<mdl_arrcount( &ctx.meshs ); i ++ )
-   {
-      mdl_mesh *mesh = mdl_arritm( &ctx.meshs, i );
-      fprintf( hdr, "   %s = %u,\n", 
-                  mdl_pstr( &ctx, mesh->pstr_name ), mesh->submesh_start );
-   }
-
-   fclose( hdr );
-}
diff --git a/client.c b/client.c
deleted file mode 100644 (file)
index e06c253..0000000
--- a/client.c
+++ /dev/null
@@ -1,88 +0,0 @@
-#include "vg/vg_opt.h"
-#include "vg/vg_loader.h"
-#include "vg/vg_io.h"
-#include "vg/vg_audio.h"
-#include "vg/vg_async.h"
-
-#include "client.h"
-#include "render.h"
-#include "network.h"
-#include "player_remote.h"
-#include "menu.h"
-
-const char* __asan_default_options() { return "detect_leaks=0"; }
-
-struct game_client g_client = 
-{
-   .demo_mode = 1
-};
-
-static void async_client_ready( void *payload, u32 size )
-{
-   g_client.loaded = 1;
-
-   if( network_client.auto_connect )
-      network_client.user_intent = k_server_intent_online;
-
-   menu_at_begin();
-}
-
-void vg_load(void)
-{
-   vg_audio.always_keep_compressed = 1;
-   vg_loader_step( render_init, NULL );
-
-   game_load();
-
-   vg_async_call( async_client_ready, NULL, 0 );
-}
-
-void vg_preload(void)
-{
-vg_info(" Copyright  .        . .       -----, ,----- ,---.   .---.  \n" );
-vg_info(" 2021-2024  |\\      /| |           /  |      |    | |    /| \n" );
-vg_info("            | \\    / | +--        /   +----- +---'  |   / | \n" );
-vg_info("            |  \\  /  | |         /    |      |   \\  |  /  | \n" );
-vg_info("            |   \\/   | |        /     |      |    \\ | /   | \n" );
-vg_info("            '        ' '--' [] '----- '----- '     ' '---'  " 
-        "SOFTWARE\n" );
-
-   /* please forgive me! */
-   u32 sz; char *drm;
-   if( (drm = vg_file_read_text( vg_mem.scratch, "DRM", &sz )) )
-      if( !strcmp(drm, "blibby!") )
-         g_client.demo_mode = 0;
-
-   vg_loader_step( remote_players_init, NULL );
-
-   steam_init();
-   vg_loader_step( NULL, steam_end );
-   vg_loader_step( network_init, network_end );
-}
-
-void vg_launch_opt(void)
-{
-   const char *arg;
-
-   if( vg_long_opt( "noauth" ) )
-      network_client.auth_mode = eServerModeNoAuthentication;
-
-   if( (arg = vg_long_opt_arg( "server" )) )
-      network_set_host( arg, NULL );
-
-   if( vg_long_opt( "demo" ) )
-      g_client.demo_mode = 1;
-
-   game_launch_opt();
-}
-
-int main( int argc, char *argv[] )
-{
-   network_set_host( "skaterift.com", NULL );
-   vg_mem.use_libc_malloc = 1;
-   vg_set_mem_quota( 160*1024*1024 );
-   vg_enter( argc, argv, "Voyager Game Engine" ); 
-   return 0;
-}
-
-#include "skaterift.c"
diff --git a/client.h b/client.h
deleted file mode 100644 (file)
index e3435b9..0000000
--- a/client.h
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-
-/*
- *   client     - entry point. window, common things like render init.. etc
- *      vg      - backend code
- *         game - top layer: game content, state
- */
-
-struct game_client
-{
-   bool loaded, demo_mode;
-}
-extern g_client;
-
-/* game defined */
-void game_launch_opt( void );
-void game_load( void );
diff --git a/common.h b/common.h
deleted file mode 100644 (file)
index 6f0c2f9..0000000
--- a/common.h
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#ifndef COMMON_H
-#define COMMON_H
-
-#endif /* COMMON_H */
diff --git a/control_overlay.c b/control_overlay.c
deleted file mode 100644 (file)
index a39d77b..0000000
+++ /dev/null
@@ -1,793 +0,0 @@
-#include "control_overlay.h"
-#include "model.h"
-#include "input.h"
-#include "player.h"
-#include "player_skate.h"
-#include "player_walk.h"
-#include "shaders/model_menu.h"
-#include "vg/vg_engine.h"
-#include "vg/vg_mem.h"
-#include "vg/vg_m.h"
-
-struct control_overlay control_overlay = { .enabled = 1 };
-
-static void render_overlay_mesh( enum control_overlay_mesh index )
-{
-   mdl_draw_submesh( mdl_arritm( &control_overlay.mdl.submeshs, index ) );
-}
-
-void control_overlay_init(void)
-{
-   void *alloc = vg_mem.rtmemory;
-   mdl_context *mdl = &control_overlay.mdl;
-
-   mdl_open( mdl, "models/rs_overlay.mdl", alloc );
-   mdl_load_metadata_block( mdl, alloc );
-   mdl_async_full_load_std( mdl );
-   mdl_close( mdl );
-
-   vg_async_stall();
-
-   if( mdl_arrcount( &mdl->textures ) )
-   {
-      mdl_texture *tex = mdl_arritm( &mdl->textures, 0 );
-      control_overlay.tex = tex->glname;
-   }
-   else
-   {
-      control_overlay.tex = vg.tex_missing;
-      vg_error( "No texture in control overlay\n" );
-   }
-
-   vg_console_reg_var( "control_overlay", &control_overlay.enabled, 
-                       k_var_dtype_i32, VG_VAR_PERSISTENT );
-}
-
-static void draw_key( bool press, bool wide )
-{
-   if( wide ) render_overlay_mesh( press? ov_shift_down: ov_shift );
-   else render_overlay_mesh( press? ov_key_down: ov_key );
-}
-
-static void colorize( bool press, bool condition )
-{
-   v4f cnorm = { 1,1,1,0.76f },
-       cdis  = { 1,1,1,0.35f },
-       chit  = { 1,0.5f,0.2f,0.8f };
-
-   if( condition )
-      if( press )
-         shader_model_menu_uColour( chit );
-      else
-         shader_model_menu_uColour( cnorm );
-   else
-      shader_model_menu_uColour( cdis );
-}
-
-void control_overlay_render(void)
-{
-   if( !control_overlay.enabled ) return;
-   if( skaterift.activity != k_skaterift_default ) return;
-
-   glEnable(GL_BLEND);
-   glDisable(GL_DEPTH_TEST);
-   glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
-   glBlendEquation(GL_FUNC_ADD);
-
-   m4x4f ortho;
-   f32  r = (f32)vg.window_x / (f32)vg.window_y,
-       fl = -r,
-       fr =  r,
-       fb =  1.0f,
-       ft = -1.0f,
-       rl = 1.0f / (fr-fl),
-       tb = 1.0f / (ft-fb);
-
-   m4x4_zero( ortho );
-   ortho[0][0] = 2.0f * rl;
-   ortho[2][1] = 2.0f * tb;
-   ortho[3][0] = (fr + fl) * -rl;
-   ortho[3][1] = (ft + fb) * -tb;
-   ortho[3][3] = 1.0f;
-
-   v4f cnorm = { 1,1,1,0.76f },
-       cdis  = { 1,1,1,0.35f },
-       chit  = { 1,0.5f,0.2f,0.8f };
-
-   shader_model_menu_use();
-   shader_model_menu_uTexMain( 1 );
-   shader_model_menu_uPv( ortho );
-   shader_model_menu_uColour( cnorm );
-
-   mdl_context *mdl = &control_overlay.mdl;
-   mesh_bind( &mdl->mesh );
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, control_overlay.tex );
-
-   enum player_subsystem subsytem = localplayer.subsystem;
-
-   m4x3f mmdl;
-   m4x3_identity( mmdl );
-
-   bool in_air = 0, grinding = 0;
-
-   if( subsytem == k_player_subsystem_walk )
-      in_air = player_walk.state.activity == k_walk_activity_air;
-   else if( subsytem == k_player_subsystem_skate )
-      in_air = player_skate.state.activity < k_skate_activity_ground;
-
-   grinding = (subsytem == k_player_subsystem_skate) &&
-              (player_skate.state.activity >= k_skate_activity_grind_any);
-
-   if( vg_input.display_input_method == k_input_method_controller )
-   {
-      bool press_jump = player_skate.state.jump_charge > 0.2f;
-      u8 lb_down = 0, rb_down = 0;
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, vg_end 
-            }, &rb_down );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, vg_end 
-            }, &lb_down );
-      f32 lt_amt = 0.0f, rt_amt = 0.0f;
-      vg_exec_input_program( k_vg_input_type_axis_f32,
-            (vg_input_op[]){ vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT, vg_end },
-            &lt_amt );
-      vg_exec_input_program( k_vg_input_type_axis_f32,
-            (vg_input_op[]){ vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, vg_end },
-            &rt_amt );
-
-      /* joystick L */
-      v2f steer;
-      joystick_state( k_srjoystick_steer, steer );
-
-      mmdl[3][0] =   -r + 0.375f;
-      mmdl[3][2] = 1.0f - 0.375f;
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( 0, 1 );
-         render_overlay_mesh( ov_ls_circ_skate );
-
-         colorize( steer[1]>=0.5f, press_jump );
-         render_overlay_mesh( ov_ls_circ_backflip );
-         colorize( steer[1]<=-0.5f, press_jump );
-         render_overlay_mesh( ov_ls_circ_frontflip );
-
-         colorize( steer[1] > 0.7f, !press_jump && !in_air );
-         render_overlay_mesh( ov_ls_circ_manual );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( 0, 1 );
-         render_overlay_mesh( ov_ls_circ_walk );
-      }
-
-      mmdl[3][0] += steer[0]*0.125f*0.75f;
-      mmdl[3][2] += steer[1]*0.125f*0.75f;
-
-      colorize( 0, 1 );
-      shader_model_menu_uMdl( mmdl );
-      render_overlay_mesh( ov_ls );
-
-      /* joystick R */
-      mmdl[3][0] =    r - 0.375f;
-      mmdl[3][2] = 1.0f - 0.375f;
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( rt_amt > 0.5f, in_air );
-         render_overlay_mesh( ov_rs_circ_grab );
-         colorize( 0, in_air );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( 0, 1 );
-         render_overlay_mesh( ov_rs_circ_look );
-      }
-
-      v2f jlook;
-      joystick_state( k_srjoystick_look, jlook );
-
-      mmdl[3][0] += jlook[0]*0.125f*0.75f;
-      mmdl[3][2] += jlook[1]*0.125f*0.75f;
-      shader_model_menu_uMdl( mmdl );
-      render_overlay_mesh( ov_rs );
-
-
-
-      /* LEFT UPPERS */
-      mmdl[3][0] =    -r;
-      mmdl[3][2] = -1.0f;
-      shader_model_menu_uMdl( mmdl );
-
-      /* LB -------------------------------------------------------------- */
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( lb_down, !in_air );
-         render_overlay_mesh( ov_carve_l );
-      }
-      else
-         colorize( 0, 0 );
-
-      render_overlay_mesh( lb_down? ov_lb_down: ov_lb );
-
-      /* LT ---------------------------------------------------------------- */
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( 0, 0 );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( lt_amt>0.2f, 1 );
-         render_overlay_mesh( ov_lt_run );
-      }
-
-      render_overlay_mesh( ov_lt );
-
-      mmdl[3][2] += lt_amt*0.125f*0.5f;
-      shader_model_menu_uMdl( mmdl );
-      render_overlay_mesh( ov_lt_act );
-
-      /* RIGHT UPPERS */
-      mmdl[3][0] =     r;
-      mmdl[3][2] = -1.0f;
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( rb_down, !in_air );
-         render_overlay_mesh( ov_carve_r );
-      }
-      else
-         colorize( 0, 0 );
-
-      render_overlay_mesh( rb_down? ov_rb_down: ov_rb );
-
-      /* RT ---------------------------------------------------------------- */
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( rt_amt>0.2f, in_air );
-         render_overlay_mesh( ov_rt_grab );
-         colorize( rt_amt>0.2f, !in_air );
-         render_overlay_mesh( ov_rt_crouch );
-         colorize( rt_amt>0.2f, 1 );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( 0, 0 );
-      }
-
-      render_overlay_mesh( ov_rt );
-
-      mmdl[3][2] += rt_amt*0.125f*0.5f;
-      shader_model_menu_uMdl( mmdl );
-      render_overlay_mesh( ov_rt_act );
-
-      /* RIGHT SIDE BUTTONS */
-      bool press_a = 0, press_b = 0, press_x = 0, press_y = 0,
-           press_dpad_w = 0, press_dpad_e = 0, press_dpad_n = 0, press_dpad_s = 0,
-           press_menu = 0, press_back = 0;
-
-      bool is_ps = 0;
-      if( (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS3) ||
-          (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS4) ||
-          (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS5) )
-      {
-         is_ps = 1;
-      }
-
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end }, &press_a );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_B, vg_end }, &press_b );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end }, &press_x );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_Y, vg_end }, &press_y );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_LEFT, vg_end }, &press_dpad_w );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, vg_end }, &press_dpad_e );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_UP, vg_end }, &press_dpad_n );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_DOWN, vg_end }, &press_dpad_s );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_BACK, vg_end }, &press_back );
-      vg_exec_input_program( k_vg_input_type_button_u8, 
-            (vg_input_op[]){
-               vg_joy_button, SDL_CONTROLLER_BUTTON_START, vg_end }, &press_menu );
-
-      mmdl[3][0] =    r;
-      mmdl[3][2] = 0.0f;
-      shader_model_menu_uMdl( mmdl );
-
-      /* B / KICKFLIP / PUSH */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( press_b, !in_air );
-         render_overlay_mesh( ov_text_b_push );
-         colorize( press_b,  in_air );
-         render_overlay_mesh( ov_text_b_kickflip );
-         colorize( press_b, 1 );
-      }
-      else
-      {
-         colorize( 0, 0 );
-      }
-
-      if( is_ps ) render_overlay_mesh( press_b? ov_b_down_ps: ov_b_ps );
-      else        render_overlay_mesh( press_b? ov_b_down: ov_b );
-
-      /* Y / SKATE / WALK / GLIDE */
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         if( localplayer.have_glider )
-         {
-            colorize( press_y, !in_air );
-            render_overlay_mesh( ov_text_y_walk_lwr );
-            colorize( press_y, in_air );
-            render_overlay_mesh( ov_text_y_glide );
-         }
-         else
-         {
-            colorize( press_y, 1 );
-            render_overlay_mesh( ov_text_y_walk );
-         }
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( press_y, player_walk.state.activity < k_walk_activity_inone );
-         render_overlay_mesh( ov_text_y_skate );
-      }
-      else if( subsytem == k_player_subsystem_glide )
-      {
-         colorize( press_y, 1 );
-         render_overlay_mesh( ov_text_y_skate );
-      }
-      else
-         colorize( 0, 0 );
-
-      if( is_ps ) render_overlay_mesh( press_y? ov_y_down_ps: ov_y_ps );
-      else        render_overlay_mesh( press_y? ov_y_down: ov_y );
-
-      /* X / TREFLIP */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( press_x, in_air );
-         render_overlay_mesh( ov_text_x_treflip );
-      }
-      else
-         colorize( press_x, 0 );
-
-      if( is_ps ) render_overlay_mesh( press_x? ov_x_down_ps: ov_x_ps );
-      else        render_overlay_mesh( press_x? ov_x_down: ov_x );
-
-      /* A / JUMP / SHUVIT */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( press_a, !in_air );
-         render_overlay_mesh( ov_text_a_jump );
-         colorize( press_a,  in_air );
-         render_overlay_mesh( ov_text_a_shuvit );
-         colorize( press_a, 1 );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( press_a, !in_air );
-         render_overlay_mesh( ov_text_a_jump_mid );
-      }
-
-      if( is_ps ) render_overlay_mesh( press_a? ov_a_down_ps: ov_a_ps );
-      else        render_overlay_mesh( press_a? ov_a_down: ov_a );
-
-      /* JUMP CHARGE */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         if( player_skate.state.jump_charge > 0.01f )
-         {
-            mmdl[0][0] = player_skate.state.jump_charge * 0.465193f;
-            mmdl[3][0] += -0.4375f;
-            mmdl[3][2] +=  0.09375f;
-            shader_model_menu_uMdl( mmdl );
-            render_overlay_mesh( ov_jump_ind );
-            mmdl[0][0] = 1.0f;
-         }
-      }
-
-
-      /* DPAD --------------------------------------------------- */
-      
-      mmdl[3][0] =   -r;
-      mmdl[3][2] = 0.0f;
-      shader_model_menu_uMdl( mmdl );
-      colorize( 0, 1 );
-      render_overlay_mesh( ov_dpad );
-      
-      colorize( press_dpad_e, 1 );
-      render_overlay_mesh( ov_text_de_camera );
-      if( press_dpad_e )
-         render_overlay_mesh( ov_dpad_e );
-
-      colorize( press_dpad_w, 1 );
-      render_overlay_mesh( ov_text_dw_rewind );
-      if( press_dpad_w )
-         render_overlay_mesh( ov_dpad_w );
-
-      if( subsytem == k_player_subsystem_dead )
-      {
-         colorize( press_dpad_n, 1 );
-         render_overlay_mesh( ov_text_dn_respawn );
-      }
-      else colorize( press_dpad_n, 0 );
-      if( press_dpad_n )
-         render_overlay_mesh( ov_dpad_n );
-
-      colorize( press_dpad_s, 0 );
-      if( press_dpad_s )
-         render_overlay_mesh( ov_dpad_s );
-
-      
-      /* WEIGHT */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         /* stored indicator text */
-         mmdl[3][0] = r -0.842671f;
-         mmdl[3][2] = -1.0f + 0.435484f;
-         colorize( 0, !in_air );
-         shader_model_menu_uMdl( mmdl );
-         render_overlay_mesh( ov_text_stored );
-      
-         mmdl[0][0] = v3_length( player_skate.state.throw_v ) / k_mmthrow_scale;
-         shader_model_menu_uMdl( mmdl );
-         colorize( 0, !in_air );
-         render_overlay_mesh( ov_stored_ind );
-
-         static f32 collect = 0.0f;
-         collect = vg_lerpf( collect, player_skate.collect_feedback, 
-                             vg.time_frame_delta * 15.0f );
-         collect = vg_clampf( collect, 0.0f, 1.0f );
-
-         mmdl[0][0] = collect;
-         mmdl[3][2] += 0.015625f;
-         shader_model_menu_uMdl( mmdl );
-         render_overlay_mesh( ov_stored_ind );
-      }
-
-      mmdl[0][0] =  1.0f;
-      mmdl[3][0] =  0.0f;
-      mmdl[3][2] = -1.0f;
-      shader_model_menu_uMdl( mmdl );
-      colorize( press_menu, 1 );
-      render_overlay_mesh( press_menu? ov_met_r_down: ov_met_r );
-      render_overlay_mesh( ov_text_met_menu );
-
-      colorize( press_back, 0 );
-      render_overlay_mesh( press_back? ov_met_l_down: ov_met_l );
-
-      colorize( 0, 0 );
-      render_overlay_mesh( ov_met );
-   }
-   else 
-   {
-      static v2f gd;
-      v2_lerp( gd, player_skate.state.grab_mouse_delta, vg.time_frame_delta*20.0f, gd );
-
-      /* CTRL  ||  CARVE */
-      if( subsytem == k_player_subsystem_skate )
-      {
-         bool press_ctrl = vg_getkey(SDLK_LCTRL);
-         mmdl[3][0] = -r + 0.25f;
-         mmdl[3][2] = 1.0f - 0.125f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_ctrl, !in_air && !grinding );
-         draw_key( press_ctrl, 1 );
-         render_overlay_mesh( ov_text_carve );
-      }
-
-      /* SHIFT  ||  CROUCH / GRAB / RUN */
-      bool press_shift = vg_getkey( SDLK_LSHIFT );
-      if( subsytem == k_player_subsystem_skate ||
-          subsytem == k_player_subsystem_walk )
-      {
-         mmdl[3][0] = -r + 0.25f;
-         mmdl[3][2] = 1.0f - 0.125f - 0.25f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_shift, !grinding );
-         draw_key( press_shift, 1 );
-         render_overlay_mesh( ov_text_shift );
-
-         if( subsytem == k_player_subsystem_skate )
-         {
-            colorize( press_shift, !in_air && !grinding );
-            render_overlay_mesh( ov_text_crouch );
-            colorize( press_shift,  in_air && !grinding );
-            render_overlay_mesh( ov_text_grab );
-         }
-         else if( subsytem == k_player_subsystem_walk )
-         {
-            render_overlay_mesh( ov_text_run );
-         }
-      }
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         /* stored indicator text */
-         mmdl[3][0] = -r + 0.25f + 0.203125f + 0.007812f;
-         colorize( 0, !in_air );
-         shader_model_menu_uMdl( mmdl );
-         render_overlay_mesh( ov_text_stored );
-      
-         mmdl[0][0] = v3_length( player_skate.state.throw_v ) / k_mmthrow_scale;
-         shader_model_menu_uMdl( mmdl );
-         colorize( 0, !in_air );
-         render_overlay_mesh( ov_stored_ind );
-
-         static f32 collect = 0.0f;
-         collect = vg_lerpf( collect, player_skate.collect_feedback, 
-                             vg.time_frame_delta * 15.0f );
-         collect = vg_clampf( collect, 0.0f, 1.0f );
-
-         mmdl[0][0] = collect;
-         mmdl[3][2] += 0.015625f;
-         shader_model_menu_uMdl( mmdl );
-         render_overlay_mesh( ov_stored_ind );
-      }
-
-      /* -1 */
-      if( subsytem != k_player_subsystem_dead )
-      {
-         bool press_c =  vg_getkey(SDLK_c);
-         mmdl[0][0] = 1.0f;
-         mmdl[3][0] = -r + 0.125f + 1.0f;
-         mmdl[3][2] = 1.0f - 0.125f - 0.25f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_c, 1 );
-         draw_key( press_c, 0 );
-         render_overlay_mesh( ov_text_camera );
-      }
-
-      /* +0 */
-      mmdl[0][0] = 1.0f;
-      mmdl[3][2] = 1.0f - 0.125f - 0.25f - 0.25f;
-
-      /* A  ||  LEFT */
-      if( subsytem != k_player_subsystem_dead )
-      {
-         bool press_a =  vg_getkey(SDLK_a);
-         mmdl[3][0] = -r + 0.125f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_a, 1 );
-         draw_key( press_a, 0 );
-         render_overlay_mesh( ov_text_left );
-      }
-
-      bool press_jump = player_skate.state.jump_charge < 0.2f;
-
-      /* S  ||  MANUAL / BACKFLIP */
-      bool press_s =  vg_getkey(SDLK_s);
-      mmdl[3][0] = -r + 0.125f + 0.25f;
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( press_s, !in_air );
-         draw_key( press_s, 0 );
-         render_overlay_mesh( ov_text_s );
-         /* backflip/manual */
-         colorize( press_s, !in_air && !press_jump );
-         render_overlay_mesh( ov_text_back_flip );
-         colorize( press_s, !in_air &&  press_jump );
-         render_overlay_mesh( ov_text_manual );
-      }
-      else if( subsytem != k_player_subsystem_dead )
-      {
-         colorize( press_s, 1 );
-         draw_key( press_s, 0 );
-         render_overlay_mesh( ov_text_s );
-         render_overlay_mesh( ov_text_back );
-      }
-
-      /* D  ||  RIGHT */
-      if( subsytem != k_player_subsystem_dead )
-      {
-         bool press_d = vg_getkey(SDLK_d);
-         mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_d, 1 );
-         draw_key( press_d, 0 );
-         render_overlay_mesh( ov_text_right );
-      }
-
-      /* +1 */
-      mmdl[3][2] = 1.0f - 0.125f - 0.25f - 0.25f - 0.25f;
-
-      /* Q */
-      if( subsytem == k_player_subsystem_dead )
-      {
-         bool press_q = vg_getkey(SDLK_q);
-         mmdl[3][0] = -r + 0.125f;
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_q, 1 );
-         draw_key( press_q, 0 );
-         render_overlay_mesh( ov_text_respawn );
-      }
-
-      /* W  ||  PUSH / FRONTFLIP */
-      bool press_w = vg_getkey(SDLK_w);
-      mmdl[3][0] = -r + 0.125f + 0.25f;
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( press_w, !in_air );
-         draw_key( press_w, 0 );
-         render_overlay_mesh( ov_text_w );
-         /* frontflip/push */
-         colorize( press_w, !in_air && !press_jump );
-         render_overlay_mesh( ov_text_front_flip );
-         colorize( press_w, !in_air &&  press_jump );
-         render_overlay_mesh( ov_text_push );
-      }
-      else if( subsytem != k_player_subsystem_dead )
-      {
-         colorize( press_w, 1 );
-         draw_key( press_w, 0 );
-         render_overlay_mesh( ov_text_w );
-         render_overlay_mesh( ov_text_forward );
-      }
-
-      /* E */
-      bool press_e = vg_getkey(SDLK_e);
-      mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f;
-
-      shader_model_menu_uMdl( mmdl );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         if( localplayer.have_glider )
-         {
-            colorize( press_e, !in_air );
-            render_overlay_mesh( ov_text_walk_lwr );
-            colorize( press_e, in_air );
-            render_overlay_mesh( ov_text_glide );
-         }
-         else
-         {
-            colorize( press_e, 1 );
-            render_overlay_mesh( ov_text_walk );
-         }
-      }
-      else if( subsytem == k_player_subsystem_glide )
-      {
-         colorize( press_e, 1 );
-         render_overlay_mesh( ov_text_skate );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( press_e, player_walk.state.activity < k_walk_activity_inone );
-         render_overlay_mesh( ov_text_skate );
-      }
-
-      if( subsytem != k_player_subsystem_dead )
-      {
-         draw_key( press_e, 0 );
-         render_overlay_mesh( ov_text_e );
-      }
-
-      /* R */
-      bool press_r = vg_getkey(SDLK_r);
-      mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f + 0.25f;
-      shader_model_menu_uColour( cnorm );
-      shader_model_menu_uMdl( mmdl );
-
-      colorize( press_r, 1 );
-      draw_key( press_r, 0 );
-      render_overlay_mesh( ov_text_rewind );
-
-      /* space */
-      bool press_space = vg_getkey(SDLK_SPACE);
-      mmdl[3][0] = 0.0f;
-      mmdl[3][2] = 1.0f - 0.125f;
-
-
-      if( subsytem == k_player_subsystem_skate ||
-          subsytem == k_player_subsystem_walk )
-      {
-         shader_model_menu_uMdl( mmdl );
-         colorize( press_space, !in_air );
-
-         render_overlay_mesh( press_space?
-               ov_space_down: ov_space );
-         render_overlay_mesh( ov_text_jump );
-      }
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         if( player_skate.state.jump_charge > 0.01f )
-         {
-            mmdl[0][0] = player_skate.state.jump_charge;
-            mmdl[3][0] = -0.4375f;
-            shader_model_menu_uMdl( mmdl );
-            render_overlay_mesh( ov_jump_ind );
-         }
-      }
-
-      bool press_esc = vg_getkey(SDLK_ESCAPE);
-      mmdl[0][0] = 1.0f;
-      mmdl[3][0] = -r + 0.125f;;
-      mmdl[3][2] = -1.0f + 0.125f;
-      shader_model_menu_uMdl( mmdl );
-      colorize( press_esc, 1 );
-      render_overlay_mesh( ov_text_menu );
-      render_overlay_mesh( press_esc? ov_key_menu_down: ov_key_menu );
-      mmdl[3][0] = r - 0.38f;
-      mmdl[3][2] = 0.0f;
-      shader_model_menu_uMdl( mmdl );
-      colorize( press_shift, in_air );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         render_overlay_mesh( ov_mouse_grabs );
-
-         if( in_air && press_shift )
-         {
-            mmdl[3][0] += gd[0]*0.125f;
-            mmdl[3][2] += gd[1]*0.125f;
-         }
-      }
-
-      shader_model_menu_uMdl( mmdl );
-
-      bool lmb = button_press( k_srbind_trick0 ),
-           rmb = button_press( k_srbind_trick1 );
-
-      if( subsytem == k_player_subsystem_skate )
-      {
-         colorize( 0, press_space || in_air );
-         render_overlay_mesh( ov_mouse );
-
-         colorize( lmb&&!rmb, press_space || in_air );
-         render_overlay_mesh( ov_text_shuvit );
-
-         colorize( lmb, press_space || in_air );
-         render_overlay_mesh( lmb? ov_lmb_down: ov_lmb );
-
-         colorize( rmb&&!lmb, press_space || in_air );
-         render_overlay_mesh( ov_text_kickflip );
-
-         colorize( rmb, press_space || in_air );
-         render_overlay_mesh( rmb? ov_rmb_down: ov_rmb );
-
-         colorize( rmb&&lmb, press_space || in_air );
-         render_overlay_mesh( ov_text_treflip );
-      }
-      else if( subsytem == k_player_subsystem_walk )
-      {
-         colorize( 0, 1 );
-         render_overlay_mesh( ov_mouse );
-         render_overlay_mesh( ov_text_look );
-
-         render_overlay_mesh( lmb? ov_lmb_down: ov_lmb );
-         render_overlay_mesh( rmb? ov_rmb_down: ov_rmb );
-      }
-   }
-}
diff --git a/control_overlay.h b/control_overlay.h
deleted file mode 100644 (file)
index 089136d..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-enum control_overlay_mesh
-{
-   #include "control_overlay.h.generated"
-};
-
-struct control_overlay
-{
-   mdl_context mdl;
-   GLuint tex;
-   i32 enabled;
-}
-extern control_overlay;
-
-void control_overlay_render(void);
-void control_overlay_init(void);
diff --git a/depth_compare.h b/depth_compare.h
deleted file mode 100644 (file)
index 1db5665..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-#include "vg/vg_m.h"
-#include "vg/vg_framebuffer.h"
-#include "skaterift.h"
-#include "render.h"
-
-static inline void depth_compare_bind( 
-      void (*uTexSceneDepth)(int),
-      void (*uInverseRatioDepth)(v3f),
-      void (*uInverseRatioMain)(v3f),
-      vg_camera *cam )
-{
-   uTexSceneDepth( 5 );
-   vg_framebuffer_bind_texture( g_render.fb_main, 2, 5 );
-   v3f inverse;
-   vg_framebuffer_inverse_ratio( g_render.fb_main, inverse );
-   inverse[2] = g_render.cam.farz-g_render.cam.nearz;
-
-   uInverseRatioDepth( inverse );
-   vg_framebuffer_inverse_ratio( NULL, inverse );
-   inverse[2] = cam->farz-cam->nearz;
-   uInverseRatioMain( inverse );
-}
diff --git a/ent_camera.c b/ent_camera.c
deleted file mode 100644 (file)
index d23d8d8..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-#include "entity.h"
-
-void ent_camera_unpack( ent_camera *ent, vg_camera *cam )
-{
-   v3f dir = {0.0f,-1.0f,0.0f};
-   mdl_transform_vector( &ent->transform, dir, dir );
-   v3_angles( dir, cam->angles );
-   v3_copy( ent->transform.co, cam->pos );
-   cam->fov = ent->fov;
-}
diff --git a/ent_camera.h b/ent_camera.h
deleted file mode 100644 (file)
index bf4ce14..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-#include "entity.h"
-
-void ent_camera_unpack( ent_camera *ent, vg_camera *cam );
diff --git a/ent_challenge.c b/ent_challenge.c
deleted file mode 100644 (file)
index d30a5dc..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-#include "vg/vg_engine.h"
-#include "entity.h"
-#include "input.h"
-#include "gui.h"
-#include "audio.h"
-
-entity_call_result ent_challenge_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-
-   if( call->function == 0 ) /* unlock() */
-   {
-      if( !challenge->status )
-      {
-         vg_info( "challenge( '%s' )\n", 
-                  mdl_pstr( &world->meta, challenge->pstr_alias) );
-         ent_call call;
-         call.data = NULL;
-         call.function = challenge->target_event;
-         call.id = challenge->target;
-         entity_call( world, &call );
-      }
-      challenge->status = 1;
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 1 ) /* view() */
-   {
-      if( (localplayer.subsystem == k_player_subsystem_walk) &&
-          (world_static.challenge_target == NULL) )
-      {
-         world_static.challenge_target = NULL;
-         world_entity_set_focus( call->id );
-         world_entity_focus_modal();
-
-         gui_helper_clear();
-         vg_str text;
-         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
-            vg_strcat( &text, "Start" );
-         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
-            vg_strcat( &text, "Exit" );
-      }
-      return k_entity_call_result_OK;
-   }
-   else 
-      return k_entity_call_result_unhandled;
-}
-
-void ent_challenge_preupdate( ent_focus_context *ctx )
-{
-   world_instance *world = ctx->world;
-   ent_challenge *challenge = mdl_arritm( &world->ent_challenge, ctx->index );
-
-   /* maximum distance from active challenge */
-   if( !ctx->active )
-   {
-      f32 min_dist2 = 999999.9f;
-
-      if( mdl_entity_id_type( challenge->first ) == k_ent_objective )
-      {
-         u32 next = challenge->first;
-         while( mdl_entity_id_type(next) == k_ent_objective ){
-            u32 index = mdl_entity_id_id( next );
-            ent_objective *objective = mdl_arritm(&world->ent_objective,index);
-            next = objective->id_next;
-
-            f32 d2 = v3_dist2( localplayer.rb.co, objective->transform.co );
-            if( d2 < min_dist2 ) 
-               min_dist2 = d2;
-         }
-      }
-
-      f32 max_dist = 100.0f;
-      if( min_dist2 > max_dist*max_dist ){
-         world_static.challenge_target = NULL;
-         world_static.challenge_timer = 0.0f;
-         world_entity_clear_focus();
-         audio_lock();
-         audio_oneshot_3d( &audio_challenge[6], localplayer.rb.co,
-                           30.0f, 1.0f );
-         audio_unlock();
-      }
-      return;
-   }
-
-   world_entity_focus_camera( world, challenge->camera );
-
-   if( mdl_entity_id_type( challenge->first ) == k_ent_objective ){
-      if( button_down( k_srbind_maccept ) ){
-         u32 index = mdl_entity_id_id( challenge->first );
-         world_static.challenge_target = mdl_arritm( &world->ent_objective, 
-                                                     index );
-         world_static.challenge_timer = 0.0f;
-         world_entity_exit_modal();
-         gui_helper_clear();
-
-         u32 next = challenge->first;
-         while( mdl_entity_id_type(next) == k_ent_objective ){
-            u32 index = mdl_entity_id_id( next );
-            ent_objective *objective = mdl_arritm(&world->ent_objective,index);
-            objective->flags &= ~k_ent_objective_passed;
-            next = objective->id_next;
-            v3_fill( objective->transform.s, 1.0f );
-         }
-         audio_lock();
-         audio_oneshot( &audio_challenge[5], 1.0f, 0.0f );
-         audio_unlock();
-         return;
-      }
-   }
-
-   if( button_down( k_srbind_mback ) )
-   {
-      world_static.challenge_target = NULL;
-      world_entity_exit_modal();
-      world_entity_clear_focus();
-      gui_helper_clear();
-      audio_lock();
-      audio_oneshot( &audio_challenge[4], 1.0f, 0.0f );
-      audio_unlock();
-      return;
-   }
-}
-
-static void ent_challenge_render( ent_challenge *challenge ){
-   
-}
diff --git a/ent_challenge.h b/ent_challenge.h
deleted file mode 100644 (file)
index f53c956..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#pragma once
-#include "entity.h"
-
-void ent_challenge_preupdate( ent_focus_context *ctx );
-entity_call_result ent_challenge_call( world_instance *world, ent_call *call );
diff --git a/ent_glider.c b/ent_glider.c
deleted file mode 100644 (file)
index 12b0575..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-#include "entity.h"
-#include "player_glide.h"
-
-entity_call_result ent_glider_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_glider *glider = mdl_arritm( &world->ent_glider, index );
-
-   if( call->function == 0 )
-   {
-      glider->flags |= 0x1;
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 1 )
-   {
-      if( glider->flags & 0x1 )
-      {
-         player_glide_equip_glider();
-      }
-      return k_entity_call_result_OK;
-   }
-   else 
-      return k_entity_call_result_unhandled;
-}
diff --git a/ent_glider.h b/ent_glider.h
deleted file mode 100644 (file)
index 5815dda..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-#pragma once
-#include "entity.h"
-
-entity_call_result ent_glider_call( world_instance *world, ent_call *call );
diff --git a/ent_miniworld.c b/ent_miniworld.c
deleted file mode 100644 (file)
index 19c88d0..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-#include "entity.h"
-#include "ent_miniworld.h"
-#include "world_render.h"
-#include "world_load.h"
-#include "input.h"
-#include "gui.h"
-#include "menu.h"
-#include "audio.h"
-
-struct global_miniworld global_miniworld;
-
-entity_call_result ent_miniworld_call( world_instance *world, ent_call *call )
-{
-   ent_miniworld *miniworld = mdl_arritm( &world->ent_miniworld, 
-                                          mdl_entity_id_id(call->id) );
-
-   int world_id = world - world_static.instances;
-
-   if( call->function == 0 ) /* zone() */
-   {
-      const char *uid = mdl_pstr( &world->meta, miniworld->pstr_world );
-      skaterift_load_world_command( 1, (const char *[]){ uid } );
-
-      mdl_transform_m4x3( &miniworld->transform, global_miniworld.mmdl );
-      global_miniworld.active = miniworld;
-
-      gui_helper_clear();
-      vg_str text;
-
-      if( gui_new_helper( input_button_list[k_srbind_miniworld_resume], &text ))
-         vg_strcat( &text, "Enter World" );
-
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 1 )
-   {
-      global_miniworld.active = NULL;
-      gui_helper_clear();
-
-      if( miniworld->proxy )
-      {
-         ent_prop *prop = mdl_arritm( &world->ent_prop, 
-                                      mdl_entity_id_id(miniworld->proxy) );
-         prop->flags &= ~0x1;
-      }
-
-      return k_entity_call_result_OK;
-   }
-   else
-      return k_entity_call_result_unhandled;
-}
-
-static void miniworld_icon( vg_camera *cam, enum gui_icon icon, 
-                            v3f pos, f32 size)
-{
-   m4x3f mmdl;
-   v3_copy( cam->transform[2], mmdl[2] );
-   mmdl[2][1] = 0.0f;
-   v3_normalize( mmdl[2] );
-   v3_copy( (v3f){0,1,0}, mmdl[1] );
-   v3_cross( mmdl[1], mmdl[2], mmdl[0] );
-   m4x3_mulv( global_miniworld.mmdl, pos, mmdl[3] );
-
-   shader_model_font_uMdl( mmdl );
-   shader_model_font_uOffset( (v4f){0,0,0,20.0f*size} );
-
-   m4x4f m4mdl;
-   m4x3_expand( mmdl, m4mdl );
-   m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
-   shader_model_font_uPvmPrev( m4mdl );
-
-   mdl_submesh *sm = gui.icons[ icon ];
-   if( sm )
-      mdl_draw_submesh( sm );
-}
-
-void ent_miniworld_render( world_instance *host_world, vg_camera *cam )
-{
-   if( host_world != &world_static.instances[k_world_purpose_hub] )
-      return;
-
-   ent_miniworld *miniworld = global_miniworld.active;
-
-   if( !miniworld )
-      return;
-
-   world_instance *dest_world = &world_static.instances[k_world_purpose_client];
-
-   int rendering = 1;
-   if( dest_world->status != k_world_status_loaded )
-      rendering = 0;
-
-   if( miniworld->proxy ){
-      ent_prop *prop = mdl_arritm( &host_world->ent_prop, 
-                                   mdl_entity_id_id(miniworld->proxy) );
-      if( !rendering )
-         prop->flags &= ~0x1;
-      else
-         prop->flags |= 0x1;
-   }
-
-
-   if( !rendering )
-      return;
-
-   render_world_override( dest_world, host_world, global_miniworld.mmdl, cam,
-                          NULL, (v4f){dest_world->tar_min,10000.0f,0.0f,0.0f} );
-   render_world_routes( dest_world, host_world, 
-                        global_miniworld.mmdl, cam, 0, 1 );
-
-   /* icons
-    * ---------------------*/
-   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, cam );
-   mesh_bind( &gui.icons_mesh );
-
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, gui.icons_texture );
-   shader_model_font_uTexMain( 0 );
-   shader_model_font_uColour( (v4f){1,1,1,1} );
-
-   miniworld_icon( cam, k_gui_icon_player, dest_world->player_co, 
-                  1.0f + sinf(vg.time)*0.2f );
-
-   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_challenge); i++ ){
-      ent_challenge *challenge = mdl_arritm( &dest_world->ent_challenge, i );
-
-      enum gui_icon icon = k_gui_icon_exclaim;
-      if( challenge->status )
-         icon = k_gui_icon_tick;
-
-      miniworld_icon( cam, icon, challenge->transform.co, 1.0f );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &dest_world->ent_route, i );
-
-      if( route->flags & k_ent_route_flag_achieve_gold ){
-         miniworld_icon( cam, k_gui_icon_rift_run_gold, 
-                         route->board_transform[3],1.0f);
-      }
-      else if( route->flags & k_ent_route_flag_achieve_silver ){
-         miniworld_icon( cam, k_gui_icon_rift_run_silver, 
-                         route->board_transform[3],1.0f);
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &dest_world->ent_route, i );
-
-      v4f colour;
-      v4_copy( route->colour, colour );
-      v3_muls( colour, 1.6666f, colour );
-      shader_model_font_uColour( colour );
-      miniworld_icon( cam, k_gui_icon_rift_run, route->board_transform[3],1.0f);
-   }
-}
-
-void ent_miniworld_preupdate(void)
-{
-   world_instance *hub = world_current_instance(),
-                  *dest = &world_static.instances[k_world_purpose_client];
-
-   ent_miniworld *miniworld = global_miniworld.active;
-
-   if( (localplayer.subsystem != k_player_subsystem_walk) ||
-       (global_miniworld.transition) ||
-       (world_static.active_instance != k_world_purpose_hub) ||
-       (!miniworld) ||
-       (dest->status != k_world_status_loaded) ||
-       (skaterift.activity != k_skaterift_default)) {
-      return;
-   }
-
-   if( button_down( k_srbind_miniworld_resume ) )
-   {
-      if( skaterift.demo_mode )
-      {
-         if( world_static.instance_addons[1]->flags & ADDON_REG_PREMIUM )
-         {
-            menu_open( k_menu_page_premium );
-            return;
-         }
-      }
-
-      global_miniworld.transition = 1;
-      global_miniworld.t = 0.0f;
-      global_miniworld.cam = g_render.cam;
-
-      world_switch_instance(1);
-      srinput.state = k_input_state_resume;
-      menu.disable_open = 0;
-      gui_helper_clear();
-      audio_lock();
-      audio_oneshot( &audio_ui[2], 1.0f, 0.0f );
-      audio_unlock();
-   }
-}
-
-void ent_miniworld_goback(void)
-{
-   audio_lock();
-   audio_oneshot( &audio_ui[2], 1.0f, 0.0f );
-   audio_unlock();
-
-   global_miniworld.transition = -1;
-   global_miniworld.t = 1.0f;
-
-   global_miniworld.cam = g_render.cam;
-   vg_m4x3_transform_camera( global_miniworld.mmdl, &global_miniworld.cam );
-   world_switch_instance(0);
-}
diff --git a/ent_miniworld.h b/ent_miniworld.h
deleted file mode 100644 (file)
index 278cf75..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-#include "entity.h"
-
-struct global_miniworld
-{
-   ent_miniworld *active;
-   int transition;
-   f32 t;
-
-   m4x3f mmdl;
-   vg_camera cam;
-}
-extern global_miniworld;
-
-entity_call_result ent_miniworld_call( world_instance *world, ent_call *call );
-void ent_miniworld_render( world_instance *host_world, vg_camera *cam );
-void ent_miniworld_goback(void);
-void ent_miniworld_preupdate(void);
diff --git a/ent_npc.c b/ent_npc.c
deleted file mode 100644 (file)
index 7a4b484..0000000
--- a/ent_npc.c
+++ /dev/null
@@ -1,304 +0,0 @@
-#include "vg/vg_mem.h"
-#include "ent_npc.h"
-#include "shaders/model_character_view.h"
-#include "input.h"
-#include "player.h"
-#include "gui.h"
-
-struct npc npc_gumpa, npc_slowmo, npc_volc_flight;
-static struct skeleton_anim *gumpa_idle;
-static struct skeleton_anim *slowmo_momentum, *slowmo_slide, *slowmo_rewind,
-                            *anim_tutorial_cam;
-static float slowmo_opacity = 0.0f;
-static f64 volc_start_preview = 0.0;
-
-void npc_load_model( struct npc *npc, const char *path )
-{
-   vg_linear_clear( vg_mem.scratch );
-   
-   mdl_context *meta = &npc->meta;
-   mdl_open( meta, path, vg_mem.rtmemory );
-   mdl_load_metadata_block( meta, vg_mem.rtmemory );
-   mdl_load_animation_block( meta, vg_mem.rtmemory );
-
-   struct skeleton *sk = &npc->skeleton;
-   skeleton_setup( sk, vg_mem.rtmemory, meta );
-
-   u32 mtx_size = sizeof(m4x3f)*sk->bone_count;
-   npc->final_mtx = vg_linear_alloc( vg_mem.rtmemory, mtx_size );
-
-   if( mdl_arrcount( &meta->textures ) )
-   {
-      mdl_texture *tex0 = mdl_arritm( &meta->textures, 0 );
-      void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
-      mdl_fread_pack_file( meta, &tex0->file, data );
-
-      vg_tex2d_load_qoi_async( data, tex0->file.pack_size,
-                               VG_TEX2D_NEAREST|VG_TEX2D_CLAMP,
-                               &npc->texture );
-   }
-   else
-   {
-      npc->texture = vg.tex_missing;
-   }
-
-   mdl_async_load_glmesh( meta, &npc->mesh, NULL );
-   mdl_close( meta );
-}
-
-void npc_init(void)
-{
-   npc_load_model( &npc_gumpa, "models/gumpa.mdl" );
-   gumpa_idle = skeleton_get_anim( &npc_gumpa.skeleton, "gumpa_idle" );
-
-   npc_load_model( &npc_slowmo, "models/slowmos.mdl" );
-   slowmo_momentum = 
-      skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_momentum" );
-   slowmo_slide = skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_slide" );
-   slowmo_rewind = skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_rewind" );
-
-   npc_load_model( &npc_volc_flight, "models/volc_flight.mdl" );
-   anim_tutorial_cam = 
-      skeleton_get_anim( &npc_volc_flight.skeleton, "tutorial" );
-}
-
-static struct npc *npc_resolve( u32 id )
-{
-   if( id == 1 ) return &npc_gumpa;
-   else if( id == 2 ) return &npc_slowmo;
-   else if( id == 3 ) return &npc_volc_flight;
-   else return NULL;
-}
-
-static entity_call_result npc_slowmo_call( ent_npc *npc, ent_call *call )
-{
-   if( call->function == 0 )
-   {
-      gui_helper_clear();
-      vg_str text;
-
-      if( npc->context == 2 )
-      {
-         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ))
-            vg_strcat( &text, "Crouch (store energy)" );
-         if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ))
-            vg_strcat( &text, "Slide" );
-      }
-      else if( npc->context == 1 )
-      {
-         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ))
-            vg_strcat( &text, "Crouch (store energy)" );
-      }
-      else if( npc->context == 3 )
-      {
-         if( gui_new_helper( input_button_list[k_srbind_reset], &text ))
-            vg_strcat( &text, "Rewind time" );
-         if( gui_new_helper( input_button_list[k_srbind_replay_resume], &text ))
-            vg_strcat( &text, "Resume" );
-      }
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == -1 )
-   {
-      world_entity_clear_focus();
-      gui_helper_clear();
-      return k_entity_call_result_OK;
-   }
-   else
-      return k_entity_call_result_unhandled;
-}
-
-entity_call_result ent_npc_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_npc *npc = mdl_arritm( &world->ent_npc, index );
-
-   if( npc->id == 2 )
-   {
-      return npc_slowmo_call( npc, call );
-   }
-   else if( npc->id == 3 )
-   {
-      if( call->function == 0 )
-      {
-         world_entity_set_focus( call->id );
-         gui_helper_clear();
-         vg_str text;
-         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
-            vg_strcat( &text, "Preview course" );
-         return k_entity_call_result_OK;
-      }
-      else if( call->function == -1 )
-      {
-         world_entity_clear_focus();
-         gui_helper_clear();
-         return k_entity_call_result_OK;
-      }
-      else
-         return k_entity_call_result_unhandled;
-   }
-   else if( npc->id == 4 )
-   {
-      if( call->function == 0 )
-      {
-         gui_helper_clear();
-         vg_str text;
-         if( gui_new_helper( input_button_list[k_srbind_camera], &text ))
-            vg_strcat( &text, "First/Thirdperson" );
-         return k_entity_call_result_OK;
-      }
-      else if( call->function == -1 )
-      {
-         gui_helper_clear();
-         return k_entity_call_result_OK;
-      }
-      else
-         return k_entity_call_result_unhandled;
-   }
-   else
-   {
-      if( call->function == 0 )
-      {
-         world_entity_set_focus( call->id );
-         gui_helper_clear();
-         vg_str text;
-         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
-            vg_strcat( &text, "Talk to ???" );
-         return k_entity_call_result_OK;
-      }
-      else if( call->function == -1 )
-      {
-         world_entity_clear_focus();
-         gui_helper_clear();
-         return k_entity_call_result_OK;
-      }
-      else 
-         return k_entity_call_result_unhandled;
-   }
-}
-
-void ent_npc_preupdate( ent_focus_context *ctx )
-{
-   world_instance *world = ctx->world;
-   ent_npc *ent = mdl_arritm( &world->ent_npc, ctx->index );
-
-   if( !ctx->active )
-   {
-      if( button_down(k_srbind_maccept) )
-      {
-         world_entity_focus_modal();
-         gui_helper_clear();
-         vg_str text;
-         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
-            vg_strcat( &text, "leave" );
-
-         volc_start_preview = vg.time;
-      }
-
-      return;
-   }
-
-   if( ent->id == 3 )
-   {
-      player_pose pose;
-      struct skeleton *sk = &npc_volc_flight.skeleton;
-
-      f64 t = (vg.time - volc_start_preview) * 0.5;
-      skeleton_sample_anim_clamped( sk, anim_tutorial_cam, t, pose.keyframes );
-      
-      ent_camera *cam = mdl_arritm( &world->ent_camera, 
-                                     mdl_entity_id_id(ent->camera) );
-      v3_copy( pose.keyframes[0].co, cam->transform.co );
-
-      v4f qp;
-      q_axis_angle( qp, (v3f){1,0,0}, VG_TAUf*0.25f );
-      q_mul( pose.keyframes[0].q, qp, cam->transform.q );
-      q_normalize( cam->transform.q );
-
-      v3_add( ent->transform.co, cam->transform.co, cam->transform.co );
-   }
-
-   world_entity_focus_camera( world, ent->camera );
-
-   if( button_down( k_srbind_mback ) )
-   {
-      world_entity_exit_modal();
-      world_entity_clear_focus();
-      gui_helper_clear();
-   }
-}
-
-void npc_update( ent_npc *ent )
-{
-   if( ent->id == 3 ) return;
-   if( ent->id == 4 ) return;
-
-   struct npc *npc_def = npc_resolve( ent->id );
-   VG_ASSERT( npc_def );
-
-   player_pose pose;
-   struct skeleton *sk = &npc_def->skeleton;
-   pose.type = k_player_pose_type_ik;
-   pose.board.lean = 0.0f;
-
-   if( ent->id == 1 )
-   {
-      skeleton_sample_anim( sk, gumpa_idle, vg.time, pose.keyframes );
-   }
-   else if( ent->id == 2 )
-   {
-      struct skeleton_anim *anim = NULL;
-      if( ent->context == 1 ) anim = slowmo_momentum;
-      else if( ent->context == 2 ) anim = slowmo_slide;
-      else if( ent->context == 3 ) anim = slowmo_rewind;
-
-      VG_ASSERT( anim );
-
-      f32 t        = vg.time*0.5f,
-          animtime = fmodf( t*anim->rate, anim->length ),
-          lt       = animtime / (f32)anim->length;
-      skeleton_sample_anim( sk, anim, t, pose.keyframes );
-      slowmo_opacity = vg_clampf(fabsf(lt-0.5f)*9.0f-3.0f,0,1);
-   }
-
-   v3_copy( ent->transform.co, pose.root_co );
-   v4_copy( ent->transform.q, pose.root_q );
-   apply_full_skeleton_pose( &npc_def->skeleton, &pose, npc_def->final_mtx );
-}
-
-void npc_render( ent_npc *ent, world_instance *world, vg_camera *cam )
-{
-   if( ent->id == 3 ) return;
-   if( ent->id == 4 ) return;
-
-   struct npc *npc_def = npc_resolve( ent->id );
-   VG_ASSERT( npc_def );
-
-   shader_model_character_view_use();
-
-       glActiveTexture( GL_TEXTURE0 );
-       glBindTexture( GL_TEXTURE_2D, npc_def->texture );
-   shader_model_character_view_uTexMain( 0 );
-   shader_model_character_view_uCamera( cam->transform[3] );
-   shader_model_character_view_uPv( cam->mtx.pv );
-
-   if( ent->id == 2 )
-   {
-      shader_model_character_view_uDepthMode( 2 );
-      shader_model_character_view_uDitherCutoff( slowmo_opacity );
-   }
-   else 
-   {
-      shader_model_character_view_uDepthMode( 0 );
-   }
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_character_view );
-
-   glUniformMatrix4x3fv( _uniform_model_character_view_uTransforms,
-                         npc_def->skeleton.bone_count,
-                         0,
-                         (const GLfloat *)npc_def->final_mtx );
-   
-   mesh_bind( &npc_def->mesh );
-   mesh_draw( &npc_def->mesh );
-}
diff --git a/ent_npc.h b/ent_npc.h
deleted file mode 100644 (file)
index c20cc97..0000000
--- a/ent_npc.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma once
-#include "player_render.h"
-#include "entity.h"
-
-struct npc
-{
-   glmesh mesh;
-   GLuint texture;
-
-   mdl_context meta;
-   struct skeleton skeleton;
-
-   m4x3f *final_mtx;
-}
-extern npc_gumpa;
-
-enum npc_id
-{
-   k_npc_id_none = 0,
-   k_npc_id_gumpa = 1
-};
-
-void npc_load_model( struct npc *npc, const char *path );
-void ent_npc_preupdate( ent_focus_context *context );
-entity_call_result ent_npc_call( world_instance *world, ent_call *call );
-void npc_update( ent_npc *ent );
-void npc_render( ent_npc *ent, world_instance *world, vg_camera *cam );
-void npc_init(void);
diff --git a/ent_objective.c b/ent_objective.c
deleted file mode 100644 (file)
index 6a8bbe5..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-#include "world.h"
-#include "world_load.h"
-#include "entity.h"
-#include "audio.h"
-#include "steam.h"
-#include "ent_region.h"
-#include "player.h"
-#include "player_skate.h"
-
-static void ent_objective_pass( world_instance *world, 
-                                   ent_objective *objective ){
-   if( objective->id_next ){
-      world_static.challenge_timer += objective->filter;
-
-      u32 index = mdl_entity_id_id( objective->id_next );
-      ent_objective *next = mdl_arritm( &world->ent_objective, index );
-      world_static.challenge_target = next;
-      objective->flags |= k_ent_objective_passed;
-
-      if( next->filter & k_ent_objective_filter_passthrough )
-         ent_objective_pass( world, next );
-      else{
-         vg_info( "pass challenge point\n" );
-         audio_lock();
-         audio_oneshot_3d( &audio_challenge[0], localplayer.rb.co,
-                           30.0f, 1.0f );
-         audio_unlock();
-      }
-   }
-   else {
-      vg_success( "challenge win\n" );
-      audio_lock();
-      audio_oneshot( &audio_challenge[2], 1.0f, 0.0f );
-      audio_unlock();
-      world_static.challenge_target = NULL;
-      world_static.challenge_timer = 0.0f;
-      world_static.focused_entity = 0;
-
-      if( objective->id_win ){
-         ent_call call;
-         call.data = NULL;
-         call.function = objective->win_event;
-         call.id = objective->id_win;
-         entity_call( world, &call );
-      }
-
-      ent_region_re_eval( world );
-   }
-}
-
-static int ent_objective_check_filter( ent_objective *objective ){
-   if( objective->filter ){
-      struct player_skate_state *s = &player_skate.state;
-      enum trick_type trick = s->trick_type;
-
-      u32 state = 0x00;
-
-      if( trick == k_trick_type_shuvit ) 
-         state |= k_ent_objective_filter_trick_shuvit;
-      if( trick == k_trick_type_treflip )
-         state |= k_ent_objective_filter_trick_treflip;
-      if( trick == k_trick_type_kickflip )
-         state |= k_ent_objective_filter_trick_kickflip;
-      
-      if( s->flip_rate < -0.0001f ) state |= k_ent_objective_filter_flip_back;
-      if( s->flip_rate >  0.0001f ) state |= k_ent_objective_filter_flip_front;
-
-      if( s->activity == k_skate_activity_grind_5050 ||
-          s->activity == k_skate_activity_grind_back50 ||
-          s->activity == k_skate_activity_grind_front50 ) 
-         state |= k_ent_objective_filter_grind_truck_any;
-
-      if( s->activity == k_skate_activity_grind_boardslide )
-         state |= k_ent_objective_filter_grind_board_any;
-
-      return ((objective->filter  & state) || !objective->filter) && 
-             ((objective->filter2 & state) || !objective->filter2);
-   }
-   else {
-      return 1;
-   }
-}
-
-entity_call_result ent_objective_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-
-   if( call->function == 0 )
-   {
-      if( objective->flags & (k_ent_objective_hidden|k_ent_objective_passed))
-      {
-         return k_entity_call_result_OK;
-      }
-
-      if( world_static.challenge_target )
-      {
-         if( (world_static.challenge_target == objective) && 
-              ent_objective_check_filter( objective )){
-            ent_objective_pass( world, objective );
-         }
-         else 
-         {
-            audio_lock();
-            audio_oneshot_3d( &audio_challenge[6], localplayer.rb.co,
-                              30.0f, 1.0f );
-            audio_unlock();
-            vg_error( "challenge failed\n" );
-            world_static.challenge_target = NULL;
-            world_static.challenge_timer = 0.0f;
-            world_static.focused_entity = 0;
-         }
-      }
-
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 2 )
-   {
-      objective->flags &= ~k_ent_objective_hidden;
-
-      if( mdl_entity_id_type( objective->id_next ) == k_ent_objective ){
-         call->id = objective->id_next;
-         entity_call( world, call );
-      }
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 3 )
-   {
-      objective->flags |=  k_ent_objective_hidden;
-
-      if( mdl_entity_id_type( objective->id_next ) == k_ent_objective ){
-         call->id = objective->id_next;
-         entity_call( world, call );
-      }
-      return k_entity_call_result_OK;
-   }
-   else 
-      return k_entity_call_result_unhandled;
-}
diff --git a/ent_objective.h b/ent_objective.h
deleted file mode 100644 (file)
index 9d94772..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-#pragma once
-#include "entity.h"
-#include "world.h"
-entity_call_result ent_objective_call( world_instance *world, ent_call *call );
diff --git a/ent_region.c b/ent_region.c
deleted file mode 100644 (file)
index 5e0ec03..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-#include "ent_region.h"
-#include "gui.h"
-#include "network_common.h"
-#include "network.h"
-
-struct global_ent_region global_ent_region;
-
-u32 region_spark_colour( u32 flags )
-{
-   if( flags & k_ent_route_flag_achieve_gold )
-      return 0xff8ce0fa;
-   else if( flags & k_ent_route_flag_achieve_silver )
-      return 0xffc2c2c2;
-   else 
-      return 0x00;
-}
-
-entity_call_result ent_region_call( world_instance *world, ent_call *call )
-{
-   ent_region *region = 
-      mdl_arritm( &world->ent_region, mdl_entity_id_id(call->id) );
-
-   if( !region->zone_volume )
-      return k_entity_call_result_invalid;
-
-   ent_volume *volume = 
-      mdl_arritm( &world->ent_volume, mdl_entity_id_id(region->zone_volume) );
-
-   if( call->function == 0 ) /* enter */
-   {
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i ++ )
-      {
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-
-         v3f local;
-         m4x3_mulv( volume->to_local, route->board_transform[3], local );
-         if( (fabsf(local[0]) <= 1.0f) &&
-             (fabsf(local[1]) <= 1.0f) &&
-             (fabsf(local[2]) <= 1.0f) )
-         {
-            route->flags &= ~k_ent_route_flag_out_of_zone;
-         }
-         else 
-         {
-            route->flags |= k_ent_route_flag_out_of_zone;
-         }
-      }
-
-      gui_location_print_ccmd( 1, (const char *[]){
-            mdl_pstr(&world->meta,region->pstr_title)} );
-
-      vg_strncpy( mdl_pstr(&world->meta,region->pstr_title), 
-                  global_ent_region.location, NETWORK_REGION_MAX,
-                  k_strncpy_always_add_null );
-      global_ent_region.flags = region->flags;
-      network_send_region();
-
-      localplayer.effect_data.spark.colour = region_spark_colour(region->flags);
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == 1 ) /* leave */
-   {
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i ++ )
-      {
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-         route->flags |= k_ent_route_flag_out_of_zone;
-      }
-      localplayer.effect_data.spark.colour = 0x00;
-      return k_entity_call_result_OK;
-   }
-   else
-      return k_entity_call_result_unhandled;
-}
-
-/* 
- * reevaluate all achievements to calculate the compiled achievement
- */
-void ent_region_re_eval( world_instance *world )
-{
-   u32 world_total = k_ent_route_flag_achieve_gold | 
-                     k_ent_route_flag_achieve_silver;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_region); i ++ ){
-      ent_region *region = mdl_arritm(&world->ent_region, i);
-
-      if( !region->zone_volume )
-         continue;
-
-      ent_volume *volume = mdl_arritm(&world->ent_volume,
-            mdl_entity_id_id(region->zone_volume));
-
-      u32 combined = k_ent_route_flag_achieve_gold | 
-                     k_ent_route_flag_achieve_silver;
-
-      for( u32 j=0; j<mdl_arrcount(&world->ent_route); j ++ ){
-         ent_route *route = mdl_arritm(&world->ent_route, j );
-
-         v3f local;
-         m4x3_mulv( volume->to_local, route->board_transform[3], local );
-         if( !((fabsf(local[0]) <= 1.0f) &&
-               (fabsf(local[1]) <= 1.0f) &&
-               (fabsf(local[2]) <= 1.0f)) ){
-            continue;
-         }
-
-         combined &= route->flags;
-      }
-
-      for( u32 j=0; j<mdl_arrcount(&world->ent_challenge); j ++ ){
-         ent_challenge *challenge = mdl_arritm( &world->ent_challenge, j );
-
-         v3f local;
-         m4x3_mulv( volume->to_local, challenge->transform.co, local );
-         if( !((fabsf(local[0]) <= 1.0f) &&
-               (fabsf(local[1]) <= 1.0f) &&
-               (fabsf(local[2]) <= 1.0f)) ){
-            continue;
-         }
-
-         u32 flags = 0x00;
-         if( challenge->status ){
-            flags |= k_ent_route_flag_achieve_gold;
-            flags |= k_ent_route_flag_achieve_silver;
-         }
-
-         combined &= flags;
-      }
-
-      region->flags = combined;
-      world_total &= combined;
-
-      /* run unlock triggers. v105+ */
-      if( world->meta.info.version >= 105 ){
-         if( region->flags & (k_ent_route_flag_achieve_gold|
-                              k_ent_route_flag_achieve_silver) ){
-            if( region->target0[0] ){
-               ent_call call;
-               call.data = NULL;
-               call.id = region->target0[0];
-               call.function = region->target0[1];
-               entity_call( world, &call );
-            }
-         }
-      }
-   }
-
-   u32 instance_id = world - world_static.instances;
-
-   if( world_static.instance_addons[instance_id]->flags & ADDON_REG_MTZERO ){
-      if( world_total & k_ent_route_flag_achieve_gold ){
-         steam_set_achievement( "MTZERO_GOLD" );
-         steam_store_achievements();
-      }
-
-      if( world_total & k_ent_route_flag_achieve_silver ){
-         steam_set_achievement( "MTZERO_SILVER" );
-         steam_store_achievements();
-      }
-   }
-
-   if( world_static.instance_addons[instance_id]->flags & ADDON_REG_CITY ){
-      steam_set_achievement( "CITY_COMPLETE" );
-      steam_store_achievements();
-   }
-}
diff --git a/ent_region.h b/ent_region.h
deleted file mode 100644 (file)
index 6a86a32..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma once
-#include "world_entity.h"
-#include "network_common.h"
-
-struct global_ent_region
-{
-   char location[ NETWORK_REGION_MAX ];
-   u32 flags;
-}
-extern global_ent_region;
-
-u32 region_spark_colour( u32 flags );
-void ent_region_re_eval( world_instance *world );
-entity_call_result ent_region_call( world_instance *world, ent_call *call );
diff --git a/ent_relay.c b/ent_relay.c
deleted file mode 100644 (file)
index b22b0b5..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-#include "ent_relay.h"
-
-entity_call_result ent_relay_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_relay *relay = mdl_arritm( &world->ent_relay, index );
-
-   if( call->function == 0 )
-   {
-      for( u32 i=0; i<VG_ARRAY_LEN(relay->targets); i++ )
-      {
-         if( relay->targets[i][0] )
-         {
-            ent_call call;
-            call.data = NULL;
-            call.function = relay->targets[i][1];
-            call.id = relay->targets[i][0];
-            entity_call( world, &call );
-         }
-      }
-      return k_entity_call_result_OK;
-   }
-   else 
-      return k_entity_call_result_unhandled;
-}
diff --git a/ent_relay.h b/ent_relay.h
deleted file mode 100644 (file)
index 054570c..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-#include "entity.h"
-entity_call_result ent_relay_call( world_instance *world, ent_call *call );
diff --git a/ent_route.c b/ent_route.c
deleted file mode 100644 (file)
index 8564eed..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-#include "ent_route.h"
-#include "input.h"
-#include "gui.h"
-
-struct global_ent_route global_ent_route;
-
-entity_call_result ent_route_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_route *route = mdl_arritm( &world->ent_route, index );
-
-   if( call->function == 0 )
-   { /* view() */
-      if( localplayer.subsystem == k_player_subsystem_walk )
-      {
-         world_entity_set_focus( call->id );
-         world_entity_focus_modal();
-
-         gui_helper_clear();
-         vg_str text;
-
-         if( (global_ent_route.helper_weekly = 
-                  gui_new_helper( input_button_list[k_srbind_mleft], &text )))
-            vg_strcat( &text, "Weekly" );
-
-         if( (global_ent_route.helper_alltime = 
-                  gui_new_helper( input_button_list[k_srbind_mright], &text )))
-            vg_strcat( &text, "All time" );
-
-         if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
-            vg_strcat( &text, "Exit" );
-      }
-
-      return k_entity_call_result_OK;
-   }
-
-   return k_entity_call_result_unhandled;
-}
-
-void ent_route_preupdate( ent_focus_context *ctx )
-{
-   if( !ctx->active ) 
-      return;
-
-   world_instance *world = ctx->world;
-   ent_route *route = mdl_arritm( &world->ent_route, ctx->index );
-
-   u32 cam_id = 0;
-   
-   if( __builtin_expect( world->meta.info.version >= 103, 1 ) )
-      cam_id = route->id_camera;
-
-   world_entity_focus_camera( world, cam_id );
-
-   if( button_down( k_srbind_mleft ) ){
-      world_sfd.view_weekly = 1;
-      world_sfd_compile_active_scores();
-   }
-
-   if( button_down( k_srbind_mright ) ){
-      world_sfd.view_weekly = 0;
-      world_sfd_compile_active_scores();
-   }
-
-   global_ent_route.helper_alltime->greyed =!world_sfd.view_weekly;
-   global_ent_route.helper_weekly->greyed =  world_sfd.view_weekly;
-
-   if( button_down( k_srbind_mback ) )
-   {
-      world_entity_exit_modal();
-      world_entity_clear_focus();
-      gui_helper_clear();
-      return;
-   }
-}
diff --git a/ent_route.h b/ent_route.h
deleted file mode 100644 (file)
index cbf61b2..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma once
-#include "entity.h"
-
-struct global_ent_route
-{
-   struct gui_helper *helper_weekly, *helper_alltime;
-}
-extern global_ent_route;
-
-entity_call_result ent_route_call( world_instance *world, ent_call *call );
-void ent_route_preupdate( ent_focus_context *ctx );
diff --git a/ent_skateshop.c b/ent_skateshop.c
deleted file mode 100644 (file)
index ded707c..0000000
+++ /dev/null
@@ -1,850 +0,0 @@
-#include "vg/vg_steam_ugc.h"
-#include "vg/vg_msg.h"
-#include "vg/vg_tex.h"
-#include "vg/vg_image.h"
-#include "vg/vg_loader.h"
-#include "ent_skateshop.h"
-#include "world.h"
-#include "player.h"
-#include "gui.h"
-#include "menu.h"
-#include "steam.h"
-#include "addon.h"
-#include "save.h"
-#include "network.h"
-
-struct global_skateshop global_skateshop = 
-{
-   .render={.reg_id=0xffffffff,.world_reg=0xffffffff}
-};
-
-/*
- * Checks string equality but does a hash check first
- */
-static inline int const_str_eq( u32 hash, const char *str, const char *cmp )
-{
-   if( hash == vg_strdjb2(cmp) )
-      if( !strcmp( str, cmp ) )
-         return 1;
-   return 0;
-}
-
-static void skateshop_update_viewpage(void){
-   u32 page = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
-
-   for( u32 i=0; i<SKATESHOP_VIEW_SLOT_MAX; i++ ){
-      u32 j = SKATESHOP_VIEW_SLOT_MAX-1-i;
-      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[j];
-      addon_cache_unwatch( k_addon_type_board, slot->cache_id );
-   }
-   
-   for( u32 i=0; i<SKATESHOP_VIEW_SLOT_MAX; i++ ){
-      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[i];
-      u32 request_id = page*SKATESHOP_VIEW_SLOT_MAX + i;
-      slot->cache_id = addon_cache_create_viewer( k_addon_type_board,
-                                                  request_id );
-   }
-}
-
-struct async_preview_load_thread_data{
-   void *data;
-   addon_reg *reg;
-};
-
-static void skateshop_async_preview_imageload( void *data, u32 len ){
-   struct async_preview_load_thread_data *inf = data;
-
-   if( inf->data ){
-      glBindTexture( GL_TEXTURE_2D, global_skateshop.tex_preview );
-      glTexSubImage2D( GL_TEXTURE_2D, 0,0,0,
-                        WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT, 
-                        GL_RGB, GL_UNSIGNED_BYTE, inf->data );
-      glGenerateMipmap( GL_TEXTURE_2D );
-      stbi_image_free( inf->data );
-
-      skaterift.rt_textures[k_skaterift_rt_workshop_preview] =
-         global_skateshop.tex_preview;
-   }
-   else {
-      skaterift.rt_textures[k_skaterift_rt_workshop_preview] = vg.tex_missing;
-   }
-
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-   global_skateshop.reg_loaded_preview = inf->reg;
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-}
-
-static void skateshop_update_preview_image_thread(void *_args)
-{
-   char path_buf[4096];
-   vg_str folder;
-   vg_strnull( &folder, path_buf, sizeof(path_buf) );
-
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-   addon_reg *reg_preview = global_skateshop.reg_preview;
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-
-   if( !addon_get_content_folder( reg_preview, &folder, 1 ) )
-   {
-      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-      global_skateshop.reg_loaded_preview = reg_preview;
-      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-      return;
-   }
-
-   vg_strcat( &folder, "/preview.jpg" );
-   vg_async_item *call = 
-      vg_async_alloc( sizeof(struct async_preview_load_thread_data) );
-   struct async_preview_load_thread_data *inf = call->payload;
-
-   inf->reg = reg_preview;
-
-   if( vg_strgood( &folder ) )
-   {
-      stbi_set_flip_vertically_on_load(1);
-      int x, y, nc;
-      inf->data = stbi_load( folder.buffer, &x, &y, &nc, 3 );
-
-      if( inf->data )
-      {
-         if( (x != WORKSHOP_PREVIEW_WIDTH) || (y != WORKSHOP_PREVIEW_HEIGHT) )
-         {
-            vg_error( "Resolution does not match framebuffer, so we can't"
-                      " show it\n" );
-            stbi_image_free( inf->data );
-            inf->data = NULL;
-         }
-      }
-
-      vg_async_dispatch( call, skateshop_async_preview_imageload );
-   }
-   else
-   {
-      vg_error( "Path too long to workshop preview image.\n" );
-
-      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-      global_skateshop.reg_loaded_preview = reg_preview;
-      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-   }
-}
-
-void skateshop_world_preview_preupdate(void)
-{
-   /* try to load preview image if we availible to do. */
-   if( vg_loader_availible() )
-   {
-      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-      if( global_skateshop.reg_preview != global_skateshop.reg_loaded_preview )
-      {
-         SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-         vg_loader_start( skateshop_update_preview_image_thread, NULL );
-      }
-      else SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-   }
-}
-
-/*
- * op/subroutine: k_workshop_op_item_load
- * -----------------------------------------------------------------------------
- */
-
-/*
- * Regular stuff
- * -----------------------------------------------------------------------------
- */
-
-static void skateshop_init_async(void *_data,u32 size){
-       glGenTextures( 1, &global_skateshop.tex_preview );
-       glBindTexture( GL_TEXTURE_2D, global_skateshop.tex_preview );
-   glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, 
-                  WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT,
-                  0, GL_RGB, GL_UNSIGNED_BYTE, NULL );
-
-   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
-                    GL_LINEAR_MIPMAP_LINEAR );
-   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
-   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
-   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
-
-   skaterift.rt_textures[ k_skaterift_rt_workshop_preview ] = vg.tex_missing;
-   skaterift.rt_textures[ k_skaterift_rt_server_status ] = vg.tex_missing;
-   render_server_status_gui();
-}
-
-/*
- * VG event init
- */
-void skateshop_init(void)
-{
-   vg_async_call( skateshop_init_async, NULL, 0 );
-}
-
-static u16 skateshop_selected_cache_id(void){
-   if( addon_count(k_addon_type_board, ADDON_REG_HIDDEN) ){
-      addon_reg *reg = get_addon_from_index( 
-            k_addon_type_board, global_skateshop.selected_board_id,
-            ADDON_REG_HIDDEN );
-      return reg->cache_id;
-   }
-   else return 0;
-}
-
-static void skateshop_server_helper_update(void){
-   vg_str text;
-   vg_strnull( &text, global_skateshop.helper_toggle->text, 
-               sizeof(global_skateshop.helper_toggle->text) );
-
-   if( skaterift.demo_mode ){
-      vg_strcat( &text, "Not availible in demo" );
-   }
-   else {
-      if( network_client.user_intent == k_server_intent_online )
-         vg_strcat( &text, "Disconnect" );
-      else
-         vg_strcat( &text, "Go Online" );
-   }
-}
-
-/*
- * VG event preupdate 
- */
-void temp_update_playermodel(void);
-void ent_skateshop_preupdate( ent_focus_context *ctx )
-{
-   if( !ctx->active ) 
-      return;
-
-   world_instance *world = ctx->world;
-   ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, ctx->index );
-
-   /* camera positioning */
-   ent_camera *ref = mdl_arritm( &world->ent_camera, 
-                                 mdl_entity_id_id(shop->id_camera) );
-      
-   v3f dir = {0.0f,-1.0f,0.0f};
-   mdl_transform_vector( &ref->transform, dir, dir );
-   v3_angles( dir, world_static.focus_cam.angles );
-
-   v3f lookat;
-   if( shop->type == k_skateshop_type_boardshop ||
-       shop->type == k_skateshop_type_worldshop ){
-      ent_marker *display = mdl_arritm( &world->ent_marker,
-                                    mdl_entity_id_id(shop->boards.id_display) );
-      v3_sub( display->transform.co, localplayer.rb.co, lookat );
-   }
-   else if( shop->type == k_skateshop_type_charshop ){
-      v3_sub( ref->transform.co, localplayer.rb.co, lookat );
-   }
-   else if( shop->type == k_skateshop_type_server ){
-      ent_prop *prop = mdl_arritm( &world->ent_prop,
-            mdl_entity_id_id(shop->server.id_lever) );
-      v3_sub( prop->transform.co, localplayer.rb.co, lookat );
-   }
-   else
-      vg_fatal_error( "Unknown store (%u)\n", shop->type );
-
-   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
-                 atan2f(lookat[0],lookat[2]) );
-
-   v3_copy( ref->transform.co, world_static.focus_cam.pos );
-   world_static.focus_cam.fov = ref->fov;
-
-   /* input */
-   if( shop->type == k_skateshop_type_boardshop ){
-      if( !vg_loader_availible() ) return;
-
-      u16 cache_id = skateshop_selected_cache_id();
-      global_skateshop.helper_pick->greyed = !cache_id;
-
-      /*
-       * Controls
-       * ----------------------
-       */
-      u32 opage = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
-
-      if( button_down( k_srbind_mleft ) ){
-         if( global_skateshop.selected_board_id > 0 ){
-            global_skateshop.selected_board_id --;
-         }
-      }
-
-      u32 valid_count = addon_count( k_addon_type_board, 0 );
-      if( button_down( k_srbind_mright ) ){
-         if( global_skateshop.selected_board_id+1 < valid_count ){
-            global_skateshop.selected_board_id ++;
-         }
-      }
-
-      u32 npage = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
-
-      if( opage != npage ){
-         skateshop_update_viewpage();
-      }
-      else if( cache_id && button_down( k_srbind_maccept )){
-         vg_info( "chose board from skateshop (%u)\n", 
-                     global_skateshop.selected_board_id );
-
-         addon_cache_unwatch( k_addon_type_board, localplayer.board_view_slot );
-         addon_cache_watch( k_addon_type_board, cache_id );
-         localplayer.board_view_slot = cache_id;
-         network_send_item( k_netmsg_playeritem_board );
-
-         world_entity_exit_modal();
-         world_entity_clear_focus();
-         gui_helper_clear();
-         skaterift_autosave(1);
-         return;
-      }
-   }
-   else if( shop->type == k_skateshop_type_charshop ){
-      if( !vg_loader_availible() ) return;
-
-      int changed = 0;
-      u32 valid_count = addon_count( k_addon_type_player, ADDON_REG_HIDDEN );
-
-      if( button_down( k_srbind_mleft ) ){
-         if( global_skateshop.selected_player_id > 0 ){
-            global_skateshop.selected_player_id --;
-         }
-         else{
-            global_skateshop.selected_player_id = valid_count-1;
-         }
-
-         changed = 1;
-      }
-
-      if( button_down( k_srbind_mright ) ){
-         if( global_skateshop.selected_player_id+1 < valid_count ){
-            global_skateshop.selected_player_id ++;
-         }
-         else{
-            global_skateshop.selected_player_id = 0;
-         }
-
-         changed = 1;
-      }
-
-      if( changed ){
-         addon_reg *addon = get_addon_from_index( 
-               k_addon_type_player, global_skateshop.selected_player_id,
-               ADDON_REG_HIDDEN );
-
-         u32 real_id = get_index_from_addon( 
-               k_addon_type_player, addon );
-
-         player__use_model( real_id );
-      }
-
-      if( button_down( k_srbind_maccept ) ){
-         network_send_item( k_netmsg_playeritem_player );
-         world_entity_exit_modal();
-         world_entity_clear_focus();
-         gui_helper_clear();
-      }
-   }
-   else if( shop->type == k_skateshop_type_worldshop ){
-      int browseable = 0,
-          loadable = 0;
-
-      u32 valid_count = addon_count( k_addon_type_world, ADDON_REG_HIDDEN );
-
-      if( valid_count && vg_loader_availible() )
-         browseable = 1;
-
-      if( valid_count && vg_loader_availible() )
-         loadable = 1;
-
-      global_skateshop.helper_browse->greyed = !browseable;
-      global_skateshop.helper_pick->greyed = !loadable;
-      
-      addon_reg *selected_world = NULL;
-
-      int change = 0;
-      if( browseable ){
-         if( button_down( k_srbind_mleft ) ){
-            if( global_skateshop.selected_world_id > 0 ){
-               global_skateshop.selected_world_id --;
-               change = 1;
-            }
-         }
-
-         if( button_down( k_srbind_mright ) ){
-            if( global_skateshop.selected_world_id+1 < valid_count ){
-               global_skateshop.selected_world_id ++;
-               change = 1;
-            }
-         }
-
-         selected_world = get_addon_from_index( k_addon_type_world, 
-                     global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
-
-         if( change || (global_skateshop.reg_preview == NULL) ){
-            SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-            global_skateshop.reg_preview = selected_world;
-            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-         }
-      }
-
-      if( loadable ){
-         if( button_down( k_srbind_maccept ) ){
-            skaterift_change_world_start( selected_world );
-         }
-      }
-   }
-   else if( shop->type == k_skateshop_type_server ){
-      f64 delta = vg.time_real - network_client.last_intent_change;
-
-      if( (delta > 5.0) && (!skaterift.demo_mode) ){
-         global_skateshop.helper_pick->greyed = 0;
-         if( button_down( k_srbind_maccept ) ){
-            network_client.user_intent = !network_client.user_intent;
-            network_client.last_intent_change = vg.time_real;
-            skateshop_server_helper_update();
-            render_server_status_gui();
-         }
-      }
-      else {
-         global_skateshop.helper_pick->greyed = 1;
-      }
-   }
-   else{
-      vg_fatal_error( "Unknown store (%u)\n", shop->type );
-   }
-
-   if( button_down( k_srbind_mback ) )
-   {
-      if( shop->type == k_skateshop_type_charshop )
-         network_send_item( k_netmsg_playeritem_player );
-
-      world_entity_exit_modal();
-      world_entity_clear_focus();
-      gui_helper_clear();
-      return;
-   }
-}
-
-void skateshop_world_preupdate( world_instance *world )
-{
-   for( u32 i=0; i<mdl_arrcount(&world->ent_skateshop); i++ ){
-      ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, i );
-
-      if( shop->type == k_skateshop_type_server ){
-         f32 a = network_client.user_intent;
-
-         vg_slewf( &network_client.fintent, a, vg.time_frame_delta );
-         a = (vg_smoothstepf( network_client.fintent ) - 0.5f) * (VG_PIf/2.0f);
-
-         ent_prop *lever = mdl_arritm( &world->ent_prop,
-               mdl_entity_id_id(shop->server.id_lever) );
-
-         /* we need parent transforms now? */
-         q_axis_angle( lever->transform.q, (v3f){0,0,1}, a );
-      }
-   }
-}
-
-static void skateshop_render_boardshop( ent_skateshop *shop ){
-   world_instance *world = world_current_instance();
-   u32 slot_count = VG_ARRAY_LEN(global_skateshop.shop_view_slots);
-
-   ent_marker *mark_rack = mdl_arritm( &world->ent_marker, 
-                                  mdl_entity_id_id(shop->boards.id_rack)),
-              *mark_display = mdl_arritm( &world->ent_marker,
-                                  mdl_entity_id_id(shop->boards.id_display));
-
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-   struct addon_cache *cache = &addon_system.cache[k_addon_type_board];
-   
-   /* Render loaded boards in the view slots */
-   for( u32 i=0; i<slot_count; i++ ){
-      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[i];
-      float selected = 0.0f;
-
-      if( !slot->cache_id ) 
-         goto fade_out;
-
-      addon_cache_entry *entry = vg_pool_item( &cache->pool, slot->cache_id );
-
-      if( entry->state != k_addon_cache_state_loaded )
-         goto fade_out;
-
-      struct player_board *board = 
-         addon_cache_item( k_addon_type_board, slot->cache_id );
-
-      mdl_transform xform;
-      transform_identity( &xform );
-
-      xform.co[0] = -((float)i - ((float)slot_count)*0.5f)*0.45f;
-      mdl_transform_mul( &mark_rack->transform, &xform, &xform );
-
-
-      if( entry->reg_index == global_skateshop.selected_board_id ){
-         selected = 1.0f;
-      }
-
-      float t = slot->view_blend;
-      v3_lerp( xform.co, mark_display->transform.co, t, xform.co );
-      q_nlerp( xform.q, mark_display->transform.q, t, xform.q );
-      v3_lerp( xform.s, mark_display->transform.s, t, xform.s );
-
-      struct player_board_pose pose = {0};
-      m4x3f mmdl;
-      mdl_transform_m4x3( &xform, mmdl );
-      render_board( &g_render.cam, world, board, mmdl, 
-                    &pose, k_board_shader_entity );
-
-fade_out:;
-      float rate = 5.0f*vg.time_delta;
-      slot->view_blend = vg_lerpf( slot->view_blend, selected, rate );
-   }
-
-   ent_marker *mark_info = mdl_arritm( &world->ent_marker, 
-                                        mdl_entity_id_id(shop->boards.id_info));
-   m4x3f mtext, mrack;
-   mdl_transform_m4x3( &mark_info->transform, mtext );
-   mdl_transform_m4x3( &mark_rack->transform, mrack );
-
-   m4x3f mlocal, mmdl;
-   m4x3_identity( mlocal );
-
-   float scale = 0.2f,
-         thickness = 0.03f;
-
-   font3d_bind( &gui.font, k_font_shader_default, 0, world, &g_render.cam );
-   shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
-
-   /* Selection counter
-    * ------------------------------------------------------------------ */
-   m3x3_zero( mlocal );
-   v3_zero( mlocal[3] );
-   mlocal[0][0] = -scale*2.0f;
-   mlocal[1][2] = -scale*2.0f;
-   mlocal[2][1] = -thickness;
-   mlocal[3][2] = -0.7f;
-   m4x3_mul( mrack, mlocal, mmdl );
-
-   u32 valid_count = addon_count(k_addon_type_board,0);
-   if( valid_count ){
-      char buf[16];
-      vg_str str;
-      vg_strnull( &str, buf, sizeof(buf) );
-      vg_strcati32( &str, global_skateshop.selected_board_id+1 );
-      vg_strcatch( &str, '/' );
-      vg_strcati32( &str, valid_count );
-      font3d_simple_draw( 0, buf, &g_render.cam, mmdl );
-   }
-   else{
-      font3d_simple_draw( 0, "Nothing installed", &g_render.cam, mmdl );
-   }
-
-   u16 cache_id = skateshop_selected_cache_id();
-   struct addon_cache_entry *entry = vg_pool_item( &cache->pool, cache_id );
-   addon_reg *reg = NULL;
-
-   if( entry ) reg = entry->reg_ptr;
-
-   if( !reg ){
-      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-      global_skateshop.render.item_title = "";
-      global_skateshop.render.item_desc = "";
-      return;
-   }
-
-   if( global_skateshop.render.reg_id != global_skateshop.selected_board_id ){
-      global_skateshop.render.item_title = "";
-      global_skateshop.render.item_desc = "";
-      vg_msg msg;
-      vg_msg_init( &msg, reg->metadata, reg->metadata_len );
-
-      if( vg_msg_seekframe( &msg, "workshop" ) ){
-         const char *title = vg_msg_getkvstr( &msg, "title" );
-         if( title ) global_skateshop.render.item_title = title;
-
-         const char *dsc = vg_msg_getkvstr( &msg, "author" );
-         if( dsc ) global_skateshop.render.item_desc = dsc;
-         vg_msg_skip_frame( &msg );
-      }
-
-      global_skateshop.render.reg_id = global_skateshop.selected_board_id;
-   }
-
-   /* Skin title
-    * ----------------------------------------------------------------- */
-   m3x3_zero( mlocal );
-   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
-   mlocal[3][0] = -font3d_string_width( 0, global_skateshop.render.item_title );
-   mlocal[3][0] *= scale*0.5f;
-   mlocal[3][1] = 0.1f;
-   mlocal[3][2] = 0.0f;
-   m4x3_mul( mtext, mlocal, mmdl );
-   font3d_simple_draw( 0, global_skateshop.render.item_title, 
-                       &g_render.cam, mmdl );
-
-   /* Author name
-    * ----------------------------------------------------------------- */
-   scale *= 0.4f;
-   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
-   mlocal[3][0] = -font3d_string_width( 0, global_skateshop.render.item_desc );
-   mlocal[3][0] *= scale*0.5f;
-   mlocal[3][1] = 0.0f;
-   mlocal[3][2] = 0.0f;
-   m4x3_mul( mtext, mlocal, mmdl );
-   font3d_simple_draw( 0, global_skateshop.render.item_desc, 
-                       &g_render.cam, mmdl );
-
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-}
-
-static void skateshop_render_charshop( ent_skateshop *shop ){
-}
-
-static void skateshop_render_worldshop( ent_skateshop *shop ){
-   world_instance *world = world_current_instance();
-
-   ent_marker *mark_display = mdl_arritm( &world->ent_marker,
-                                  mdl_entity_id_id(shop->worlds.id_display)),
-              *mark_info = mdl_arritm( &world->ent_marker, 
-                                  mdl_entity_id_id(shop->boards.id_info));
-
-   if( global_skateshop.render.world_reg != global_skateshop.selected_world_id){
-      global_skateshop.render.world_title = "missing: workshop.title";
-
-      addon_reg *reg = get_addon_from_index( k_addon_type_world,
-            global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
-
-      if( !reg ) 
-         goto none;
-
-      if( reg->alias.workshop_id )
-      {
-         vg_msg msg;
-         vg_msg_init( &msg, reg->metadata, reg->metadata_len );
-
-         global_skateshop.render.world_loc = vg_msg_getkvstr(&msg,"location");
-         global_skateshop.render.world_reg = global_skateshop.selected_world_id;
-
-         if( vg_msg_seekframe( &msg, "workshop" ) )
-         {
-            global_skateshop.render.world_title = vg_msg_getkvstr(&msg,"title");
-            vg_msg_skip_frame( &msg );
-         }
-         else {
-            vg_warn( "No workshop body\n" );
-         }
-      }
-      else {
-         global_skateshop.render.world_title = reg->alias.foldername;
-      }
-   }
-
-none:;
-
-   /* Text */
-   char buftext[128], bufsubtext[128];
-   vg_str info, subtext;
-   vg_strnull( &info, buftext, 128 );
-   vg_strnull( &subtext, bufsubtext, 128 );
-   
-   u32 valid_count = addon_count(k_addon_type_world,ADDON_REG_HIDDEN);
-   if( valid_count )
-   {
-      vg_strcati32( &info, global_skateshop.selected_world_id+1 );
-      vg_strcatch( &info, '/' );
-      vg_strcati32( &info, valid_count );
-      vg_strcatch( &info, ' ' );
-      vg_strcat( &info, global_skateshop.render.world_title );
-
-      if( !vg_loader_availible() )
-      {
-         vg_strcat( &subtext, "Loading..." );
-      }
-      else
-      {
-         addon_reg *reg = get_addon_from_index( k_addon_type_world,
-                     global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
-
-         if( reg->alias.workshop_id )
-            vg_strcat( &subtext, "(Workshop) " );
-
-         vg_strcat( &subtext, global_skateshop.render.world_loc );
-      }
-   }
-   else
-   {
-      vg_strcat( &info, "No workshop worlds installed" );
-   }
-
-   m4x3f mtext,mlocal,mtextmdl;
-   mdl_transform_m4x3( &mark_info->transform, mtext );
-
-   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, &g_render.cam );
-   shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
-
-   float scale = 0.2f, thickness = 0.015f, scale1 = 0.08f;
-   m3x3_zero( mlocal );
-   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
-   mlocal[3][0] = -font3d_string_width( 0, buftext );
-   mlocal[3][0] *= scale*0.5f;
-   mlocal[3][1] = 0.1f;
-   mlocal[3][2] = 0.0f;
-   m4x3_mul( mtext, mlocal, mtextmdl );
-   font3d_simple_draw( 0, buftext, &g_render.cam, mtextmdl );
-
-   m3x3_setdiagonalv3( mlocal, (v3f){ scale1, scale1, thickness } );
-   mlocal[3][0] = -font3d_string_width( 0, bufsubtext );
-   mlocal[3][0] *= scale1*0.5f;
-   mlocal[3][1] = -scale1*0.3f;
-   m4x3_mul( mtext, mlocal, mtextmdl );
-   font3d_simple_draw( 0, bufsubtext, &g_render.cam, mtextmdl );
-}
-
-/*
- * World: render event
- */
-void skateshop_render( ent_skateshop *shop )
-{
-   if( shop->type == k_skateshop_type_boardshop )
-      skateshop_render_boardshop( shop );
-   else if( shop->type == k_skateshop_type_charshop )
-      skateshop_render_charshop( shop );
-   else if( shop->type == k_skateshop_type_worldshop )
-      skateshop_render_worldshop( shop );
-   else if( shop->type == k_skateshop_type_server ){
-   }
-   else
-      vg_fatal_error( "Unknown store (%u)\n", shop->type );
-}
-
-void skateshop_render_nonfocused( world_instance *world, vg_camera *cam )
-{
-   for( u32 j=0; j<mdl_arrcount( &world->ent_skateshop ); j ++ )
-   {
-      ent_skateshop *shop = mdl_arritm(&world->ent_skateshop, j );
-
-      if( shop->type != k_skateshop_type_boardshop ) continue;
-
-      f32 dist2 = v3_dist2( cam->pos, shop->transform.co ),
-          maxdist = 50.0f;
-
-      if( dist2 > maxdist*maxdist ) continue;
-      ent_marker *mark_rack = mdl_arritm( &world->ent_marker, 
-                                     mdl_entity_id_id(shop->boards.id_rack));
-
-      if( !mark_rack ) 
-         continue;
-
-      u32 slot_count = VG_ARRAY_LEN(global_skateshop.shop_view_slots);
-      for( u32 i=0; i<slot_count; i++ )
-      {
-         struct player_board *board = &localplayer.fallback_board;
-
-         mdl_transform xform;
-         transform_identity( &xform );
-
-         xform.co[0] = -((float)i - ((float)slot_count)*0.5f)*0.45f;
-         mdl_transform_mul( &mark_rack->transform, &xform, &xform );
-
-         struct player_board_pose pose = {0};
-         m4x3f mmdl;
-         mdl_transform_m4x3( &xform, mmdl );
-         render_board( cam, world, board, mmdl, &pose, k_board_shader_entity );
-      }
-   }
-}
-
-static void ent_skateshop_helpers_pickable( const char *acceptance )
-{
-   vg_str text;
-
-   if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
-      vg_strcat( &text, "Exit" );
-
-   if( (global_skateshop.helper_pick = gui_new_helper(
-               input_button_list[k_srbind_maccept], &text))){
-      vg_strcat( &text, acceptance );
-   }
-
-   if( (global_skateshop.helper_browse = gui_new_helper( 
-               input_axis_list[k_sraxis_mbrowse_h], &text ))){
-      vg_strcat( &text, "Browse" );
-   }
-}
-
-static void board_scan_thread( void *_args )
-{
-   addon_mount_content_folder( k_addon_type_board, "boards", ".mdl" );
-   addon_mount_workshop_items();
-   vg_async_call( async_addon_reg_update, NULL, 0 );
-   vg_async_stall();
-
-   /* 04.03.24
-    * REVIEW: This is removed as it *should* be done on the preupdate of the 
-    *         addon system.
-    *
-    *         Verify that it works the same.
-    */
-#if 0
-   board_processview_thread(NULL);
-#endif
-}
-
-static void world_scan_thread( void *_args )
-{
-   addon_mount_content_folder( k_addon_type_world, "maps", ".mdl" );
-   addon_mount_workshop_items();
-   vg_async_call( async_addon_reg_update, NULL, 0 );
-}
-
-/*
- * Entity logic: entrance event
- */
-entity_call_result ent_skateshop_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, index );
-   vg_info( "skateshop_call\n" );
-
-   if( (skaterift.activity != k_skaterift_default) || 
-       !vg_loader_availible() ) 
-      return k_entity_call_result_invalid;
-
-   if( call->function == k_ent_function_trigger )
-   {
-      if( localplayer.subsystem != k_player_subsystem_walk )
-         return k_entity_call_result_OK;
-      
-      vg_info( "Entering skateshop\n" );
-
-      world_entity_set_focus( call->id );
-      world_entity_focus_modal();
-      gui_helper_clear();
-      
-      if( shop->type == k_skateshop_type_boardshop )
-      {
-         skateshop_update_viewpage();
-         vg_loader_start( board_scan_thread, NULL );
-         ent_skateshop_helpers_pickable( "Pick" );
-      }
-      else if( shop->type == k_skateshop_type_charshop )
-      {
-         ent_skateshop_helpers_pickable( "Pick" );
-      }
-      else if( shop->type == k_skateshop_type_worldshop )
-      {
-         ent_skateshop_helpers_pickable( "Open rift" );
-         vg_loader_start( world_scan_thread, NULL );
-      }
-      else if( shop->type == k_skateshop_type_server )
-      {
-         vg_str text;
-         global_skateshop.helper_pick = gui_new_helper(
-                     input_button_list[k_srbind_maccept], &text);
-         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
-            vg_strcat( &text, "exit" );
-         skateshop_server_helper_update();
-      }
-      return k_entity_call_result_OK;
-   }
-   else
-      return k_entity_call_result_unhandled;
-}
diff --git a/ent_skateshop.h b/ent_skateshop.h
deleted file mode 100644 (file)
index 2f8e3a6..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#pragma once
-#include "world.h"
-#include "world_load.h"
-#include "player.h"
-#include "vg/vg_steam_remote_storage.h"
-#include "workshop.h"
-#include "addon.h"
-
-#define SKATESHOP_VIEW_SLOT_MAX    5
-
-struct global_skateshop
-{
-   v3f look_target;
-
-   struct shop_view_slot{
-      u16 cache_id;
-      float view_blend;
-   }
-   shop_view_slots[ SKATESHOP_VIEW_SLOT_MAX ];
-
-   u32 selected_world_id,
-       selected_board_id,
-       selected_player_id,
-       pointcloud_world_id;
-
-   struct {
-      const char *item_title, *item_desc;
-      u32 reg_id;
-
-      const char *world_title, *world_loc;
-      u32 world_reg;
-   }
-   render;
-
-   union {
-      struct gui_helper *helper_pick, *helper_toggle;
-   };
-
-   struct gui_helper *helper_browse;
-
-
-   addon_reg *reg_preview, *reg_loaded_preview;
-   GLuint tex_preview;
-}
-extern global_skateshop;
-
-void skateshop_init(void);
-void ent_skateshop_preupdate( ent_focus_context *ctx );
-void skateshop_render( ent_skateshop *shop );
-void skateshop_render_nonfocused( world_instance *world, vg_camera *cam );
-void skateshop_autostart_loading(void);
-void skateshop_world_preupdate( world_instance *world );
-entity_call_result ent_skateshop_call( world_instance *world, ent_call *call );
-void skateshop_world_preview_preupdate(void);
diff --git a/ent_tornado.c b/ent_tornado.c
deleted file mode 100644 (file)
index f9fca03..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-#include "world.h"
-#include "particle.h"
-
-static f32 k_tornado_strength = 0.0f,
-           k_tornado_ratio    = 0.5f,
-           k_tornado_range    = 10.f;
-
-void ent_tornado_init(void)
-{
-   vg_console_reg_var( "k_tonado_strength", &k_tornado_strength,
-                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
-   vg_console_reg_var( "k_tonado_ratio", &k_tornado_ratio,
-                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
-   vg_console_reg_var( "k_tonado_range", &k_tornado_range,
-                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
-}
-
-void ent_tornado_debug(void) 
-{
-   world_instance *world = world_current_instance();
-   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
-      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
-
-      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
-         v3f p1;
-         v3_add( marker->transform.co, (v3f){0,20,0}, p1 );
-         vg_line( marker->transform.co, p1, VG__RED );
-
-         m4x3f mmdl;
-         m4x3_identity( mmdl );
-         v3_copy( marker->transform.co, mmdl[3] );
-         vg_line_sphere( mmdl, k_tornado_range, 0 );
-      }
-   }
-}
-
-void ent_tornado_forces( v3f co, v3f cv, v3f out_a )
-{
-   world_instance *world = world_current_instance();
-   v3_zero( out_a );
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
-      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
-
-      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
-         v3f d, dir;
-         v3_sub( co, marker->transform.co, d );
-         d[1] = 0.0f;
-
-         f32 dist = v3_length( d );
-         v3_normalize( d );
-
-         v3_cross( d, (v3f){0,1,0}, dir );
-         if( v3_dot( dir, cv ) < 0.0f )
-            v3_negate( dir, dir );
-
-         f32  s = vg_maxf(0.0f, 1.0f-dist/k_tornado_range),
-              F0 = s*k_tornado_strength,
-              F1 = s*s*k_tornado_strength;
-
-         v3_muladds( out_a, dir, F0 * k_tornado_ratio, out_a );
-         v3_muladds( out_a, d,   F1 * -(1.0f-k_tornado_ratio), out_a );
-      }
-   }
-}
-
-void ent_tornado_pre_update(void)
-{
-   world_instance *world = world_current_instance();
-   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
-      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
-
-      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
-         v3f co;
-         vg_rand_sphere( &vg.rand, co );
-
-         v3f tangent = { co[2], 0, co[0] };
-
-         f32 s = vg_signf( co[1] );
-         v3_muls( tangent, s*10.0f, tangent );
-         co[1] *= s;
-
-         v3_muladds( marker->transform.co, co, k_tornado_range, co );
-         particle_spawn( &particles_env, co, tangent, 2.0f, 0xffffffff );
-      }
-   }
-}
diff --git a/ent_tornado.h b/ent_tornado.h
deleted file mode 100644 (file)
index f89c070..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-#pragma once
-
-void ent_tornado_init(void);
-void ent_tornado_debug(void);
-void ent_tornado_forces( v3f co, v3f cv, v3f out_a );
-void ent_tornado_pre_update(void);
diff --git a/ent_traffic.c b/ent_traffic.c
deleted file mode 100644 (file)
index 8bb19b9..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-#include "world.h"
-
-void ent_traffic_update( world_instance *world, v3f pos )
-{
-   for( u32 i=0; i<mdl_arrcount( &world->ent_traffic ); i++ ){
-      ent_traffic *traffic = mdl_arritm( &world->ent_traffic, i );
-      
-      u32 i1 = traffic->index,
-          i0,
-          i2 = i1+1;
-
-      if( i1 == 0 ) i0 = traffic->node_count-1;
-      else i0 = i1-1;
-
-      if( i2 >= traffic->node_count ) i2 = 0;
-
-      i0 += traffic->start_node;
-      i1 += traffic->start_node;
-      i2 += traffic->start_node;
-      
-      v3f h[3];
-
-      ent_route_node *rn0 = mdl_arritm( &world->ent_route_node, i0 ),
-                     *rn1 = mdl_arritm( &world->ent_route_node, i1 ),
-                     *rn2 = mdl_arritm( &world->ent_route_node, i2 );
-
-      v3_copy( rn1->co, h[1] );
-      v3_lerp( rn0->co, rn1->co, 0.5f, h[0] );
-      v3_lerp( rn1->co, rn2->co, 0.5f, h[2] );
-
-      float const k_sample_dist = 0.0025f;
-      v3f pc, pd;
-      eval_bezier3( h[0], h[1], h[2], traffic->t, pc );
-      eval_bezier3( h[0], h[1], h[2], traffic->t+k_sample_dist, pd );
-
-      v3f v0;
-      v3_sub( pd, pc, v0 );
-      float length = vg_maxf( 0.0001f, v3_length( v0 ) );
-      v3_muls( v0, 1.0f/length, v0 );
-
-      float mod = k_sample_dist / length;
-
-      traffic->t += traffic->speed * vg.time_delta * mod;
-
-      if( traffic->t > 1.0f ){
-         traffic->t -= 1.0f;
-
-         if( traffic->t > 1.0f ) traffic->t = 0.0f;
-
-         traffic->index ++;
-
-         if( traffic->index >= traffic->node_count ) 
-            traffic->index = 0;
-      }
-
-      v3_copy( pc, traffic->transform.co );
-
-      float a = atan2f( -v0[0], v0[2] );
-      q_axis_angle( traffic->transform.q, (v3f){0.0f,1.0f,0.0f}, -a );
-
-      vg_line_point( traffic->transform.co, 0.3f, VG__BLUE );
-   }
-}
diff --git a/ent_traffic.h b/ent_traffic.h
deleted file mode 100644 (file)
index 18d8d1e..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-#include "world.h"
-void ent_traffic_update( world_instance *world, v3f pos );
diff --git a/entity.c b/entity.c
deleted file mode 100644 (file)
index 797e29d..0000000
--- a/entity.c
+++ /dev/null
@@ -1,77 +0,0 @@
-#include "world.h"
-#include "entity.h"
-#include "world_entity.h"
-
-#include "ent_objective.h"
-#include "ent_skateshop.h"
-#include "ent_relay.h"
-#include "ent_challenge.h"
-#include "ent_route.h"
-#include "ent_miniworld.h"
-#include "ent_region.h"
-#include "ent_glider.h"
-#include "ent_npc.h"
-#include "world_water.h"
-
-#include <string.h>
-
-void entity_call( world_instance *world, ent_call *call )
-{
-   u32 type = mdl_entity_id_type( call->id ),
-       index = mdl_entity_id_id( call->id );
-
-   fn_entity_call_handler table[] = {
-      [k_ent_volume]    = ent_volume_call,
-      [k_ent_audio]     = ent_audio_call,
-      [k_ent_skateshop] = ent_skateshop_call,
-      [k_ent_objective] = ent_objective_call,
-      [k_ent_ccmd]      = ent_ccmd_call,
-      [k_ent_gate]      = ent_gate_call,
-      [k_ent_relay]     = ent_relay_call,
-      [k_ent_challenge] = ent_challenge_call,
-      [k_ent_route]     = ent_route_call,
-      [k_ent_miniworld] = ent_miniworld_call,
-      [k_ent_region]    = ent_region_call,
-      [k_ent_glider]    = ent_glider_call,
-      [k_ent_npc]       = ent_npc_call,
-      [k_ent_water]     = ent_water_call,
-   };
-
-   if( type >= VG_ARRAY_LEN(table) ){
-      vg_error( "call to entity type: %u is out of range\n", type );
-      return;
-   }
-
-   fn_entity_call_handler fn = table[ type ];
-
-   if( !fn )
-   {
-      vg_error( "Entity type %u does not have a call handler, "
-                "but was called anyway\n", type );
-      return;
-   }
-
-   enum entity_call_result res = fn( world, call );
-
-   if( res == k_entity_call_result_unhandled )
-   {
-      vg_warn( "Call to entity %u#%u was unhandled.\n", type, index );
-   }
-}
-
-ent_marker *ent_find_marker( mdl_context *mdl, mdl_array_ptr *arr, 
-                             const char *alias )
-{
-   for( u32 i=0; i<mdl_arrcount(arr); i++ )
-   {
-      ent_marker *marker = mdl_arritm( arr, i );
-
-      if( !strcmp( mdl_pstr( mdl, marker->pstr_alias ), alias ) )
-      {
-         return marker;
-      }
-   }
-
-   return NULL;
-}
-
diff --git a/entity.h b/entity.h
deleted file mode 100644 (file)
index 6d503f9..0000000
--- a/entity.h
+++ /dev/null
@@ -1,582 +0,0 @@
-#pragma once
-
-#include "vg/vg_audio.h"
-#include "vg/vg_ui/imgui.h"
-#include "model.h"
-
-typedef struct ent_spawn ent_spawn;
-typedef struct ent_light ent_light;
-typedef struct ent_gate ent_gate;
-typedef struct ent_route_node ent_route_node;
-typedef struct ent_path_index ent_path_index;
-typedef struct ent_checkpoint ent_checkpoint;
-typedef struct ent_route ent_route;
-typedef struct ent_water ent_water;
-typedef struct ent_audio_clip ent_audio_clip;
-typedef struct volume_particles volume_particles;
-typedef struct volume_trigger volume_trigger;
-typedef struct ent_volume ent_volume;
-typedef struct ent_audio ent_audio;
-typedef struct ent_marker ent_marker;
-typedef struct ent_traffic ent_traffic;
-typedef struct ent_font ent_font;
-typedef struct ent_font_variant ent_font_variant;
-typedef struct ent_glyph ent_glyph;
-typedef struct ent_skateshop ent_skateshop;
-typedef struct ent_camera ent_camera;
-typedef struct ent_swspreview ent_swspreview;
-typedef struct ent_worldinfo ent_worldinfo;
-typedef struct ent_ccmd ent_ccmd;
-typedef struct ent_objective ent_objective;
-typedef struct ent_challenge ent_challenge;
-typedef struct ent_relay ent_relay;
-typedef struct ent_cubemap ent_cubemap;
-typedef struct ent_miniworld ent_miniworld;
-typedef struct ent_prop ent_prop;
-typedef struct ent_region ent_region;
-typedef struct ent_list ent_list;
-typedef struct ent_glider ent_glider;
-typedef struct ent_npc ent_npc;
-
-enum entity_alias{
-   k_ent_none        = 0,
-   k_ent_gate        = 1,
-   k_ent_spawn       = 2,
-   k_ent_route_node  = 3,
-   k_ent_route       = 4,
-   k_ent_water       = 5,
-   k_ent_volume      = 6,
-   k_ent_audio       = 7,
-   k_ent_marker      = 8,
-   k_ent_font        = 9,
-   k_ent_font_variant= 10,
-   k_ent_traffic     = 11,
-   k_ent_skateshop   = 12,
-   k_ent_camera      = 13,
-   k_ent_swspreview  = 14,
-   k_ent_menuitem    = 15,
-   k_ent_worldinfo   = 16,
-   k_ent_ccmd        = 17,
-   k_ent_objective   = 18,
-   k_ent_challenge   = 19,
-   k_ent_relay       = 20,
-   k_ent_cubemap     = 21,
-   k_ent_miniworld   = 22,
-   k_ent_prop        = 23,
-   k_ent_list        = 24,
-   k_ent_region      = 25,
-   k_ent_glider      = 26,
-   k_ent_npc         = 27
-};
-
-typedef struct ent_call ent_call;
-typedef enum entity_call_result entity_call_result;
-enum entity_call_result 
-{
-   k_entity_call_result_OK,
-   k_entity_call_result_unhandled,
-   k_entity_call_result_invalid
-};
-
-static inline u32 mdl_entity_id_type( u32 entity_id )
-{
-   return (entity_id & 0x0fff0000) >> 16;
-}
-
-static inline u32 mdl_entity_id_id( u32 entity_id )
-{
-   return entity_id & 0x0000ffff;
-}
-
-static inline u32 mdl_entity_id( u32 type, u32 index )
-{
-   return (type & 0xfffff)<<16 | (index & 0xfffff);
-}
-
-enum entity_function
-{
-   k_ent_function_trigger,
-   k_ent_function_particle_spawn,
-   k_ent_function_trigger_leave
-};
-
-struct ent_spawn{
-   mdl_transform transform;
-   u32 pstr_name;
-};
-
-enum light_type{
-   k_light_type_point = 0,
-   k_light_type_spot = 1
-};
-
-struct ent_light{
-   mdl_transform transform;
-   u32 daytime,
-       type;
-
-   v4f colour;
-   float angle,
-         range;
-
-   m4x3f inverse_world;
-   v2f angle_sin_cos;
-};
-
-/* v101 */
-#if 0
-enum gate_type{
-   k_gate_type_unlinked = 0,
-   k_gate_type_teleport = 1,
-   k_gate_type_nonlocal_unlinked = 2,
-   k_gate_type_nonlocel = 3
-};
-#endif
-
-/* v102+ */
-enum ent_gate_flag{
-   k_ent_gate_linked      = 0x1, /* this is a working portal */
-   k_ent_gate_nonlocal    = 0x2, /* use the key string to link this portal.
-                                       NOTE: if set, it adds the flip flag. */
-   k_ent_gate_flip        = 0x4, /* flip direction 180* for exiting portal */
-   k_ent_gate_custom_mesh = 0x8, /* use a custom submesh instead of default */
-   k_ent_gate_locked      = 0x10,/* has to be unlocked to be useful */
-
-   k_ent_gate_clean_pass  = 0x20,/* player didn't rewind while getting here */
-};
-
-struct ent_gate{
-   u32 flags,
-       target, 
-       key;
-
-   v3f dimensions,
-       co[2];
-
-   v4f q[2];
-
-   /* runtime */
-   m4x3f to_world, transport;
-
-   union{
-      u32 timing_version;
-
-      struct{
-         u8 ref_count;
-      };
-   };
-
-   double timing_time;
-   u16 routes[4];       /* routes that pass through this gate */
-   u8 route_count;
-
-   /* v102+ */
-   u32 submesh_start, submesh_count;
-};
-
-struct ent_route_node{
-   v3f co;
-   u8 ref_count, ref_total;
-};
-
-struct ent_path_index{
-   u16 index;
-};
-
-struct ent_checkpoint{
-   u16 gate_index,
-       path_start,
-       path_count;
-
-   /* EXTENSION */
-   f32 best_time;
-};
-
-enum ent_route_flag {
-   k_ent_route_flag_achieve_silver = 0x1,
-   k_ent_route_flag_achieve_gold   = 0x2,
-
-   k_ent_route_flag_out_of_zone    = 0x10,
-   k_ent_region_flag_hasname       = 0x20
-};
-
-struct ent_route{
-   union{
-      mdl_transform transform;
-      u32 official_track_id;   /* TODO: remove this */
-   }
-   anon;
-
-   u32 pstr_name;
-   u16 checkpoints_start,
-       checkpoints_count;
-
-   v4f colour;
-
-   /* runtime */
-   u16 active_checkpoint, 
-       valid_checkpoints;
-
-   f32 factive;
-   m4x3f board_transform;
-   mdl_submesh sm;
-   f64 timing_base;
-
-   u32 id_camera; /* v103+ */
-
-   /* v104+, but always accessible */
-   u32 flags;
-   f64 best_laptime;
-   f32 ui_stopper, ui_residual;
-
-   ui_px ui_first_block_width, ui_residual_block_w;
-};
-
-struct ent_water{
-   mdl_transform transform;
-   float max_dist;
-   u32 reserved0, reserved1;
-};
-
-struct ent_audio_clip{
-   union{
-      mdl_file file;
-      audio_clip clip;
-   }_;
-
-   float probability;
-};
-
-struct volume_particles{
-   u32 blank, blank2;
-};
-
-struct volume_trigger{
-   i32 event, event_leave;
-};
-
-enum ent_volume_flag {
-   k_ent_volume_flag_particles = 0x1,
-   k_ent_volume_flag_disabled  = 0x2
-};
-
-struct ent_volume{
-   mdl_transform transform;
-   m4x3f to_world, to_local;
-   u32 flags;
-
-   u32 target;
-   union{
-      volume_trigger trigger;
-      volume_particles particles;
-   };
-};
-
-struct ent_audio{
-   mdl_transform transform;
-   u32 flags,
-       clip_start,
-       clip_count;
-   float volume, crossfade;
-   u32 behaviour,
-       group,
-       probability_curve,
-       max_channels;
-};
-
-struct ent_marker{
-   mdl_transform transform;
-   u32 pstr_alias;
-};
-
-enum skateshop_type{
-   k_skateshop_type_boardshop = 0,
-   k_skateshop_type_charshop = 1,
-   k_skateshop_type_worldshop = 2,
-   k_skateshop_type_DELETED = 3,
-   k_skateshop_type_server = 4
-};
-
-struct ent_skateshop{
-   mdl_transform transform;
-   u32 type, id_camera;
-
-   union{
-      struct{
-         u32 id_display,
-             id_info,
-             id_rack;
-      }
-      boards;
-
-      struct{
-         u32 id_display,
-             id_info;
-      }
-      character;
-
-      struct{
-         u32 id_display,
-             id_info;
-      }
-      worlds;
-
-      struct{
-         u32 id_lever;
-      }
-      server;
-   };
-};
-
-struct ent_swspreview{
-   u32 id_camera, id_display, id_display1;
-};
-
-struct ent_traffic{
-   mdl_transform transform;
-   u32 submesh_start,
-       submesh_count,
-       start_node,
-       node_count;
-   float speed,
-         t;
-   u32 index;     /* into the path */
-};
-
-struct ent_camera{
-   mdl_transform transform;
-   float fov;
-};
-
-enum ent_menuitem_type{
-   k_ent_menuitem_type_visual       = 0,
-   k_ent_menuitem_type_event_button = 1,
-   k_ent_menuitem_type_page_button  = 2,
-   k_ent_menuitem_type_toggle       = 3,
-   k_ent_menuitem_type_slider       = 4,
-   k_ent_menuitem_type_page         = 5,
-   k_ent_menuitem_type_binding      = 6,
-   k_ent_menuitem_type_visual_nocol = 7,
-   k_ent_menuitem_type_disabled     = 90
-};
-
-enum ent_menuitem_stack_behaviour{
-   k_ent_menuitem_stack_append  = 0,
-   k_ent_menuitem_stack_replace = 1
-};
-
-typedef struct ent_menuitem ent_menuitem;
-struct ent_menuitem{
-   u32 type, groups, 
-       id_links[4];  /* ent_menuitem */
-   f32 factive, fvisible;
-
-   mdl_transform transform;
-   u32 submesh_start, submesh_count;
-
-   union{ u64 _u64;  /* force storage for 64bit pointers */
-          i32 *pi32;
-          f32 *pf32;
-          void *pvoid;
-   };
-
-   union{
-      struct{
-         u32 pstr_name;
-      }
-      visual;
-
-      struct{
-         u32 id_min,    /* ent_marker */
-             id_max,    /* . */
-             id_handle, /* ent_menuitem */
-             pstr_data;
-      }
-      slider;
-
-      struct{
-         u32 pstr,
-             stack_behaviour;
-      }
-      button;
-
-      struct{
-         u32 id_check, /* ent_menuitem */
-             pstr_data;
-         v3f offset; /* relative to parent */
-      }
-      checkmark;
-
-      struct{
-         u32 pstr_name, 
-             id_entrypoint,  /* ent_menuitem */
-             id_viewpoint;   /* ent_camera */
-      }
-      page;
-
-      struct{
-         u32 pstr_bind,
-             font_variant;
-      }
-      binding;
-   };
-};
-
-struct ent_worldinfo{
-   u32 pstr_name, pstr_author, pstr_desc;
-   f32 timezone;
-   u32 pstr_skybox;
-   u32 flags;
-};
-
-ent_marker *ent_find_marker( mdl_context *mdl, mdl_array_ptr *arr, 
-                             const char *alias );
-
-enum channel_behaviour{
-   k_channel_behaviour_unlimited = 0,
-   k_channel_behaviour_discard_if_full = 1,
-   k_channel_behaviour_crossfade_if_full = 2
-};
-
-enum probability_curve{
-   k_probability_curve_constant = 0,
-   k_probability_curve_wildlife_day = 1,
-   k_probability_curve_wildlife_night = 2
-};
-
-struct ent_font{
-   u32 alias,
-       variant_start,
-       variant_count,
-       glyph_start,
-       glyph_count,
-       glyph_utf32_base;
-};
-
-struct ent_font_variant{
-   u32 name,
-       material_id;
-};
-
-struct ent_glyph{
-   v2f size;
-   u32 indice_start,
-       indice_count;
-};
-
-struct ent_ccmd{
-   u32 pstr_command;
-};
-
-enum ent_objective_filter{
-   k_ent_objective_filter_none            = 0x00000000,
-   k_ent_objective_filter_trick_shuvit    = 0x00000001,
-   k_ent_objective_filter_trick_kickflip  = 0x00000002,
-   k_ent_objective_filter_trick_treflip   = 0x00000004,
-   k_ent_objective_filter_trick_any       = 
-      k_ent_objective_filter_trick_shuvit|
-      k_ent_objective_filter_trick_treflip|
-      k_ent_objective_filter_trick_kickflip,
-   k_ent_objective_filter_flip_back       = 0x00000008,
-   k_ent_objective_filter_flip_front      = 0x00000010,
-   k_ent_objective_filter_flip_any        =
-      k_ent_objective_filter_flip_back|
-      k_ent_objective_filter_flip_front,
-   k_ent_objective_filter_grind_truck_any = 0x00000020,
-   k_ent_objective_filter_grind_board_any = 0x00000040,
-   k_ent_objective_filter_grind_any       =
-      k_ent_objective_filter_grind_truck_any|
-      k_ent_objective_filter_grind_board_any,
-   k_ent_objective_filter_footplant       = 0x00000080,
-   k_ent_objective_filter_passthrough     = 0x00000100
-};
-
-enum ent_objective_flag {
-   k_ent_objective_hidden = 0x1,
-   k_ent_objective_passed = 0x2
-};
-
-struct ent_objective{
-   mdl_transform transform;
-   u32 submesh_start,
-       submesh_count,
-       flags,
-       id_next,
-       filter,filter2,
-       id_win;
-   i32 win_event;
-   f32 time_limit;
-};
-
-enum ent_challenge_flag {
-   k_ent_challenge_timelimit = 0x1
-};
-
-struct ent_challenge{
-   mdl_transform transform;
-   u32 pstr_alias,
-       flags,
-       target;
-   i32 target_event;
-   u32 reset;
-   i32 reset_event;
-   u32 first,
-       camera,
-       status;
-};
-
-struct ent_relay {
-   u32 targets[4][2];
-   i32 targets_events[4];
-};
-
-struct ent_cubemap {
-   v3f co;
-   u32 resolution, live, texture_id, 
-       framebuffer_id, renderbuffer_id, placeholder[2];
-};
-
-struct ent_miniworld {
-   mdl_transform transform;
-   u32 pstr_world;
-   u32 camera;
-   u32 proxy;
-};
-
-struct ent_prop {
-   mdl_transform transform;
-   u32 submesh_start, submesh_count, flags, pstr_alias;
-};
-
-struct ent_region {
-   mdl_transform transform;
-   u32 submesh_start, submesh_count, pstr_title, flags, zone_volume,
-
-       /* 105+ */
-       target0[2];
-};
-
-struct ent_glider {
-   mdl_transform transform;
-   u32 flags;
-   f32 cooldown;
-};
-
-struct ent_npc 
-{
-   mdl_transform transform;
-   u32 id, context, camera;
-};
-
-#include "world.h"
-
-struct ent_call{
-   u32 id;
-   i32 function;
-   void *data;
-};
-
-typedef enum entity_call_result 
-   (*fn_entity_call_handler)( world_instance *, ent_call *);
-
-void entity_call( world_instance *world, ent_call *call );
diff --git a/font.h b/font.h
deleted file mode 100644 (file)
index fa144c4..0000000
--- a/font.h
+++ /dev/null
@@ -1,324 +0,0 @@
-#pragma once
-#include "model.h"
-#include "entity.h"
-#include "vg/vg_camera.h"
-#include "shaders/model_font.h"
-#include "shaders/scene_font.h"
-#include "world_render.h"
-#include "depth_compare.h"
-#include "vg/vg_tex.h"
-#include <string.h>
-
-enum efont_SRglyph{
-   k_SRglyph_end           = 0x00, /* control characters */
-   k_SRglyph_ctrl_variant  = 0x01,
-   k_SRglyph_ctrl_size     = 0x02, /* normalized 0-1 */
-   k_SRglyph_ctrl_center   = 0x03, /* useful when text is scaled down */
-   k_SRglyph_ctrl_baseline = 0x04, /* . */
-   k_SRglyph_ctrl_top      = 0x05, /* . */
-   k_SRglyph_mod_circle    = 0x1e, /* surround and center next charater */
-   k_SRglyph_mod_square    = 0x1f, /* surround and center next character */
-   k_SRglyph_ascii_min     = 0x20, /* standard ascii */
-   k_SRglyph_ascii_max     = 0x7e,
-   k_SRglyph_ps4_square    = 0x7f,/* playstation buttons */
-   k_SRglyph_ps4_triangle  = 0x80,
-   k_SRglyph_ps4_circle    = 0x81,
-   k_SRglyph_ps4_cross     = 0x82,
-   k_SRglyph_xb1_x         = 0x83,/* xbox buttons */
-   k_SRglyph_xb1_y         = 0x84,
-   k_SRglyph_xb1_a         = 0x85,
-   k_SRglyph_xb1_b         = 0x86,
-   k_SRglyph_gen_ls        = 0x87,/* generic gamepad */
-   k_SRglyph_gen_lsh       = 0x88,
-   k_SRglyph_gen_lsv       = 0x89,
-   k_SRglyph_gen_lshv      = 0x8a,
-   k_SRglyph_gen_rs        = 0x8b,
-   k_SRglyph_gen_rsh       = 0x8c,
-   k_SRglyph_gen_rsv       = 0x8d,
-   k_SRglyph_gen_rshv      = 0x8e,
-   k_SRglyph_gen_lt        = 0x8f,
-   k_SRglyph_gen_rt        = 0x90,
-   k_SRglyph_gen_lb        = 0x91,
-   k_SRglyph_gen_rb        = 0x92,
-   k_SRglyph_gen_left      = 0x93,
-   k_SRglyph_gen_up        = 0x94,
-   k_SRglyph_gen_right     = 0x95,
-   k_SRglyph_gen_down      = 0x96,
-   k_SRglyph_gen_options   = 0x97,
-   k_SRglyph_gen_shareview = 0x98,
-   k_SRglyph_kbm_m0        = 0x99,/* mouse */
-   k_SRglyph_kbm_m1        = 0x9a,
-   k_SRglyph_kbm_m01       = 0x9b,
-   k_SRglyph_kbm_m2        = 0x9c,
-   k_SRglyph_kbm_m2s       = 0x9d,
-   k_SRglyph_kbm_shift     = 0x9e,/* modifiers */
-   k_SRglyph_kbm_ctrl      = 0x9f,
-   k_SRglyph_kbm_alt       = 0xa0,
-   k_SRglyph_kbm_space     = 0xa1,
-   k_SRglyph_kbm_return    = 0xa2,
-   k_SRglyph_kbm_escape    = 0xa3,
-   k_SRglyph_kbm_mousemove = 0xa4,
-
-#if 0
-   k_SRglyph_vg_ret        = 0xa5,
-   k_SRglyph_vg_link       = 0xa6,
-   k_SRglyph_vg_square     = 0xa7,
-   k_SRglyph_vg_triangle   = 0xa8,
-   k_SRglyph_vg_circle     = 0xa9
-#endif
-};
-
-typedef struct font3d font3d;
-struct font3d{
-   mdl_context mdl;
-   GLuint texture;
-   glmesh mesh;
-
-   ent_font info;
-   mdl_array_ptr font_variants,
-                 glyphs;
-};
-
-static void font3d_load( font3d *font, const char *mdl_path, void *alloc ){
-   mdl_open( &font->mdl, mdl_path, alloc );
-   mdl_load_metadata_block( &font->mdl, alloc );
-
-   vg_linear_clear( vg_mem.scratch );
-   mdl_array_ptr fonts;
-   MDL_LOAD_ARRAY( &font->mdl, &fonts, ent_font, vg_mem.scratch );
-   font->info = *((ent_font *)mdl_arritm(&fonts,0));
-
-   MDL_LOAD_ARRAY( &font->mdl, &font->font_variants, ent_font_variant, alloc);
-   MDL_LOAD_ARRAY( &font->mdl, &font->glyphs, ent_glyph, alloc );
-
-   vg_linear_clear( vg_mem.scratch );
-
-   if( !mdl_arrcount( &font->mdl.textures ) )
-      vg_fatal_error( "No texture in font file" );
-
-   mdl_texture *tex0 = mdl_arritm( &font->mdl.textures, 0 );
-   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
-   mdl_fread_pack_file( &font->mdl, &tex0->file, data );
-
-   mdl_async_load_glmesh( &font->mdl, &font->mesh, NULL );
-   vg_tex2d_load_qoi_async( data, tex0->file.pack_size, 
-                            VG_TEX2D_LINEAR|VG_TEX2D_CLAMP,
-                            &font->texture );
-
-   mdl_close( &font->mdl );
-}
-
-static u32 font3d_find_variant( font3d *font, const char *name ){
-   for( u32 i=0; i<mdl_arrcount( &font->font_variants ); i ++ ){
-      ent_font_variant *variant = mdl_arritm( &font->font_variants, i );
-
-      if( !strcmp( mdl_pstr( &font->mdl, variant->name ), name ) ){
-         return i;
-      }
-   }
-
-   return 0;
-}
-
-struct _font3d_render{
-   v4f offset;
-   font3d *font;
-   u32 variant_id;
-
-   enum font_shader {
-      k_font_shader_default,
-      k_font_shader_world
-   }
-   shader;
-}
-static gui_font3d;
-
-/*
- * world can be null if not using world shader 
- */
-static void font3d_bind( font3d *font, enum font_shader shader, 
-                         int depth_compare, world_instance *world,
-                         vg_camera *cam ){
-   gui_font3d.shader = shader;
-   gui_font3d.font = font;
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, font->texture );
-
-   if( shader == k_font_shader_default )
-   {
-      shader_model_font_use();
-      shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
-      shader_model_font_uTexMain( 1 );
-      shader_model_font_uDepthMode( depth_compare );
-
-      if( depth_compare ){
-         depth_compare_bind( 
-            shader_model_font_uTexSceneDepth,
-            shader_model_font_uInverseRatioDepth,
-            shader_model_font_uInverseRatioMain, cam );
-      }
-
-      shader_model_font_uPv( cam->mtx.pv );
-   }
-   else if( shader == k_font_shader_world )
-   {
-      shader_scene_font_use();
-      shader_scene_font_uTexGarbage(0);
-      shader_scene_font_uTexMain(1);
-
-      shader_scene_font_uPv( g_render.cam.mtx.pv );
-      shader_scene_font_uTime( vg.time );
-
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_font );
-
-      bind_terrain_noise();
-      shader_scene_font_uCamera( g_render.cam.transform[3] );
-   }
-   mesh_bind( &font->mesh );
-}
-
-static ent_glyph *font3d_glyph( font3d *font, u32 variant_id, u32 utf32 ){
-   if( utf32 < font->info.glyph_utf32_base ) return NULL;
-   if( utf32 >= font->info.glyph_utf32_base+font->info.glyph_count) return NULL;
-
-   u32 index = utf32 - font->info.glyph_utf32_base;
-       index += font->info.glyph_start;
-       index += font->info.glyph_count * variant_id;
-   return mdl_arritm( &font->glyphs, index );
-}
-
-static void font3d_set_transform( const char *text,
-                                  vg_camera *cam, m4x3f transform ){
-   v4_copy( (v4f){0.0f,0.0f,0.0f,1.0f}, gui_font3d.offset );
-
-   m4x4f prev_mtx;
-   m4x3_expand( transform, prev_mtx );
-   m4x4_mul( cam->mtx_prev.pv, prev_mtx, prev_mtx );
-
-   if( gui_font3d.shader == k_font_shader_default ){
-      shader_model_font_uPvmPrev( prev_mtx );
-      shader_model_font_uMdl( transform );
-   }
-   else if( gui_font3d.shader == k_font_shader_world ){
-      shader_scene_font_uPvmPrev( prev_mtx );
-      shader_scene_font_uMdl( transform );
-   }
-}
-
-static void font3d_setoffset( v4f offset ){
-   if( gui_font3d.shader == k_font_shader_default )
-      shader_model_font_uOffset( offset );
-   else if( gui_font3d.shader == k_font_shader_world )
-      shader_scene_font_uOffset( offset );
-}
-
-static void font3d_setcolour( v4f colour ){
-   if( gui_font3d.shader == k_font_shader_default )
-      shader_model_font_uColour( colour );
-#if 0
-   else if( gui_font3d.shader == k_font_shader_world )
-      shader_scene_font_uColour( colour );
-#endif
-}
-
-static void font3d_draw( const char *text ){
-   u8 *u8pch = (u8*)text;
-
-   u32 max_chars = 512;
-   while( u8pch && max_chars ){
-      max_chars --;
-
-      u32 c0 = *u8pch, c1;
-      u8pch ++;
-
-      if( !c0 ) break;
-
-      ent_glyph *glyph0 = font3d_glyph( gui_font3d.font, 
-                                        gui_font3d.variant_id, c0 ),
-                *glyph1 = NULL;
-
-      /* multibyte characters */
-      if( c0 >= 1 && c0 < k_SRglyph_ascii_min ){
-         c1 = *u8pch;
-         if( !c1 ) break;
-         glyph1 = font3d_glyph( gui_font3d.font, gui_font3d.variant_id, c1 );
-      }
-
-      if( c0 == k_SRglyph_ctrl_variant ){
-         gui_font3d.variant_id = c1;
-         u8pch ++;
-         continue;
-      }
-      else if( c0 == k_SRglyph_ctrl_size ){
-         gui_font3d.offset[3] = (float)c1 * (1.0f/255.0f);
-         u8pch ++;
-         continue;
-      }
-      else if( c0 == k_SRglyph_ctrl_baseline ){
-         gui_font3d.offset[1] = 0.0f;
-         continue;
-      }
-      else if( c0 == k_SRglyph_ctrl_center ){
-         if( glyph1 ){
-            float diff = glyph1->size[1] - glyph1->size[1]*gui_font3d.offset[3];
-            gui_font3d.offset[1] = diff * 0.5f;
-         }
-         continue;
-      }
-      else if( c0 == k_SRglyph_ctrl_top ){
-         if( glyph1 ){
-            float diff = glyph1->size[1] - glyph1->size[1]*gui_font3d.offset[3];
-            gui_font3d.offset[1] = diff;
-         }
-         continue;
-      }
-
-      if( !glyph0 ) continue;
-
-      if( glyph1 && (c0 == k_SRglyph_mod_square || c0 == k_SRglyph_mod_circle)){
-         v4f v0;
-         v2_sub( glyph0->size, glyph1->size, v0 );
-         v2_muladds( gui_font3d.offset, v0, -0.5f, v0 );
-         v0[2] = gui_font3d.offset[2];
-         v0[3] = gui_font3d.offset[3];
-
-         font3d_setoffset( v0 );
-         mesh_drawn( glyph0->indice_start, glyph0->indice_count );
-         continue;
-      }
-      else{
-         font3d_setoffset( gui_font3d.offset );
-         mesh_drawn( glyph0->indice_start, glyph0->indice_count );
-      }
-
-      gui_font3d.offset[0] += glyph0->size[0]*gui_font3d.offset[3];
-   }
-}
-
-static f32 font3d_simple_draw( u32 variant_id, const char *text, 
-                               vg_camera *cam, m4x3f transform ){
-   if( !text ) return 0.0f;
-
-   gui_font3d.variant_id = variant_id;
-   font3d_set_transform( text, cam, transform );
-   font3d_draw( text );
-   return gui_font3d.offset[0];
-}
-
-static f32 font3d_string_width( u32 variant_id, const char *text ){
-   if( !text ) return 0.0f;
-   float width = 0.0f;
-
-   const u8 *buf = (const u8 *)text;
-   for( int i=0;; i++ ){
-      u32 c = buf[i];
-      if(!c) break;
-
-      ent_glyph *glyph = font3d_glyph( gui_font3d.font, variant_id, c );
-      if( !glyph ) continue;
-
-      width += glyph->size[0];
-   }
-
-   return width;
-}
diff --git a/freecam.c b/freecam.c
deleted file mode 100644 (file)
index d961ed6..0000000
--- a/freecam.c
+++ /dev/null
@@ -1,44 +0,0 @@
-#include "skaterift.h"
-#include "player.h"
-#include "player_render.h"
-#include "player_replay.h"
-#include "input.h"
-
-void freecam_preupdate(void)
-{
-   vg_camera *cam = &player_replay.replay_freecam;
-   v3f angles;
-   v3_copy( cam->angles, angles );
-   player_look( angles, 1.0f );
-
-   f32 decay = vg_maxf(0.0f,1.0f-vg.time_frame_delta*10.0f);
-
-   v3f d;
-   v3_sub( angles, cam->angles, d );
-   v3_muladds( player_replay.freecam_w, d, 20.0f, player_replay.freecam_w );
-   v3_muls( player_replay.freecam_w, decay, player_replay.freecam_w );
-   v3_muladds( cam->angles, player_replay.freecam_w, vg.time_frame_delta,
-               cam->angles );
-   cam->angles[1] = vg_clampf( cam->angles[1], -VG_PIf*0.5f,VG_PIf*0.5f);
-
-   vg_camera_update_transform( cam );
-
-   v3f lookdir = { 0.0f, 0.0f, -1.0f },
-       sidedir = { 1.0f, 0.0f,  0.0f };
-   
-   m3x3_mulv( cam->transform, lookdir, lookdir );
-   m3x3_mulv( cam->transform, sidedir, sidedir );
-
-   v2f input;
-   joystick_state( k_srjoystick_steer, input );
-   v2_muls( input, vg.time_frame_delta*6.0f*20.0f, input );
-   
-   v3_muladds( player_replay.freecam_v, lookdir, -input[1], 
-               player_replay.freecam_v );
-   v3_muladds( player_replay.freecam_v, sidedir, input[0], 
-               player_replay.freecam_v );
-
-   v3_muls( player_replay.freecam_v, decay, player_replay.freecam_v );
-   v3_muladds( cam->pos,
-               player_replay.freecam_v, vg.time_frame_delta, cam->pos );
-}
diff --git a/freecam.h b/freecam.h
deleted file mode 100644 (file)
index 1f9f5e2..0000000
--- a/freecam.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-void freecam_preupdate(void);
-int freecam_cmd( int argc, const char *argv[] );
diff --git a/gameserver.c b/gameserver.c
deleted file mode 100644 (file)
index e315fd0..0000000
+++ /dev/null
@@ -1,1136 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#define _DEFAULT_SOURCE
-#include <signal.h>
-#include <unistd.h>
-#include <time.h>
-#include <string.h>
-
-volatile sig_atomic_t sig_stop;
-
-#include "gameserver.h" 
-#include "vg/vg_opt.h"
-#include "network_common.h"
-#include "gameserver_db.h"
-#include "vg/vg_m.h"
-#include "vg/vg_msg.h"
-
-static u64 const k_steamid_max = 0xffffffffffffffff;
-
-static void inthandler( int signum ) {
-   sig_stop = 1;
-}
-
-static void release_message( SteamNetworkingMessage_t *msg )
-{
-   msg->m_nUserData --;
-
-   if( msg->m_nUserData == 0 )
-      SteamAPI_SteamNetworkingMessage_t_Release( msg );
-}
-
-/*
- * Send message to single client, with authentication checking
- */
-static void gameserver_send_to_client( i32 client_id,
-                                       const void *pData, u32 cbData,
-                                       int nSendFlags )
-{
-   struct gameserver_client *client = &gameserver.clients[ client_id ];
-
-   if( gameserver.loopback_test && !client->connection ) 
-      return;
-
-   if( !client->steamid )
-      return;
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, client->connection,
-         pData, cbData, nSendFlags, NULL );
-}
-
-/*
- * Send message to all clients if they are authenticated
- */
-static void gameserver_send_to_all( int ignore, 
-                                    const void *pData, u32 cbData, 
-                                    int nSendFlags )
-{
-   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
-   {
-      struct gameserver_client *client = &gameserver.clients[i];
-
-      if( i != ignore )
-         gameserver_send_to_client( i, pData, cbData, nSendFlags );
-   }
-}
-
-static void gameserver_send_version_to_client( int index )
-{
-   struct gameserver_client *client = &gameserver.clients[index];
-
-   if( gameserver.loopback_test && !client->connection ) 
-      return;
-
-   netmsg_version version;
-   version.inetmsg_id = k_inetmsg_version;
-   version.version = NETWORK_SKATERIFT_VERSION;
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, client->connection,
-         &version, sizeof(netmsg_version), 
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-/*
- * handle server update that client #'index' has joined
- */
-static void gameserver_player_join( int index )
-{
-   struct gameserver_client *joiner = &gameserver.clients[index];
-   
-   netmsg_playerjoin join = { .inetmsg_id = k_inetmsg_playerjoin,
-                              .index = index,
-                              .steamid = joiner->steamid };
-
-   gameserver_send_to_all( index, &join, sizeof(join),
-                           k_nSteamNetworkingSend_Reliable );
-
-   /* 
-    * update the joining user about current connections and our version
-    */
-   gameserver_send_version_to_client( index );
-
-   netmsg_playerusername *username = 
-      alloca( sizeof(netmsg_playerusername) + NETWORK_USERNAME_MAX );
-   username->inetmsg_id = k_inetmsg_playerusername;
-
-   netmsg_playeritem *item = 
-      alloca( sizeof(netmsg_playeritem) + ADDON_UID_MAX );
-   item->inetmsg_id = k_inetmsg_playeritem;
-
-   netmsg_region *region = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
-   region->inetmsg_id = k_inetmsg_region;
-
-   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
-   {
-      struct gameserver_client *client = &gameserver.clients[i];
-
-      if( (i == index) || !client->steamid )
-         continue;
-
-      /* join */
-      netmsg_playerjoin init = { .inetmsg_id = k_inetmsg_playerjoin,
-                                 .index = i,
-                                 .steamid = client->steamid };
-      gameserver_send_to_client( index, &init, sizeof(init),
-                                 k_nSteamNetworkingSend_Reliable );
-
-      /* username */
-      username->index = i;
-      u32 chs = vg_strncpy( client->username, username->name, 
-                            NETWORK_USERNAME_MAX,
-                            k_strncpy_always_add_null );
-      u32 size = sizeof(netmsg_playerusername) + chs + 1;
-      gameserver_send_to_client( index, username, size,
-                                 k_nSteamNetworkingSend_Reliable );
-
-      /* items */
-      for( int j=0; j<k_netmsg_playeritem_max; j++ )
-      {
-         chs = vg_strncpy( client->items[j].uid, item->uid, ADDON_UID_MAX, 
-                           k_strncpy_always_add_null );
-         item->type_index = j;
-         item->client = i;
-         size = sizeof(netmsg_playeritem) + chs + 1;
-         gameserver_send_to_client( index, item, size,
-                                    k_nSteamNetworkingSend_Reliable );
-      }
-
-      /* region */
-      
-      region->client = i;
-      region->flags = client->region_flags;
-      u32 l = vg_strncpy( client->region, region->loc, NETWORK_REGION_MAX, 
-                          k_strncpy_always_add_null );
-      size = sizeof(netmsg_region) + l + 1;
-
-      gameserver_send_to_client( index, region, size,
-                                 k_nSteamNetworkingSend_Reliable );
-   }
-}
-
-/*
- * Handle server update that player has left
- */
-static void gameserver_player_leave( int index ){
-   if( gameserver.auth_mode == eServerModeAuthentication ){
-      if( !gameserver.clients[ index ].steamid )
-         return;
-   }
-
-   netmsg_playerleave leave;
-   leave.inetmsg_id = k_inetmsg_playerleave;
-   leave.index = index;
-
-   vg_info( "Player leave (%d)\n", index );
-   gameserver_send_to_all( index, &leave, sizeof(leave),
-                           k_nSteamNetworkingSend_Reliable );
-}
-
-static void gameserver_update_all_knowledge( int client, int clear );
-
-/*
- * Deletes client at index and disconnects the connection handle if it was 
- * set.
- */
-static void remove_client( int index ){
-   struct gameserver_client *client = &gameserver.clients[index];
-   if( client->connection ){
-      SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
-            hSteamNetworkingSockets, client->connection, -1 );
-      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-            hSteamNetworkingSockets, client->connection,
-            k_ESteamNetConnectionEnd_Misc_InternalError,
-            NULL, 1 );
-   }
-   memset( client, 0, sizeof(struct gameserver_client) );
-   gameserver_update_all_knowledge( index, 1 );
-}
-
-/*
- * Handle incoming new connection and init flags on the steam handle. if the 
- * server is full the userdata (client_id) will be set to -1 on the handle.
- */
-static void handle_new_connection( HSteamNetConnection conn )
-{
-   SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
-         hSteamNetworkingSockets, conn, -1 );
-
-   int index = -1;
-
-   for( int i=0; i<vg_list_size(gameserver.clients); i++ ){
-      if( !gameserver.clients[i].active ){
-         index = i;
-         break;
-      }
-   }
-
-   if( index == -1 ){
-      vg_error( "Server full\n" );
-      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-            hSteamNetworkingSockets, conn, 
-            4500,
-            NULL, 1 );
-      return;
-   }
-
-   struct gameserver_client *client = &gameserver.clients[index];
-   EResult accept_status = SteamAPI_ISteamNetworkingSockets_AcceptConnection(
-            hSteamNetworkingSockets, conn );
-
-   if( accept_status == k_EResultOK )
-   {
-      vg_success( "Accepted client (id: %u, index: %d)\n", conn, index );
-
-      client->active = 1;
-      client->connection = conn;
-
-      SteamAPI_ISteamNetworkingSockets_SetConnectionPollGroup(
-            hSteamNetworkingSockets, conn, gameserver.client_group );
-      
-      SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
-            hSteamNetworkingSockets, conn, index );
-
-      if( gameserver.loopback_test )
-      {
-         vg_warn( "[DEV] Creating loopback client\n" );
-         struct gameserver_client *loopback = &gameserver.clients[1];
-         loopback->active = 1;
-         loopback->connection = 0;
-      }
-   }
-   else
-   {
-      vg_warn( "Error accepting connection (id: %u)\n", conn );
-      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-            hSteamNetworkingSockets, conn, 
-            k_ESteamNetConnectionEnd_Misc_InternalError,
-            NULL, 1 );
-   }
-}
-
-static void on_auth_status( CallbackMsg_t *msg ){
-   SteamNetAuthenticationStatus_t *info = (void *)msg->m_pubParam;
-   vg_info( "  Authentication availibility: %s\n", 
-         string_ESteamNetworkingAvailability(info->m_eAvail) );
-   vg_info( "  %s\n", info->m_debugMsg );
-}
-
-/*
- * Get client id of connection handle. Will be -1 if unkown to us either because
- * the server is full or we already disconnected them
- */
-static i32 gameserver_conid( HSteamNetConnection hconn )
-{
-   i64 id;
-
-   if( hconn == 0 )
-   {
-      if( gameserver.loopback_test )
-         return 1;
-      else
-         return -1;
-   }
-   else
-      id = SteamAPI_ISteamNetworkingSockets_GetConnectionUserData(
-               hSteamNetworkingSockets, hconn );
-
-   if( (id < 0) || (id >= NETWORK_MAX_PLAYERS) )
-      return -1;
-
-   return id;
-}
-
-/*
- * Callback for steam connection state change
- */
-static void on_connect_status( CallbackMsg_t *msg )
-{
-   SteamNetConnectionStatusChangedCallback_t *info = (void *)msg->m_pubParam;
-   vg_info( "  Connection status changed for %lu\n", info->m_hConn );
-
-   vg_info( "  %s -> %s\n", 
-         string_ESteamNetworkingConnectionState(info->m_eOldState),
-         string_ESteamNetworkingConnectionState(info->m_info.m_eState) );
-
-   if( info->m_info.m_eState==k_ESteamNetworkingConnectionState_Connecting )
-   {
-      handle_new_connection( info->m_hConn );
-   }
-
-   if( (info->m_info.m_eState == 
-            k_ESteamNetworkingConnectionState_ClosedByPeer ) ||
-       (info->m_info.m_eState == 
-        k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) ||
-       (info->m_info.m_eState == 
-        k_ESteamNetworkingConnectionState_Dead) ||
-       (info->m_info.m_eState ==
-        k_ESteamNetworkingConnectionState_None) )
-   {
-      vg_info( "End reason: %d\n", info->m_info.m_eEndReason );
-
-      int client_id = gameserver_conid( info->m_hConn );
-      if( client_id != -1 )
-      {
-         gameserver_player_leave( client_id );
-         remove_client( client_id );
-
-         if( gameserver.loopback_test )
-         {
-            gameserver_player_leave( 1 );
-            remove_client( 1 );
-         }
-      }
-      else 
-      {
-         SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-               hSteamNetworkingSockets, info->m_hConn, 0, NULL, 0 );
-      }
-   }
-}
-
-static void gameserver_rx_version( SteamNetworkingMessage_t *msg )
-{
-   netmsg_version *version = msg->m_pData;
-
-   int client_id = gameserver_conid( msg->m_conn );
-   if( client_id == -1 ) 
-   {
-      vg_warn( "Recieved version from unkown connection (%u)\n", msg->m_conn );
-      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-            hSteamNetworkingSockets, msg->m_conn,
-            k_ESteamNetConnectionEnd_Misc_InternalError,
-            NULL, 1 );
-      return;
-   }
-
-   struct gameserver_client *client = &gameserver.clients[ client_id ];
-
-   if( client->version )
-   {
-      vg_warn( "Already have version for this client (%d conn: %u)", 
-               client_id, msg->m_conn );
-      return;
-   }
-
-   client->version = version->version;
-
-   if( client->version != NETWORK_SKATERIFT_VERSION )
-   {
-      gameserver_send_version_to_client( client_id );
-      remove_client( client_id );
-      return;
-   }
-
-   /* this is the sign on point for non-auth servers,
-    * for auth servers it comes at the end of rx_auth 
-    */
-   if( gameserver.auth_mode != eServerModeAuthentication )
-   {
-      client->steamid = k_steamid_max;
-      gameserver_player_join( client_id );
-
-      if( gameserver.loopback_test )
-      {
-         struct gameserver_client *loopback = &gameserver.clients[1];
-         loopback->steamid = k_steamid_max;
-         gameserver_player_join( 1 );
-      }
-   }
-}
-
-/* 
- * recieve auth ticket from connection. will only accept it if we've added them
- * to the client list first.
- */
-static void gameserver_rx_auth( SteamNetworkingMessage_t *msg ){
-   if( gameserver.auth_mode != eServerModeAuthentication ){
-      vg_warn( "Running server without authentication. "
-               "Connection %u tried to authenticate.\n", msg->m_conn );
-      return;
-   }
-
-   int client_id = gameserver_conid( msg->m_conn );
-   if( client_id == -1 ) {
-      vg_warn( "Recieved auth ticket from unkown connection (%u)\n", 
-               msg->m_conn );
-      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-            hSteamNetworkingSockets, msg->m_conn,
-            k_ESteamNetConnectionEnd_Misc_InternalError, NULL, 1 );
-      return;
-   }
-
-   struct gameserver_client *client = &gameserver.clients[ client_id ];
-   if( client->steamid ){
-      vg_warn( "Already authorized this user but another app ticket was sent"
-               " again (%d conn: %u)\n", client_id, msg->m_conn );
-      return;
-   }
-
-   if( client->version == 0 ){
-      vg_error( "Client has not sent their version yet (%u)\n", msg->m_conn );
-      remove_client( client_id );
-      return;
-   }
-
-   vg_low( "Attempting to verify user\n" );
-
-   if( msg->m_cbSize < sizeof(netmsg_auth) ){
-      vg_error( "Malformed auth ticket, too small (%u)\n", msg->m_conn );
-      remove_client( client_id );
-      return;
-   }
-
-   netmsg_auth *auth = msg->m_pData;
-
-   if( msg->m_cbSize < sizeof(netmsg_auth)+auth->ticket_length ||
-       auth->ticket_length > 1024 ){
-      vg_error( "Malformed auth ticket, ticket_length incorrect (%u)\n",
-                  auth->ticket_length );
-      remove_client( client_id );
-      return;
-   }
-
-   u8 decrypted[1024];
-   u32 ticket_len = 1024;
-
-   int success = SteamEncryptedAppTicket_BDecryptTicket(
-         auth->ticket, auth->ticket_length, decrypted,
-         &ticket_len, gameserver.app_symmetric_key,
-         k_nSteamEncryptedAppTicketSymmetricKeyLen );
-
-   if( !success ){
-      vg_error( "Failed to decrypt users ticket (client %u)\n", msg->m_conn );
-      vg_error( "  ticket length: %u\n", auth->ticket_length );
-      remove_client( client_id );
-      return;
-   }
-
-   if( SteamEncryptedAppTicket_GetTicketIssueTime( decrypted, ticket_len )){
-      RTime32 ctime = time(NULL),
-              tickettime = SteamEncryptedAppTicket_GetTicketIssueTime(
-                    decrypted, ticket_len ),
-              expiretime = tickettime + 24*3*60*60;
-      
-      if( ctime > expiretime ){
-         vg_error( "Ticket expired (client %u)\n", msg->m_conn );
-         remove_client( client_id );
-         return;
-      }
-   }
-
-   CSteamID steamid;
-   SteamEncryptedAppTicket_GetTicketSteamID( decrypted, ticket_len, &steamid );
-   vg_success( "User is authenticated! steamid %lu (%u)\n", 
-         steamid.m_unAll64Bits, msg->m_conn );
-
-   client->steamid = steamid.m_unAll64Bits;
-   gameserver_player_join( client_id );
-}
-
-/*
- * Player updates sent to us
- * -----------------------------------------------------------------------------
- */
-
-static int packet_minsize( SteamNetworkingMessage_t *msg, u32 size ){
-   if( msg->m_cbSize < size ) {
-      vg_error( "Invalid packet size (must be at least %u)\n", size );
-      return 0;
-   }
-   else{
-      return 1;
-   }
-}
-
-struct db_set_username_thread_data {
-   u64 steamid;
-   char username[ NETWORK_USERNAME_MAX ];
-};
-
-static void gameserver_update_db_username( db_request *db_req ){
-   struct db_set_username_thread_data *inf = (void *)db_req->data;
-
-   if( inf->steamid == k_steamid_max )
-      return;
-
-   int admin = 0;
-   if( inf->steamid == 76561198072130043 )
-      admin = 2;
-
-   db_updateuser( inf->steamid, inf->username, admin );
-}
-
-static int gameserver_item_eq( struct gameserver_item *ia, 
-                               struct gameserver_item *ib ){
-   if( ia->hash == ib->hash )
-      if( !strcmp(ia->uid,ib->uid) )
-         return 1;
-
-   return 0;
-}
-
-/*
- * Match addons between two player IDs. if clear is set, then the flags between
- * those two IDs will all be set to 0.
- */
-static void gameserver_update_knowledge_table( int client0, int client1, 
-                                               int clear ){
-   u32 idx = network_pair_index( client0, client1 );
-
-   struct gameserver_client *c0 = &gameserver.clients[client0],
-                            *c1 = &gameserver.clients[client1];
-
-   u8 flags = 0x00;
-
-   if( !clear ){
-      if( gameserver_item_eq(&c0->items[k_netmsg_playeritem_world0],
-                             &c1->items[k_netmsg_playeritem_world0]))
-         flags |= CLIENT_KNOWLEDGE_SAME_WORLD0;
-
-      if( gameserver_item_eq(&c0->items[k_netmsg_playeritem_world1],
-                             &c1->items[k_netmsg_playeritem_world1]))
-         flags |= CLIENT_KNOWLEDGE_SAME_WORLD1;
-   }
-
-   gameserver.client_knowledge_mask[idx] = flags;
-}
-
-/*
- * If a change has been made on this client, then it will adjust the entire
- * table of other players. if clear is set, all references to client will be set
- * to 0.
- */
-static void gameserver_update_all_knowledge( int client, int clear ){
-   for( int i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
-      if( i == client )
-         continue;
-
-      struct gameserver_client *ci = &gameserver.clients[i];
-
-      if( ci->steamid )
-         gameserver_update_knowledge_table( client, i, clear );
-   }
-}
-
-static void gameserver_propogate_player_frame( int client_id, 
-                                               netmsg_playerframe *frame, 
-                                               u32 size ){
-   u32 basic_size = sizeof(netmsg_playerframe) + ((24*3)/8);
-   netmsg_playerframe *full = alloca(size),
-                      *basic= alloca(basic_size);
-
-   memcpy( full, frame, size );
-   memcpy( basic, frame, basic_size );
-
-   full->client = client_id;
-   basic->client = client_id;
-   basic->subsystem = 4; /* (.._basic_info: 24f*3 animator ) */
-   basic->sound_effects = 0;
-
-   struct gameserver_client *c0 = &gameserver.clients[client_id];
-   c0->instance = frame->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
-
-   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
-   {
-      if( i == client_id )
-         continue;
-
-      struct gameserver_client *ci = &gameserver.clients[i];
-
-      int send_full = 0;
-
-      if( c0->instance == ci->instance )
-      {
-         u32 k_index = network_pair_index( client_id, i );
-         u8 k_mask = gameserver.client_knowledge_mask[ k_index ];
-         
-         if( (k_mask & (CLIENT_KNOWLEDGE_SAME_WORLD0<<c0->instance)) )
-            send_full = 1;
-      }
-
-      if( send_full )
-      {
-         gameserver_send_to_client( i, full, size, 
-                                    k_nSteamNetworkingSend_Unreliable );
-      }
-      else 
-      {
-         gameserver_send_to_client( i, basic, basic_size, 
-                                    k_nSteamNetworkingSend_Unreliable );
-      }
-   }
-}
-
-static void gameserver_rx_200_300( SteamNetworkingMessage_t *msg )
-{
-   netmsg_blank *tmp = msg->m_pData;
-
-   int client_id = gameserver_conid( msg->m_conn );
-   if( client_id == -1 ) return;
-
-   struct gameserver_client *client = &gameserver.clients[ client_id ];
-
-   if( tmp->inetmsg_id == k_inetmsg_playerusername )
-   {
-      if( !packet_minsize( msg, sizeof(netmsg_playerusername)+1 ))
-         return;
-      
-      netmsg_playerusername *src = msg->m_pData;
-
-      u32 name_len = network_msgstring( src->name, msg->m_cbSize, 
-                                        sizeof(netmsg_playerusername),
-                                        client->username, 
-                                        NETWORK_USERNAME_MAX );
-
-      /* update other users about this change */
-      netmsg_playerusername *prop = alloca(sizeof(netmsg_playerusername)+
-                                             NETWORK_USERNAME_MAX );
-                                           
-      prop->inetmsg_id = k_inetmsg_playerusername;
-      prop->index = client_id;
-      u32 chs = vg_strncpy( client->username, prop->name, NETWORK_USERNAME_MAX,
-                            k_strncpy_always_add_null );
-
-      vg_info( "client #%d changed name to: %s\n", client_id, prop->name );
-
-      u32 propsize = sizeof(netmsg_playerusername) + chs + 1;
-      gameserver_send_to_all( client_id, prop, propsize,
-                              k_nSteamNetworkingSend_Reliable );
-
-      /* update database about this */
-      db_request *call = db_alloc_request( 
-                           sizeof(struct db_set_username_thread_data) );
-      struct db_set_username_thread_data *inf = (void *)call->data;
-      inf->steamid = client->steamid;
-      vg_strncpy( client->username, inf->username, 
-                  sizeof(inf->username), k_strncpy_always_add_null );
-      call->handler = gameserver_update_db_username;
-      db_send_request( call );
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playerframe )
-   {
-      gameserver_propogate_player_frame( client_id, 
-                                         msg->m_pData, msg->m_cbSize );
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playeritem )
-   {
-      netmsg_playeritem *item = msg->m_pData;
-
-      /* record */
-      if( item->type_index >= k_netmsg_playeritem_max )
-      {
-         vg_warn( "Client #%d invalid equip type %u\n", 
-                  client_id, (u32)item->type_index );
-         return;
-      }
-      
-      char *dest = client->items[ item->type_index ].uid;
-
-      network_msgstring( item->uid, msg->m_cbSize, sizeof(netmsg_playeritem),
-                         dest, ADDON_UID_MAX );
-
-      vg_info( "Client #%d equiped: [%s] %s\n", 
-               client_id, 
-               (const char *[]){[k_netmsg_playeritem_board]="board",
-                                [k_netmsg_playeritem_player]="player",
-                                [k_netmsg_playeritem_world0]="world0",
-                                [k_netmsg_playeritem_world1]="world1"
-               }[item->type_index], item->uid );
-
-      gameserver_update_all_knowledge( client_id, 0 );
-                           
-      /* propogate */
-      netmsg_playeritem *prop = alloca(msg->m_cbSize);
-      memcpy( prop, msg->m_pData, msg->m_cbSize );
-      prop->client = client_id;
-      gameserver_send_to_all( client_id, prop, msg->m_cbSize, 
-                              k_nSteamNetworkingSend_Reliable );
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_chat )
-   {
-      netmsg_chat *chat = msg->m_pData,
-                  *prop = alloca( sizeof(netmsg_chat) + NETWORK_MAX_CHAT );
-      prop->inetmsg_id = k_inetmsg_chat;
-      prop->client = client_id;
-
-      u32 l = network_msgstring( chat->msg, msg->m_cbSize, sizeof(netmsg_chat),
-                                 prop->msg, NETWORK_MAX_CHAT );
-      vg_info( "[%d]: %s\n", client_id, prop->msg );
-
-      gameserver_send_to_all( client_id, prop, sizeof(netmsg_chat)+l+1, 
-                              k_nSteamNetworkingSend_Reliable );
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_region )
-   {
-      netmsg_region *region = msg->m_pData,
-                    *prop = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
-
-      prop->inetmsg_id = k_inetmsg_region;
-      prop->client = client_id;
-      prop->flags = region->flags;
-
-      u32 l = network_msgstring( 
-            region->loc, msg->m_cbSize, sizeof(netmsg_region),
-            client->region, NETWORK_REGION_MAX );
-      client->region_flags = region->flags;
-
-      l = vg_strncpy( client->region, prop->loc, NETWORK_REGION_MAX, 
-                      k_strncpy_always_add_null );
-
-      gameserver_send_to_all( client_id, prop, sizeof(netmsg_region)+l+1, 
-                              k_nSteamNetworkingSend_Reliable );
-      vg_info( "client %d moved to region: %s\n", client_id, client->region );
-   }
-   else 
-   {
-      vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
-               tmp->inetmsg_id );
-   }
-}
-
-static void gameserver_request_respond( enum request_status status,
-                                        netmsg_request *res, vg_msg *body,
-                                        SteamNetworkingMessage_t *msg ){
-   int client_id = gameserver_conid( msg->m_conn );
-   u32 len = 0;
-   if( body ){
-      len = body->cur.co;
-      vg_low( "[%d#%d] Response: %d\n", client_id, (i32)res->id, status );
-      vg_msg_print( body, len );
-   }
-
-   res->status = status;
-
-   if( gameserver.loopback_test && !msg->m_conn )
-   {
-      release_message( msg );
-      return;
-   }
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, msg->m_conn,
-         res, sizeof(netmsg_request) + len,
-         k_nSteamNetworkingSend_Reliable, NULL );
-
-   release_message( msg );
-}
-
-struct user_request_thread_data {
-   SteamNetworkingMessage_t *msg;
-};
-
-static u32 gameserver_get_current_week(void){
-   return time(NULL) / (7*24*60*60);
-}
-
-static enum request_status gameserver_cat_table( 
-      vg_msg *msg, 
-      const char *mod, const char *route, u32 week, const char *alias )
-{
-   char table_name[ DB_TABLE_UID_MAX ];
-   if( !db_get_highscore_table_name( mod, route, week, table_name ) )
-      return k_request_status_out_of_memory;
-
-   char buf[512];
-   vg_str q;
-   vg_strnull( &q, buf, 512 );
-   vg_strcat( &q, "SELECT * FROM \"" );
-   vg_strcat( &q, table_name );
-   vg_strcat( &q, "\" ORDER BY time ASC LIMIT 10;" );
-   if( !vg_strgood(&q) )
-      return k_request_status_out_of_memory;
-
-   sqlite3_stmt *stmt = db_stmt( q.buffer );
-   if( !stmt )
-      return k_request_status_database_error;
-
-   vg_msg_frame( msg, alias );
-   for( u32 i=0; i<10; i ++ ){
-      int fc = sqlite3_step( stmt );
-
-      if( fc == SQLITE_ROW ){
-         i32 time = sqlite3_column_int( stmt, 1 );
-         i64 steamid_i64 = sqlite3_column_int64( stmt, 0 );
-         u64 steamid = *((u64 *)&steamid_i64);
-
-         if( steamid == k_steamid_max )
-            continue;
-
-         vg_msg_frame( msg, "" );
-         vg_msg_wkvnum( msg, "time", k_vg_msg_u32, 1, &time );
-         vg_msg_wkvnum( msg, "steamid", k_vg_msg_u64, 1, &steamid );
-
-         char username[32];
-         if( db_getuserinfo( steamid, username, sizeof(username), NULL ) )
-            vg_msg_wkvstr( msg, "username", username );
-         vg_msg_end_frame( msg );
-      }
-      else if( fc == SQLITE_DONE ){
-         break;
-      }
-      else {
-         log_sqlite3( fc );
-         break;
-      }
-   }
-
-   sqlite3_finalize( stmt );
-   vg_msg_end_frame( msg );
-   return k_request_status_ok;
-}
-
-static void gameserver_process_user_request( db_request *db_req )
-{
-   struct user_request_thread_data *inf = (void *)db_req->data;
-   SteamNetworkingMessage_t *msg = inf->msg;
-
-   int client_id = gameserver_conid( msg->m_conn );
-   if( client_id == -1 )
-   {
-      release_message( msg );
-      return;
-   }
-
-   struct gameserver_client *client = &gameserver.clients[ client_id ];
-
-   netmsg_request *req = (netmsg_request *)msg->m_pData;
-   vg_msg data;
-   vg_msg_init( &data, req->q, msg->m_cbSize - sizeof(netmsg_request) );
-
-   /* create response packet */
-   netmsg_request *res = alloca( sizeof(netmsg_request) + NETWORK_REQUEST_MAX );
-   res->inetmsg_id = k_inetmsg_response;
-   res->id = req->id;
-   vg_msg body;
-   vg_msg_init( &body, res->q, NETWORK_REQUEST_MAX );
-
-   const char *endpoint = vg_msg_getkvstr( &data, "endpoint" );
-
-   if( !endpoint ){
-      gameserver_request_respond( k_request_status_invalid_endpoint,
-                                  res, NULL, msg );
-      return;
-   }
-
-   if( !strcmp( endpoint, "scoreboard" ) ){
-      const char *mod = vg_msg_getkvstr( &data, "mod" );
-      const char *route = vg_msg_getkvstr( &data, "route" );
-      u32 week;
-      vg_msg_getkvintg( &data, "week", k_vg_msg_u32, &week, NULL );
-      
-      if( week == NETWORK_LEADERBOARD_CURRENT_WEEK ){
-         gameserver_cat_table( &body, mod, route, 
-                               gameserver_get_current_week(), "rows_weekly" );
-      }
-      else if( week == NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK ){
-         gameserver_cat_table( &body, mod, route, 0, "rows" );
-         gameserver_cat_table( &body, mod, route, 
-                               gameserver_get_current_week(), "rows_weekly" );
-      }
-      else 
-         gameserver_cat_table( &body, mod, route, week, "rows" );
-
-      if( body.error != k_vg_msg_error_OK ){
-         gameserver_request_respond( k_request_status_out_of_memory,
-                                     res, NULL, msg );
-         return;
-      }
-
-      gameserver_request_respond( k_request_status_ok, res, &body, msg );
-   }
-   else if( !strcmp( endpoint, "setlap" ) ){
-      if( client->steamid == k_steamid_max ){
-         gameserver_request_respond( k_request_status_unauthorized,
-                                     res, NULL, msg );
-         return;
-      }
-
-      const char *mod = vg_msg_getkvstr( &data, "mod" );
-      const char *route = vg_msg_getkvstr( &data, "route" );
-      
-      char weekly_table[ DB_TABLE_UID_MAX ],
-           alltime_table[ DB_TABLE_UID_MAX ];
-
-      u32 week = gameserver_get_current_week();
-
-      if( !db_get_highscore_table_name( mod, route, 0, alltime_table ) ||
-          !db_get_highscore_table_name( mod, route, week, weekly_table ) ){
-         gameserver_request_respond( k_request_status_out_of_memory,
-                                     res, NULL, msg );
-         return;
-      }
-
-      i32 centiseconds;
-      vg_msg_getkvintg( &data, "time", k_vg_msg_i32, &centiseconds, NULL );
-      if( centiseconds < 5*100 ){
-         gameserver_request_respond( k_request_status_client_error,
-                                     res, NULL, msg );
-         return;
-      }
-
-      db_writeusertime( alltime_table, client->steamid, centiseconds, 1 );
-      db_writeusertime( weekly_table, client->steamid, centiseconds, 1 );
-      gameserver_request_respond( k_request_status_ok, res, NULL, msg );
-   }
-   else{
-      gameserver_request_respond( k_request_status_invalid_endpoint,
-                                  res, NULL, msg );
-   }
-}
-
-static void gameserver_rx_300_400( SteamNetworkingMessage_t *msg )
-{
-   netmsg_blank *tmp = msg->m_pData;
-
-   int client_id = gameserver_conid( msg->m_conn );
-   if( client_id == -1 )
-   {
-      release_message( msg );
-      return;
-   }
-
-   if( tmp->inetmsg_id == k_inetmsg_request )
-   {
-      if( gameserver.loopback_test && (client_id == 1) )
-      {
-         release_message( msg );
-         return;
-      }
-
-      if( !packet_minsize( msg, sizeof(netmsg_request)+1 ))
-      {
-         release_message( msg );
-         return;
-      }
-
-      db_request *call = db_alloc_request( 
-                           sizeof(struct user_request_thread_data) );
-      struct user_request_thread_data *inf = (void *)call->data;
-      inf->msg = msg;
-      call->handler = gameserver_process_user_request;
-      db_send_request( call );
-   }
-   else 
-   {
-      vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
-               tmp->inetmsg_id );
-      release_message( msg );
-   }
-}
-
-static void process_network_message( SteamNetworkingMessage_t *msg )
-{
-   if( msg->m_cbSize < sizeof(netmsg_blank) ){
-      vg_warn( "Discarding message (too small: %d)\n", 
-            msg->m_cbSize );
-      return;
-   }
-
-   netmsg_blank *tmp = msg->m_pData;
-
-   if( (tmp->inetmsg_id >= 200) && (tmp->inetmsg_id < 300) )
-   {
-      gameserver_rx_200_300( msg );
-      release_message( msg );
-   }
-   else if( (tmp->inetmsg_id >= 300) && (tmp->inetmsg_id < 400) )
-   {
-      gameserver_rx_300_400( msg );
-   }
-   else{
-      if( tmp->inetmsg_id == k_inetmsg_auth )
-         gameserver_rx_auth( msg );
-      else if( tmp->inetmsg_id == k_inetmsg_version ){
-         gameserver_rx_version( msg );
-      }
-      else {
-         vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
-                  tmp->inetmsg_id );
-      }
-      release_message( msg );
-   }
-}
-
-static void poll_connections(void)
-{
-   SteamNetworkingMessage_t *messages[32];
-   int len;
-
-   while(1)
-   {
-      len = SteamAPI_ISteamNetworkingSockets_ReceiveMessagesOnPollGroup(
-            hSteamNetworkingSockets,
-            gameserver.client_group, messages, vg_list_size(messages) );
-
-      if( len <= 0 )
-         return;
-
-      for( int i=0; i<len; i++ )
-      {
-         SteamNetworkingMessage_t *msg = messages[i];
-         msg->m_nUserData = 1;
-
-         if( gameserver.loopback_test )
-         {
-            HSteamNetConnection conid = msg->m_conn;
-            msg->m_conn = 0;
-            msg->m_nUserData ++;
-            process_network_message( msg );
-            msg->m_conn = conid;
-         }
-
-         process_network_message( msg );
-      }
-   }
-}
-
-static u64 seconds_to_server_ticks( double s ){
-   return s / 0.01;
-}
-
-int main( int argc, char *argv[] ){
-   signal( SIGINT, inthandler );
-   signal( SIGQUIT, inthandler );
-   signal( SIGPIPE, SIG_IGN );
-
-   char *arg;
-   while( vg_argp( argc, argv ) )
-   {
-      if( vg_long_opt( "noauth" ) )
-         gameserver.auth_mode = eServerModeNoAuthentication;
-
-      if( vg_long_opt( "loopback" ) )
-         gameserver.loopback_test = 1;
-   }
-   
-   vg_set_mem_quota( 80*1024*1024 );
-   vg_alloc_quota();
-   db_init();
-
-   /* steamworks init 
-    * --------------------------------------------------------------- */
-   steamworks_ensure_txt( "2103940" );
-   if( gameserver.auth_mode == eServerModeAuthentication ){
-      if( !vg_load_steam_symetric_key( "application_key", 
-                                       gameserver.app_symmetric_key )){
-         return 0;
-      }
-   }
-   else{
-      vg_warn( "Running without user authentication.\n" );
-   }
-
-   if( !SteamGameServer_Init( 0, NETWORK_PORT, NETWORK_PORT+1, 
-                              gameserver.auth_mode, "1.0.0.0" ) ){
-      vg_error( "SteamGameServer_Init failed\n" );
-      return 0;
-   }
-
-   void *hSteamGameServer = SteamAPI_SteamGameServer();
-   SteamAPI_ISteamGameServer_LogOnAnonymous( hSteamGameServer );
-
-   SteamAPI_ManualDispatch_Init();
-   HSteamPipe hsteampipe = SteamGameServer_GetHSteamPipe();
-   hSteamNetworkingSockets = 
-      SteamAPI_SteamGameServerNetworkingSockets_SteamAPI();
-
-   steam_register_callback( k_iSteamNetAuthenticationStatus, on_auth_status );
-   steam_register_callback( k_iSteamNetConnectionStatusChangedCallBack,
-                             on_connect_status );
-
-   vg_success( "Steamworks API running\n" );
-   steamworks_event_loop( hsteampipe );
-
-   /*
-    * Create a listener
-    */
-   HSteamListenSocket listener;
-   SteamNetworkingIPAddr localAddr;
-   SteamAPI_SteamNetworkingIPAddr_Clear( &localAddr );
-   localAddr.m_port = NETWORK_PORT;
-
-   listener = SteamAPI_ISteamNetworkingSockets_CreateListenSocketIP(
-                  hSteamNetworkingSockets, &localAddr, 0, NULL );
-   gameserver.client_group = SteamAPI_ISteamNetworkingSockets_CreatePollGroup(
-         hSteamNetworkingSockets );
-
-   u64 server_ticks = 8000,
-       last_record_save = 8000,
-       last_scoreboard_gen = 0;
-
-   while( !sig_stop ){
-      steamworks_event_loop( hsteampipe );
-      poll_connections();
-
-      usleep(10000);
-      server_ticks ++;
-
-      if( db_killed() )
-         break;
-   }
-   
-   SteamAPI_ISteamNetworkingSockets_DestroyPollGroup( hSteamNetworkingSockets,
-         gameserver.client_group );
-   SteamAPI_ISteamNetworkingSockets_CloseListenSocket( 
-         hSteamNetworkingSockets, listener );
-   
-   vg_info( "Shutting down\n..." );
-   SteamGameServer_Shutdown();
-   db_kill();
-   db_free();
-
-   return 0;
-}
diff --git a/gameserver.h b/gameserver.h
deleted file mode 100644 (file)
index a03b1a0..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-#pragma once
-#define VG_SERVER
-
-#include "vg/vg_platform.h"
-#include "vg/vg_steam.h"
-#include "vg/vg_steam_networking.h"
-#include "vg/vg_steam_http.h"
-#include "vg/vg_steam_auth.h"
-#include "network_msg.h"
-#include "network_common.h"
-#include <sys/socket.h>
-
-#define CLIENT_KNOWLEDGE_SAME_WORLD0 0x1
-#define CLIENT_KNOWLEDGE_SAME_WORLD1 0x2
-#define CLIENT_KNOWLEDGE_FRIENDS     0x4 /* unused */
-
-struct {
-   HSteamNetPollGroup client_group;
-   EServerMode auth_mode;
-
-   struct gameserver_client {
-      int active;
-      u32 version;
-      int authenticated;
-      HSteamNetConnection connection;
-      char username[ NETWORK_USERNAME_MAX ];
-
-      u8 instance;
-
-      struct gameserver_item {
-         char uid[ADDON_UID_MAX];
-         u32  hash;
-      }
-      items[k_netmsg_playeritem_max];
-
-      char region[ NETWORK_REGION_MAX ];
-      u32  region_flags;
-
-      u64  steamid;
-   }
-   clients[ NETWORK_MAX_PLAYERS ];
-
-   u8 client_knowledge_mask[ (NETWORK_MAX_PLAYERS*(NETWORK_MAX_PLAYERS-1))/2 ];
-   u8 app_symmetric_key[ k_nSteamEncryptedAppTicketSymmetricKeyLen ];
-
-   bool loopback_test;
-}
-static gameserver = {
-   .auth_mode = eServerModeAuthentication
-};
-
-static ISteamNetworkingSockets *hSteamNetworkingSockets = NULL;
diff --git a/gameserver_db.h b/gameserver_db.h
deleted file mode 100644 (file)
index fe39931..0000000
+++ /dev/null
@@ -1,398 +0,0 @@
-#ifndef GAMESERVER_DB_H
-#define GAMESERVER_DB_H
-
-#include "vg/vg_log.h"
-#include "vg/vg_mem_queue.h"
-#include "network_common.h"
-#include "dep/sqlite3/sqlite3.h"
-#include <pthread.h>
-#include <unistd.h>
-
-#define DB_COURSE_UID_MAX 32
-#define DB_TABLE_UID_MAX (ADDON_UID_MAX+DB_COURSE_UID_MAX+32)
-//#define DB_CRASH_ON_SQLITE_ERROR
-#define DB_LOG_SQL_STATEMENTS
-#define DB_REQUEST_BUFFER_SIZE (1024*2)
-
-typedef struct db_request db_request;
-struct db_request {
-   void (*handler)( db_request *req );
-   u32 size,_;
-   u8 data[];
-};
-
-struct {
-   sqlite3 *db;
-   pthread_t thread;
-   pthread_mutex_t mux;
-
-   vg_queue queue;
-   int kill;
-}
-static database;
-
-/*
- * Log the error code (or carry on if its OK).
- */
-static void log_sqlite3( int code ){
-   if( code == SQLITE_OK ) return;
-   vg_print_backtrace();
-   vg_error( "sqlite3(%d): %s\n", code, sqlite3_errstr(code) );
-
-#ifdef DB_CRASH_ON_SQLITE_ERROR 
-   int crash = *((int*)2);
-#endif
-}
-
-/*
- * Perpare statement and auto throw away if fails. Returns NULL on failure.
- */
-static sqlite3_stmt *db_stmt( const char *code ){
-#ifdef DB_LOG_SQL_STATEMENTS
-   vg_low( code );
-#endif
-
-   sqlite3_stmt *stmt;
-   int fc = sqlite3_prepare_v2( database.db, code, -1, &stmt, NULL );
-
-   if( fc != SQLITE_OK ){
-      log_sqlite3( fc );
-      sqlite3_finalize( stmt );
-      return NULL;
-   }
-
-   return stmt;
-}
-
-/*
- * bind zero terminated string
- */
-static int db_sqlite3_bind_sz( sqlite3_stmt *stmt, int pos, const char *sz ){
-   return sqlite3_bind_text( stmt, pos, sz, -1, SQLITE_STATIC );
-}
-
-/*
- * Allowed characters in sqlite table names. We use "" as delimiters.
- */
-static int db_verify_charset( const char *str, int mincount ){
-   for( int i=0; ; i++ ){
-      char c = str[i];
-      if( c == '\0' ){
-         if( i < mincount ) return 0;
-         else return 1;
-      }
-
-      if( !((c==' ')||(c=='!')||(c>='#'&&c<='~')) ) return 0;
-   }
-
-   return 0;
-}
-
-/*
- * Find table name from mod UID and course UID, plus the week number
- */
-static int db_get_highscore_table_name( const char *mod_uid,
-                                        const char *run_uid,
-                                        u32 week,
-                                        char table_name[DB_TABLE_UID_MAX] ){
-   if( !db_verify_charset( mod_uid, 13 ) ||
-       !db_verify_charset( run_uid, 1 ) ) return 0;
-
-   vg_str a;
-   vg_strnull( &a, table_name, DB_TABLE_UID_MAX );
-   vg_strcat( &a, mod_uid );
-   vg_strcat( &a, ":" );
-   vg_strcat( &a, run_uid );
-
-   if( week ){
-      vg_strcat( &a, "#" );
-      vg_strcati32( &a, week );
-   }
-
-   return vg_strgood( &a );
-}
-
-/*
- * Read value from highscore table. If not found or error, returns 0
- */
-static i32 db_readusertime( char table[DB_TABLE_UID_MAX], u64 steamid ){
-   char buf[ 512 ];
-   vg_str q;
-   vg_strnull( &q, buf, 512 );
-   vg_strcat( &q, "SELECT time FROM \"" );
-   vg_strcat( &q, table );
-   vg_strcat( &q, "\" WHERE steamid = ?;" );
-   if( !vg_strgood(&q) ) return 0;
-
-   sqlite3_stmt *stmt = db_stmt( q.buffer );
-   if( stmt ){
-      sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
-      int fc = sqlite3_step( stmt );
-
-      i32 result = 0;
-
-      if( fc == SQLITE_ROW )
-         result = sqlite3_column_int( stmt, 0 );
-      else if( fc != SQLITE_DONE )
-         log_sqlite3(fc);
-
-      sqlite3_finalize( stmt );
-      return result;
-   }
-   else return 0;
-}
-
-/*
- * Write to highscore table
- */
-static int db_writeusertime( char table[DB_TABLE_UID_MAX], u64 steamid, 
-                             i32 score, int only_if_faster ){
-   /* auto create table 
-    * ------------------------------------------*/
-   char buf[ 512 ];
-   vg_str q;
-   vg_strnull( &q, buf, 512 );
-   vg_strcat( &q, "CREATE TABLE IF NOT EXISTS \n \"" );
-   vg_strcat( &q, table );
-   vg_strcat( &q, "\"\n (steamid BIGINT UNIQUE, time INT);" );
-   if( !vg_strgood(&q) ) return 0;
-
-   vg_str str;
-   sqlite3_stmt *create_table = db_stmt( q.buffer );
-
-   if( create_table ){
-      db_sqlite3_bind_sz( create_table, 1, table );
-
-      int fc = sqlite3_step( create_table );
-      sqlite3_finalize( create_table );
-      if( fc != SQLITE_DONE )
-         return 0;
-   }
-   else return 0;
-
-   if( only_if_faster ){
-      i32 current = db_readusertime( table, steamid );
-      if( (current != 0) && (score > current) )
-         return 1;
-   }
-
-   /* insert score 
-    * -------------------------------------------------*/
-   vg_strnull( &q, buf, 512 );
-   vg_strcat( &q, "REPLACE INTO \"" );
-   vg_strcat( &q, table );
-   vg_strcat( &q, "\"(steamid,time)\n VALUES (?,?);" );
-   if( !vg_strgood(&q) ) return 0;
-
-   sqlite3_stmt *stmt = db_stmt( q.buffer );
-
-   if( stmt ){
-      sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
-      sqlite3_bind_int( stmt, 2, score );
-
-      int fc = sqlite3_step( stmt );
-      sqlite3_finalize( stmt );
-      if( fc != SQLITE_DONE )
-         return 0;
-      else 
-         return 1;
-   }
-   else return 0;
-}
-
-/*
- * Set username and type
- */
-static int db_updateuser( u64 steamid, const char *username, int admin ){
-   sqlite3_stmt *stmt = db_stmt(
-         "INSERT OR REPLACE INTO users (steamid, name, type) "
-         "VALUES (?,?,?);" );
-
-   if( stmt ){
-      sqlite3_bind_int64( stmt, 1, *((i64*)(&steamid)) ); 
-      db_sqlite3_bind_sz( stmt, 2, username );
-      sqlite3_bind_int( stmt, 3, admin );
-
-      int fc = sqlite3_step( stmt );
-      sqlite3_finalize(stmt);
-
-      if( fc == SQLITE_DONE ){
-         vg_success( "Inserted %lu (%s), type: %d\n",
-                     steamid, username, admin );
-         return 1;
-      }
-      else{
-         log_sqlite3( fc );
-         return 0;
-      }
-   }
-   else return 0;
-}
-
-/*
- * Get user info 
- */
-static int db_getuserinfo( u64 steamid, char *out_username, u32 username_max, 
-                           i32 *out_type ){
-   sqlite3_stmt *stmt = db_stmt( "SELECT * FROM users WHERE steamid = ?;" );
-   if( !stmt ) return 0;
-
-   sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
-   int fc = sqlite3_step( stmt );
-
-   if( fc != SQLITE_ROW ){
-      log_sqlite3( fc );
-      sqlite3_finalize( stmt );
-      return 0;
-   }
-
-   if( out_username ){
-      const char *name = (const char *)sqlite3_column_text( stmt, 1 );
-      vg_strncpy( name, out_username, username_max, k_strncpy_allow_cutoff );
-   }
-   
-   if( out_type )
-      *out_type = sqlite3_column_int( stmt, 2 );
-   
-   sqlite3_finalize( stmt );
-   return 1;
-}
-
-static void _db_thread_end(void){
-   pthread_mutex_lock( &database.mux );
-   database.kill = 1;
-   pthread_mutex_unlock( &database.mux );
-   sqlite3_close( database.db );
-}
-
-static void *db_loop(void *_){
-   int rc = sqlite3_open( "highscores.db", &database.db );
-
-   if( rc ){
-      vg_error( "database failure: %s\n", sqlite3_errmsg(database.db) );
-      _db_thread_end();
-      return NULL;
-   }
-
-   sqlite3_stmt *stmt = db_stmt(
-         "CREATE TABLE IF NOT EXISTS \n"
-         " users(steamid BIGINT UNIQUE, name VARCHAR(128), type INT);" );
-
-   if( stmt ){
-      int fc = sqlite3_step( stmt );
-      sqlite3_finalize(stmt);
-
-      if( fc == SQLITE_DONE ){
-         vg_success( "Created users table\n" );
-         db_updateuser( 76561198072130043, "harry", 2 );
-      }
-      else{
-         log_sqlite3( fc );
-         _db_thread_end();
-         return NULL;
-      }
-   }
-   else {
-      _db_thread_end();
-      return NULL;
-   }
-
-   /*
-    * Request processing loop 
-    */
-   while(1){
-      pthread_mutex_lock( &database.mux );
-
-      if( database.kill ){
-         pthread_mutex_unlock( &database.mux );
-         _db_thread_end();
-         break;
-      }
-
-      u32 processed = 0;
-
-      for( u32 i=0; i<16; i ++ ){
-         db_request *req = NULL;
-         if( database.queue.tail ){
-            req = (db_request *)database.queue.tail->data;
-            pthread_mutex_unlock( &database.mux );
-         }
-         else{
-            pthread_mutex_unlock( &database.mux );
-            break;
-         }
-         
-         req->handler( req );
-         processed ++;
-
-         pthread_mutex_lock( &database.mux );
-         vg_queue_pop( &database.queue );
-      }
-
-      if( processed )
-         vg_low( "Processed %u database requests.\n", processed );
-
-      usleep(50000);
-   }
-
-   vg_low( "Database thread terminates.\n" );
-   return NULL;
-}
-
-/*
- * Create database connection and users table
- */
-static int db_init(void){
-   database.queue.buffer = 
-      (u8 *)vg_linear_alloc( vg_mem.rtmemory, DB_REQUEST_BUFFER_SIZE ),
-   database.queue.size = DB_REQUEST_BUFFER_SIZE;
-
-   if( pthread_mutex_init( &database.mux, NULL ) )
-      return 0;
-
-   if( pthread_create( &database.thread, NULL, db_loop, NULL ) )
-      return 0;
-
-   return 1;
-}
-
-static int db_killed(void){
-   pthread_mutex_lock( &database.mux );
-   int result = database.kill;
-   pthread_mutex_unlock( &database.mux );
-   return result;
-}
-
-static void db_kill(void){
-   pthread_mutex_lock( &database.mux );
-   database.kill = 1;
-   pthread_mutex_unlock( &database.mux );
-   pthread_join( database.thread, NULL );
-}
-
-static void db_free(void){
-   pthread_mutex_destroy( &database.mux );
-}
-
-static db_request *db_alloc_request( u32 size ){
-   u32 total = sizeof(db_request) + size;
-
-   pthread_mutex_lock( &database.mux );
-   vg_queue_frame *frame = vg_queue_alloc( &database.queue, total );
-
-   if( frame ){
-      db_request *req = (db_request *)frame->data;
-      req->size = size;
-      return req;
-   }
-   else {
-      pthread_mutex_unlock( &database.mux );
-      return NULL;
-   }
-}
-
-static void db_send_request( db_request *request ){
-   pthread_mutex_unlock( &database.mux );
-}
-
-#endif /* GAMESERVER_DB_H */
diff --git a/gui.h b/gui.h
deleted file mode 100644 (file)
index 46e2e1e..0000000
--- a/gui.h
+++ /dev/null
@@ -1,360 +0,0 @@
-#pragma once
-#include "font.h"
-#include "input.h"
-#include "player.h"
-#include "vg/vg_engine.h"
-#include "vg/vg_ui/imgui.h"
-
-#define GUI_COL_DARK   ui_opacity( 0x00000000, 0.7f )
-#define GUI_COL_NORM   ui_opacity( 0x00101010, 0.7f )
-#define GUI_COL_ACTIVE ui_opacity( 0x00444444, 0.7f )
-#define GUI_COL_CLICK  ui_opacity( 0x00858585, 0.7f )
-#define GUI_COL_HI     ui_opacity( 0x00ffffff, 0.8f )
-
-enum gui_icon {
-   k_gui_icon_tick    = 0,
-   k_gui_icon_tick_2d,
-   k_gui_icon_exclaim,
-   k_gui_icon_exclaim_2d,
-   k_gui_icon_board,
-   k_gui_icon_world,
-   k_gui_icon_rift,
-   k_gui_icon_rift_run,
-   k_gui_icon_rift_run_2d,
-   k_gui_icon_friend,
-   k_gui_icon_player,
-   k_gui_icon_rift_run_gold,
-   k_gui_icon_rift_run_silver,
-   k_gui_icon_glider,
-   k_gui_icon_spawn,
-   k_gui_icon_spawn_select,
-
-   k_gui_icon_count,
-};
-
-#define GUI_HELPER_TEXT_LENGTH 32
-
-struct{
-   struct gui_helper{
-      vg_input_op *binding;
-      char text[GUI_HELPER_TEXT_LENGTH];
-      int greyed;
-   }
-   helpers[4];
-   u32 helper_count;
-
-   int active_positional_helper;
-
-   struct icon_call {
-      enum gui_icon icon;
-      v4f location;
-      v4f colour;
-      int colour_changed;
-   }
-   icon_draw_buffer[64];
-   u32 icon_draw_count;
-   v4f cur_icon_colour;
-   int colour_changed;
-
-   char location[64];
-   f64  location_time;
-
-   f32 factive;
-   font3d font;
-
-   v3f trick_co;
-
-   mdl_context model_icons;
-   GLuint icons_texture;
-   glmesh icons_mesh;
-
-   mdl_submesh *icons[ k_gui_icon_count ];
-}
-static gui = {.cur_icon_colour = {1.0f,1.0f,1.0f,1.0f},.colour_changed=1};
-
-static void gui_helper_clear(void){
-   gui.helper_count = 0;
-   gui.active_positional_helper = 0;
-}
-
-static struct gui_helper *gui_new_helper( vg_input_op *bind, vg_str *out_text ){
-   if( gui.helper_count >= VG_ARRAY_LEN(gui.helpers) ){
-      vg_error( "Too many helpers\n" );
-      return NULL;
-   }
-
-   struct gui_helper *helper = &gui.helpers[ gui.helper_count ++ ];
-   helper->greyed = 0;
-   helper->binding = bind;
-   vg_strnull( out_text, helper->text, sizeof(helper->text) );
-   return helper;
-}
-
-static void gui_render_icons(void)
-{
-   vg_camera ortho;
-
-   float fl = 0.0f,
-         fr = vg.window_x,
-         fb = 0.0f,
-         ft = vg.window_y,
-         rl = 1.0f / (fr-fl),
-         tb = 1.0f / (ft-fb);
-
-   m4x4_zero( ortho.mtx.p );
-   ortho.mtx.p[0][0] = 2.0f * rl;
-   ortho.mtx.p[1][1] = 2.0f * tb;
-   ortho.mtx.p[3][0] = (fr + fl) * -rl;
-   ortho.mtx.p[3][1] = (ft + fb) * -tb;
-   ortho.mtx.p[3][3] = 1.0f;
-   m4x3_identity( ortho.transform );
-   vg_camera_update_view( &ortho );
-   m4x4_mul( ortho.mtx.p, ortho.mtx.v, ortho.mtx.pv );   /* HACK */
-   vg_camera_finalize( &ortho );
-
-   /* icons */
-   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, &ortho );
-   mesh_bind( &gui.icons_mesh );
-
-   m4x3f mmdl;
-   m4x3_identity( mmdl );
-   shader_model_font_uMdl( mmdl );
-
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, gui.icons_texture );
-   shader_model_font_uTexMain( 0 );
-
-   for( u32 i=0; i<gui.icon_draw_count; i++ ){
-      struct icon_call *call = &gui.icon_draw_buffer[i];
-
-      if( call->colour_changed )
-         shader_model_font_uColour( call->colour );
-
-      shader_model_font_uOffset( call->location );
-
-      mdl_submesh *sm = gui.icons[ call->icon ];
-      if( sm )
-         mdl_draw_submesh( sm );
-   }
-
-   gui.icon_draw_count = 0;
-}
-
-static void gui_draw( ui_context *ctx )
-{
-   if( gui.active_positional_helper && 
-         (v3_dist2(localplayer.rb.co,gui.trick_co) > 2.0f) )
-      gui_helper_clear();
-
-   /* helpers 
-    * -----------------------------------------------------------------  */
-
-   gui.factive = vg_lerpf( gui.factive, gui.helper_count?1.0f:0.0f,
-                           vg.time_frame_delta*2.0f );
-   
-   ctx->font = &vgf_default_title;
-   ui_px height = ctx->font->ch + 16;
-   ui_rect lwr = { 0, vg.window_y - height, vg.window_x, height };
-
-   ui_px x = 0;
-   for( u32 i=0; i<gui.helper_count; i++ )
-   {
-      struct gui_helper *helper = &gui.helpers[i];
-
-      char buf[128];
-      vg_str str;
-      vg_strnull( &str, buf, sizeof(buf) );
-      vg_input_string( &str, helper->binding, 1 );
-      
-      ui_rect box = { x, lwr[1], 1000, lwr[3] };
-
-      u32 fg = 0;
-      f32 opacity = 0.4f;
-      if( helper->greyed ) 
-      {
-         fg = ui_colour(ctx, k_ui_fg+2);
-         opacity = 0.1f;
-      }
-
-      struct ui_vert *bg = ui_fill( ctx, box, 
-                                    ui_opacity( GUI_COL_DARK, opacity ) );
-
-      u32 w;
-      box[0] += 16;
-      w = ui_text( ctx, box, buf, 1, k_ui_align_middle_left, fg );
-      w *= ctx->font->sx;
-      bg[1].co[0] = x + w + 32;
-      bg[2].co[0] = x + w + 32;
-      x += w + 32;
-
-      box[0] = x;
-      bg = ui_fill( ctx, box, ui_opacity( GUI_COL_NORM, opacity*0.7f ) );
-      box[0] += 8;
-      w = ui_text( ctx, box, helper->text, 1, k_ui_align_middle_left, fg );
-      w *= ctx->font->sx;
-      bg[1].co[0] = box[0] + w + 16;
-      bg[2].co[0] = box[0] + w + 16;
-      x += w + 32;
-   }
-
-   vg_ui.frosting = gui.factive*0.015f; 
-   ui_flush( ctx, k_ui_shader_colour, NULL );
-   vg_ui.frosting = 0.0f;
-
-
-   f64 loc_t = (vg.time_real - gui.location_time) / 5.0;
-   if( (loc_t < 1.0) && (gui.location_time != 0.0) )
-   {
-      f32 t = 1.0f-vg_minf(1.0f,vg_minf(loc_t*20.0f,2.0f-loc_t*2.0f)),
-          o = 1.0f-t*t*(2.0f-t);
-
-      ui_rect box = { 0, (vg.window_y*2)/3 - height/2, vg.window_x, height };
-      ui_fill( ctx, box, ui_opacity( GUI_COL_NORM, 0.5f ) );
-      ui_text( ctx, box, gui.location, 1, k_ui_align_middle_center, 0 );
-
-      vg_ui.colour[3] = o;
-      ui_flush( ctx, k_ui_shader_colour, NULL );
-   }
-
-   vg_ui.colour[3] = 1.0f;
-   ctx->font = &vgf_default_small;
-}
-
-static int gui_location_print_ccmd( int argc, const char *argv[] )
-{
-   if( argc > 0 )
-   {
-      char new_loc[64];
-      vg_str str;
-      vg_strnull( &str, new_loc, 64 );
-      for( int i=0; i<argc; i++ )
-      {
-         vg_strcat( &str, argv[i] );
-         vg_strcat( &str, " " );
-      }
-      if( !strcmp(gui.location,new_loc) ) return 0;
-      vg_strncpy( new_loc, gui.location, 64, k_strncpy_always_add_null );
-      gui.location_time = vg.time_real;
-   }
-   return 0;
-}
-
-static int gui_showtrick_ccmd( int argc, const char *argv[] )
-{
-   if( argc == 1 )
-   {
-      gui_helper_clear();
-      vg_str text;
-
-           if( !strcmp( argv[0], "pump" ) ){
-         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ) )
-            vg_strcat( &text, "Pump" );
-      }
-      else if( !strcmp( argv[0], "flip" ) ){
-         if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ) )
-            vg_strcat( &text, "Flip" );
-      }
-      else if( !strcmp( argv[0], "ollie" ) ){
-         if( gui_new_helper( input_button_list[k_srbind_jump], &text ) )
-            vg_strcat( &text, "Ollie" );
-      }
-      else if( !strcmp( argv[0], "trick" ) ){
-         if( gui_new_helper( input_button_list[k_srbind_trick0], &text ) )
-            vg_strcat( &text, "Shuvit" );
-         if( gui_new_helper( input_button_list[k_srbind_trick1], &text ) )
-            vg_strcat( &text, "Kickflip" );
-         if( gui_new_helper( input_button_list[k_srbind_trick2], &text ) )
-            vg_strcat( &text, "Tre-Flip" );
-      }
-      else if( !strcmp( argv[0], "misc" ) ){
-         if( gui_new_helper( input_button_list[k_srbind_camera], &text ) )
-            vg_strcat( &text, "Camera" );
-         if( gui_new_helper( input_button_list[k_srbind_use], &text ) )
-            vg_strcat( &text, "Skate/Walk" );
-      }
-      else return 1;
-
-      v3_copy( localplayer.rb.co, gui.trick_co );
-      gui.active_positional_helper = 1;
-      return 0;
-   }
-   return 1;
-}
-
-static void gui_draw_icon( enum gui_icon icon, v2f co, f32 size )
-{
-   if( gui.icon_draw_count == VG_ARRAY_LEN(gui.icon_draw_buffer) )
-      return;
-
-   struct icon_call *call = &gui.icon_draw_buffer[ gui.icon_draw_count ++ ];
-
-   call->icon = icon;
-   call->location[0] = co[0] * (f32)vg.window_x;
-   call->location[1] = co[1] * (f32)vg.window_y;
-   call->location[2] = 0.0f;
-   call->location[3] = size * (f32)vg.window_x;
-
-   v4_copy( gui.cur_icon_colour, call->colour );
-   call->colour_changed = gui.colour_changed;
-   gui.colour_changed = 0;
-}
-
-static void gui_icon_setcolour( v4f colour ){
-   gui.colour_changed = 1;
-   v4_copy( colour, gui.cur_icon_colour );
-}
-
-static mdl_submesh *gui_find_icon( const char *name ){
-   mdl_mesh *mesh = mdl_find_mesh( &gui.model_icons, name );
-   if( mesh ){
-      if( mesh->submesh_count ){
-         return mdl_arritm( &gui.model_icons.submeshs, mesh->submesh_start );
-      }
-   }
-
-   return NULL;
-}
-
-static void gui_init(void)
-{
-   font3d_load( &gui.font, "models/rs_font.mdl", vg_mem.rtmemory );
-   vg_console_reg_cmd( "gui_location", gui_location_print_ccmd, NULL );
-   vg_console_reg_cmd( "showtrick", gui_showtrick_ccmd, NULL );
-
-   /* load icons */
-   void *alloc = vg_mem.rtmemory;
-   mdl_open( &gui.model_icons, "models/rs_icons.mdl", alloc );
-   mdl_load_metadata_block( &gui.model_icons, alloc );
-
-   gui.icons[ k_gui_icon_tick ] = gui_find_icon( "icon_tick" );
-   gui.icons[ k_gui_icon_tick_2d ] = gui_find_icon( "icon_tick2d" );
-   gui.icons[ k_gui_icon_exclaim ] = gui_find_icon( "icon_exclaim" );
-   gui.icons[ k_gui_icon_exclaim_2d ] = gui_find_icon( "icon_exclaim2d" );
-   gui.icons[ k_gui_icon_board ] = gui_find_icon( "icon_board" );
-   gui.icons[ k_gui_icon_world ] = gui_find_icon( "icon_world" );
-   gui.icons[ k_gui_icon_rift ] = gui_find_icon( "icon_rift" );
-   gui.icons[ k_gui_icon_rift_run ] = gui_find_icon( "icon_rift_run" );
-   gui.icons[ k_gui_icon_rift_run_2d ] = gui_find_icon( "icon_rift_run2d" );
-   gui.icons[ k_gui_icon_friend ] = gui_find_icon( "icon_friend" );
-   gui.icons[ k_gui_icon_player ] = gui_find_icon( "icon_player" );
-   gui.icons[ k_gui_icon_glider ] = gui_find_icon( "icon_glider" );
-   gui.icons[ k_gui_icon_spawn ] = gui_find_icon( "icon_spawn" );
-   gui.icons[ k_gui_icon_spawn_select ] = gui_find_icon( "icon_spawn_select" );
-   gui.icons[ k_gui_icon_rift_run_gold ] =
-      gui_find_icon("icon_rift_run_medal_gold");
-   gui.icons[ k_gui_icon_rift_run_silver]=
-      gui_find_icon("icon_rift_run_medal_silver");
-
-   vg_linear_clear( vg_mem.scratch );
-   if( !mdl_arrcount( &gui.model_icons.textures ) )
-      vg_fatal_error( "No texture in menu file" );
-   mdl_texture *tex0 = mdl_arritm( &gui.model_icons.textures, 0 );
-   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
-   mdl_fread_pack_file( &gui.model_icons, &tex0->file, data );
-   vg_tex2d_load_qoi_async( data, tex0->file.pack_size, 
-                            VG_TEX2D_LINEAR|VG_TEX2D_CLAMP,
-                            &gui.icons_texture );
-
-   mdl_async_load_glmesh( &gui.model_icons, &gui.icons_mesh, NULL );
-   mdl_close( &gui.model_icons );
-}
diff --git a/input.h b/input.h
deleted file mode 100644 (file)
index 3377824..0000000
--- a/input.h
+++ /dev/null
@@ -1,311 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h" 
-#include "vg/vg_console.h"
-#include "vg/vg_input.h"
-#include "vg/vg_m.h"
-#include "font.h"
-
-enum sr_bind
-{
-   k_srbind_jump = 0,
-   k_srbind_push,
-   k_srbind_skid,
-   k_srbind_trick0,
-   k_srbind_trick1,
-   k_srbind_trick2,
-   k_srbind_sit,
-   k_srbind_use,
-   k_srbind_reset,
-   k_srbind_dead_respawn,
-   k_srbind_camera,
-   k_srbind_mleft, 
-   k_srbind_mright, 
-   k_srbind_mup, 
-   k_srbind_mdown,
-   k_srbind_mback, 
-   k_srbind_maccept,
-   k_srbind_mopen,
-   k_srbind_mhub,
-   k_srbind_replay_play,
-   k_srbind_replay_freecam,
-   k_srbind_replay_resume,
-   k_srbind_world_left,
-   k_srbind_world_right,
-   k_srbind_home,
-   k_srbind_lobby,
-   k_srbind_chat,
-   k_srbind_run,
-   
-   k_srbind_miniworld_teleport,
-   k_srbind_miniworld_resume,
-   k_srbind_devbutton,
-   k_srbind_max,
-};
-
-enum sr_joystick{
-   k_srjoystick_steer = 0,
-   k_srjoystick_grab,
-   k_srjoystick_look,
-   k_srjoystick_max
-};
-
-enum sr_axis{
-   k_sraxis_grab = 0,
-   k_sraxis_mbrowse_h,
-   k_sraxis_mbrowse_v,
-   k_sraxis_replay_h,
-   k_sraxis_skid,
-   k_sraxis_max
-};
-
-
-#define INPUT_BASIC( KB, JS ) \
-   (vg_input_op[]){vg_keyboard, KB, vg_joy_button, JS, vg_end}
-
-static vg_input_op *input_button_list[] = {
-[k_srbind_jump]  = INPUT_BASIC( SDLK_SPACE, SDL_CONTROLLER_BUTTON_A ),
-[k_srbind_push]  = INPUT_BASIC( SDLK_w, SDL_CONTROLLER_BUTTON_B ),
-[k_srbind_trick0] = (vg_input_op[]){
-   vg_mouse, SDL_BUTTON_LEFT,
-   vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end
-},
-[k_srbind_trick1] = (vg_input_op[]){
-   vg_mouse, SDL_BUTTON_RIGHT,
-   vg_joy_button, SDL_CONTROLLER_BUTTON_B, vg_end
-},
-[k_srbind_trick2] = (vg_input_op[]){ 
-   vg_mouse, SDL_BUTTON_LEFT, vg_mode_mul, vg_mouse, SDL_BUTTON_RIGHT, 
-   vg_mode_absmax, vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end 
-},
-[k_srbind_use]   = INPUT_BASIC( SDLK_e, SDL_CONTROLLER_BUTTON_Y ),
-[k_srbind_reset] = INPUT_BASIC( SDLK_r, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
-[k_srbind_dead_respawn] =
-   INPUT_BASIC( SDLK_q, SDL_CONTROLLER_BUTTON_DPAD_UP ),
-[k_srbind_camera]= INPUT_BASIC( SDLK_c, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
-[k_srbind_mleft] = INPUT_BASIC( SDLK_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
-[k_srbind_mright]= INPUT_BASIC( SDLK_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
-[k_srbind_world_left] = 
-                   INPUT_BASIC( SDLK_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
-[k_srbind_world_right] =
-                   INPUT_BASIC( SDLK_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
-[k_srbind_mup]   = INPUT_BASIC( SDLK_UP, SDL_CONTROLLER_BUTTON_DPAD_UP ),
-[k_srbind_mdown] = INPUT_BASIC( SDLK_DOWN, SDL_CONTROLLER_BUTTON_DPAD_DOWN ),
-[k_srbind_mback] = INPUT_BASIC( SDLK_ESCAPE, SDL_CONTROLLER_BUTTON_B ),
-[k_srbind_mopen] = INPUT_BASIC( SDLK_ESCAPE, SDL_CONTROLLER_BUTTON_START ),
-[k_srbind_mhub]  = INPUT_BASIC( SDLK_h, SDL_CONTROLLER_BUTTON_Y ),
-[k_srbind_maccept] = (vg_input_op[]){
-   vg_keyboard, SDLK_e, vg_gui_visible, 0,
-      vg_keyboard, SDLK_RETURN, vg_keyboard, SDLK_RETURN2,
-   vg_gui_visible, 1,
-   vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end
-},
-[k_srbind_replay_play]    = INPUT_BASIC( SDLK_g, SDL_CONTROLLER_BUTTON_X ),
-[k_srbind_replay_resume]  = INPUT_BASIC( SDLK_SPACE, SDL_CONTROLLER_BUTTON_A ),
-[k_srbind_replay_freecam] = INPUT_BASIC( SDLK_f, SDL_CONTROLLER_BUTTON_Y ),
-[k_srbind_sit]   = INPUT_BASIC( SDLK_z, SDL_CONTROLLER_BUTTON_B ),
-[k_srbind_lobby] = INPUT_BASIC( SDLK_TAB, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
-[k_srbind_chat ] = (vg_input_op[]){ vg_keyboard, SDLK_y, vg_end },
-[k_srbind_run ] = (vg_input_op[]){ vg_keyboard, SDLK_LSHIFT, 
-   vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT, vg_end },
-
-[k_srbind_miniworld_resume]  = (vg_input_op[]){
-   vg_keyboard, SDLK_RETURN, vg_gui_visible, 0,
-   vg_keyboard, SDLK_RETURN2,
-   vg_gui_visible, 1,
-   vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end
-},
-[k_srbind_miniworld_teleport]= INPUT_BASIC( SDLK_q, 
-                                       SDL_CONTROLLER_BUTTON_LEFTSHOULDER ),
-[k_srbind_skid] = (vg_input_op[]){ vg_keyboard, SDLK_LCTRL, vg_end },
-[k_srbind_devbutton] = (vg_input_op[]){ vg_keyboard, SDLK_3, vg_end },
-[k_srbind_max]=NULL
-};
-
-static vg_input_op *input_axis_list[] = {
-[k_sraxis_grab] = (vg_input_op[]){
-   vg_keyboard, SDLK_LSHIFT, 
-   vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, vg_end
-},
-[k_sraxis_mbrowse_h] = (vg_input_op[]){
-   vg_mode_sub, vg_keyboard, SDLK_LEFT,
-   vg_mode_add, vg_keyboard, SDLK_RIGHT,
-   vg_mode_add, vg_joy_axis, SDL_CONTROLLER_AXIS_LEFTX,
-   vg_end
-},
-[k_sraxis_mbrowse_v] = (vg_input_op[]){
-   vg_mode_sub, vg_keyboard, SDLK_DOWN,
-   vg_mode_add, vg_keyboard, SDLK_UP,
-   vg_mode_sub, vg_joy_axis, SDL_CONTROLLER_AXIS_LEFTY,
-   vg_end
-},
-[k_sraxis_replay_h] = (vg_input_op[]){
-   vg_mode_sub, vg_keyboard, SDLK_q,
-   vg_mode_add, vg_keyboard, SDLK_e,
-   vg_mode_sub, vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT,
-   vg_mode_add, vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
-   vg_end
-},
-[k_sraxis_skid] = (vg_input_op[]){
-   vg_mode_sub, vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
-   vg_mode_add, vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
-   vg_end
-},
-[k_sraxis_max]=NULL
-};
-
-static vg_input_op *input_joy_list[] = {
-[k_srjoystick_steer] = (vg_input_op[]){
-   vg_index, 0, vg_mode_sub, vg_keyboard, SDLK_a,
-                vg_mode_add, vg_keyboard, SDLK_d,
-   vg_index, 1, vg_mode_sub, vg_keyboard, SDLK_w,
-                vg_mode_add, vg_keyboard, SDLK_s,
-                vg_mode_absmax, vg_joy_ls,
-   vg_end
-},
-[k_srjoystick_grab] = (vg_input_op[]){
-   vg_joy_rs, vg_end
-},
-[k_srjoystick_look] = (vg_input_op[]){
-   vg_joy_rs, vg_end
-},
-[k_srjoystick_max]=NULL
-};
-
-struct {
-   float axis_states[ k_sraxis_max ][2];
-   v2f joystick_states[ k_srjoystick_max ][2];
-   u8 button_states[ k_srbind_max ][2];
-
-   enum input_state {
-      k_input_state_enabled,
-      k_input_state_resume,
-      k_input_state_resuming,
-      k_input_state_pause
-   }
-   state;
-}
-static srinput;
-
-static int input_filter_generic(void){
-   if( (srinput.state != k_input_state_enabled) || vg_console.enabled ||
-       (workshop_form.page != k_workshop_form_hidden)  )
-      return 1;
-   else 
-      return 0;
-}
-
-static int buttons_filter_fixed(void){
-   if( input_filter_generic() ) 
-      return 1;
-
-   if( vg.engine_stage == k_engine_stage_update_fixed )
-      if( vg.fixed_iterations > 0 )
-         return 1;
-
-   return 0;
-}
-
-/* Rising edge of button */
-static int button_down( enum sr_bind button ){
-   if( buttons_filter_fixed() ) return 0;
-   
-   if(  srinput.button_states[ button ][0] && 
-       !srinput.button_states[ button ][1] )
-      return 1;
-   else
-      return 0;
-}
-
-/* Falling edge of button */
-static int button_up( enum sr_bind button ){
-   if( buttons_filter_fixed() ) return 0;
-   
-   if( !srinput.button_states[ button ][0] && 
-        srinput.button_states[ button ][1] )
-      return 1;
-   else
-      return 0;
-}
-
-/* State of button */
-static int button_press( enum sr_bind button ){
-   if( input_filter_generic() )
-      return 0;
-   return 
-      srinput.button_states[ button ][0];
-}
-
-static void joystick_state( enum sr_joystick joystick, v2f state ){
-   if( input_filter_generic() )
-      v2_zero( state );
-   else
-      v2_copy( srinput.joystick_states[ joystick ][0], state );
-}
-
-static float axis_state( enum sr_axis axis ){
-   if( input_filter_generic() )
-      return 0.0f;
-   else 
-      return srinput.axis_states[axis][0];
-}
-
-static void skaterift_preupdate_inputs(void){
-   if( srinput.state == k_input_state_resuming )
-      srinput.state = k_input_state_enabled;
-
-   if( srinput.state == k_input_state_resume )
-      srinput.state = k_input_state_resuming;
-
-   for( u32 i=0; i<k_srbind_max; i++ ){
-      srinput.button_states[i][1] = srinput.button_states[i][0];
-      srinput.button_states[i][0] = 0;
-   }
-
-   for( u32 i=0; i<k_srjoystick_max; i++ ){
-      v2_copy( srinput.joystick_states[i][0], srinput.joystick_states[i][1] );
-      v2_zero( srinput.joystick_states[i][0] );
-   }
-
-   for( u32 i=0; i<k_sraxis_max; i++ ){
-      srinput.axis_states[i][1] = srinput.axis_states[i][0];
-      srinput.axis_states[i][0] = 0.0f;
-   }
-
-   for( int i=0; i<k_srbind_max; i++ ){
-      vg_input_op *prog = input_button_list[i];
-      if( prog ){
-         vg_exec_input_program( k_vg_input_type_button_u8, prog,
-                                &srinput.button_states[i][0] );
-      }
-   }
-
-   for( int i=0; i<k_sraxis_max; i++ ){
-      vg_input_op *prog = input_axis_list[i];
-      if( prog ){
-         vg_exec_input_program( k_vg_input_type_axis_f32, prog,
-                                &srinput.axis_states[i][0] );
-      }
-   }
-
-   for( int i=0; i<k_srjoystick_max; i++ ){
-      vg_input_op *prog = input_joy_list[i];
-      if( prog ){
-         vg_exec_input_program( k_vg_input_type_joy_v2f, prog,
-                                srinput.joystick_states[i][0] );
-      }
-   }
-
-   f32 x = srinput.axis_states[k_sraxis_mbrowse_h][0],
-       y = srinput.axis_states[k_sraxis_mbrowse_v][0],
-       sensitivity = 0.35f;
-
-   if( fabsf(x) > sensitivity ){
-      if( x > 0.0f ) srinput.button_states[k_srbind_mright][0] = 1;
-      else           srinput.button_states[k_srbind_mleft][0] = 1;
-   }
-
-   if( fabsf(y) > sensitivity ){
-      if( y > 0.0f ) srinput.button_states[k_srbind_mup][0] = 1;
-      else           srinput.button_states[k_srbind_mdown][0] = 1;
-   }
-}
diff --git a/menu.c b/menu.c
deleted file mode 100644 (file)
index 1f2b9c8..0000000
--- a/menu.c
+++ /dev/null
@@ -1,1095 +0,0 @@
-#pragma once
-#include "skaterift.h"
-#include "menu.h"
-#include "model.h"
-#include "entity.h"
-#include "input.h"
-#include "world_map.h"
-#include "ent_miniworld.h"
-#include "audio.h"
-#include "workshop.h"
-#include "gui.h"
-#include "control_overlay.h"
-#include "network.h"
-#include "shaders/model_menu.h"
-
-struct global_menu menu = { .skip_starter = 0 };
-
-void menu_at_begin(void)
-{
-   if( menu.skip_starter ) return;
-
-   skaterift.activity = k_skaterift_menu;
-   menu.page = k_menu_page_starter;
-}
-
-void menu_init(void)
-{
-   vg_console_reg_var( "skip_starter_menu", &menu.skip_starter,
-                       k_var_dtype_i32, VG_VAR_PERSISTENT );
-   vg_tex2d_load_qoi_async_file( "textures/prem.qoi", 
-         VG_TEX2D_CLAMP|VG_TEX2D_NOMIP|VG_TEX2D_NEAREST,
-         &menu.prem_tex );
-}
-
-void menu_open( enum menu_page page )
-{
-   skaterift.activity = k_skaterift_menu;
-
-   if( page != k_menu_page_any )
-   {
-      menu.page = page;
-   }
-}
-
-bool menu_viewing_map(void)
-{
-   return (skaterift.activity == k_skaterift_menu) && 
-          (menu.page == k_menu_page_main) &&
-          (menu.main_index == k_menu_main_map);
-}
-
-static void menu_decor_select( ui_context *ctx, ui_rect rect )
-{
-   ui_px b = ctx->font->sx, hb = b/2;
-   ui_rect a0 = { rect[0] - 20 - hb,           rect[1] + rect[3]/2 - hb, b,b },
-           a1 = { rect[0] + rect[2] + 20 + hb, rect[1] + rect[3]/2 - hb, b,b };
-
-   ui_text( ctx, a0, "\x95", 1, k_ui_align_middle_center, 0 );
-   ui_text( ctx, a1, "\x93", 1, k_ui_align_middle_center, 0 );
-}
-
-static void menu_standard_widget( ui_context *ctx,
-                                  ui_rect inout_panel, ui_rect rect, ui_px s )
-{
-   ui_split( inout_panel, k_ui_axis_h, ctx->font->sy*s*2, 
-             8, rect, inout_panel );
-}
-
-static bool menu_slider( ui_context *ctx,
-                         ui_rect inout_panel, bool select, const char *label,
-                         const f32 disp_min, const f32 disp_max, f32 *value, 
-                         const char *format )
-{
-   ui_rect rect, box;
-   menu_standard_widget( ctx, inout_panel, rect, 1 );
-   ui_label( ctx, rect, label, 1, 8, box );
-
-   f32 t;
-   enum ui_button_state state = ui_slider_base( ctx, box, 0, 1, value, &t ),
-         mask_using = 
-         k_ui_button_holding_inside |
-         k_ui_button_holding_outside |
-         k_ui_button_click,
-      mask_brighter = mask_using | k_ui_button_hover;
-
-   if( vg_input.display_input_method == k_input_method_controller )
-   {
-      if( select )
-      {
-         f32 m = axis_state( k_sraxis_mbrowse_h );
-         if( fabsf(m) > 0.5f )
-         {
-            *value += m * vg.time_frame_delta * (1.0f/2.0f);
-            *value = vg_clampf( *value, 0, 1 );
-         }
-
-         menu_decor_select( ctx, rect );
-         state |= k_ui_button_hover;
-      }
-      else
-         state = k_ui_button_none;
-   }
-
-   ui_rect line = { box[0], box[1], t * (f32)box[2], box[3] };
-   ui_fill( ctx, line, state&mask_brighter? GUI_COL_ACTIVE: GUI_COL_NORM );
-   ui_fill( ctx, (ui_rect){ box[0]+line[2], box[1], box[2]-line[2], box[3] },
-               GUI_COL_DARK );
-
-   ui_outline( ctx, box, 1, state? GUI_COL_HI: GUI_COL_ACTIVE, 0 );
-   ui_slider_text( ctx, box, 
-                   format, disp_min + (*value)*( disp_max-disp_min ) );
-
-   return (state & mask_using) && 1;
-}
-
-static bool menu_button( ui_context *ctx,
-                         ui_rect inout_panel, bool select, const char *text )
-{
-   ui_rect rect;
-   menu_standard_widget( ctx, inout_panel, rect, 1 );
-
-   enum ui_button_state state = k_ui_button_none;
-
-   if( vg_input.display_input_method == k_input_method_controller )
-   {
-      if( select )
-      {
-         menu_decor_select( ctx, rect );
-
-         if( button_down( k_srbind_maccept ) )
-            state = k_ui_button_click;
-      }
-   }
-   else
-   {
-      state = ui_button_base( ctx, rect );
-      select = 0;
-   }
-
-   if( state == k_ui_button_click )
-   {
-      ui_fill( ctx, rect, GUI_COL_DARK );
-   }
-   else if( state == k_ui_button_holding_inside )
-   {
-      ui_fill( ctx, rect, GUI_COL_DARK );
-   }
-   else if( state == k_ui_button_holding_outside )
-   {
-      ui_fill( ctx, rect, GUI_COL_DARK );
-      ui_outline( ctx, rect, 1, GUI_COL_CLICK, 0 );
-   }
-   else if( state == k_ui_button_hover )
-   {
-      ui_fill( ctx, rect, GUI_COL_ACTIVE );
-      ui_outline( ctx, rect, 1, GUI_COL_CLICK, 0 );
-   }
-   else 
-   {
-      ui_fill( ctx, rect, select? GUI_COL_ACTIVE: GUI_COL_NORM );
-      if( select )
-         ui_outline( ctx, rect, 1, GUI_COL_HI, 0 );
-   }
-
-   ui_text( ctx, rect, text, 1, k_ui_align_middle_center, 0 );
-
-   if( state == k_ui_button_click )
-   {
-      audio_lock();
-      audio_oneshot( &audio_ui[0], 1.0f, 0.0f );
-      audio_unlock();
-      return 1;
-   }
-   else return 0;
-}
-
-static bool menu_checkbox( ui_context *ctx, ui_rect inout_panel, bool select, 
-                           const char *str_label, i32 *data )
-{
-   ui_rect rect, label, box;
-   menu_standard_widget( ctx, inout_panel, rect, 1 );
-
-   ui_split( rect, k_ui_axis_v, -rect[3], 0, label, box );
-   ui_text( ctx, label, str_label, ctx->scale, k_ui_align_middle_left, 0 );
-   
-   enum ui_button_state state = k_ui_button_none;
-
-   if( vg_input.display_input_method == k_input_method_controller )
-   {
-      if( select )
-      {
-         menu_decor_select( ctx, rect );
-
-         if( button_down( k_srbind_maccept ) )
-         {
-            *data = (*data) ^ 0x1;
-            state = k_ui_button_click;
-         }
-      }
-   }
-   else
-   {
-      state = ui_checkbox_base( ctx, box, data );
-      select = 0;
-   }
-
-   if( state == k_ui_button_holding_inside )
-   {
-      ui_fill( ctx, box, GUI_COL_ACTIVE );
-      ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
-   }
-   else if( state == k_ui_button_holding_outside )
-   {
-      ui_fill( ctx, box, GUI_COL_DARK );
-      ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
-   }
-   else if( state == k_ui_button_hover )
-   {
-      ui_fill( ctx, box, GUI_COL_ACTIVE );
-      ui_outline( ctx, box, 1, GUI_COL_HI, 0 );
-   }
-   else 
-   {
-      ui_fill( ctx, box, select? GUI_COL_ACTIVE: GUI_COL_DARK );
-      ui_outline( ctx, box, 1, select? GUI_COL_HI: GUI_COL_NORM, 0 );
-   }
-
-   if( *data )
-   {
-      ui_rect_pad( box, (ui_px[2]){8,8} );
-      ui_fill( ctx, box, GUI_COL_HI );
-   }
-
-   if( state == k_ui_button_click )
-   {
-      audio_lock();
-      audio_oneshot( &audio_ui[0], 1.0f, 0.0f );
-      audio_unlock();
-      return 1;
-   }
-   else return 0;
-}
-
-static void menu_heading( ui_context *ctx,
-                          ui_rect inout_panel, const char *label, u32 colour )
-{
-   ui_rect rect;
-   menu_standard_widget( ctx, inout_panel, rect, 1 );
-
-   rect[0] -= 8;
-   rect[2] += 16;
-
-   u32 c0 = ui_opacity( GUI_COL_DARK, 0.36f ),
-       c1 = ui_opacity( GUI_COL_DARK, 0.5f );
-
-   struct ui_vert *vs = ui_fill( ctx, rect, c0 );
-
-   vs[0].colour = c1;
-   vs[1].colour = c1;
-
-   rect[1] += 4;
-   ui_text( ctx, rect, label, 1, k_ui_align_middle_center, 1 );
-   rect[0] += 1;
-   rect[1] -= 1;
-   ui_text( ctx, rect, label, 1, k_ui_align_middle_center, colour? colour:
-            ui_colour(ctx, k_ui_blue+k_ui_brighter) );
-}
-
-static u32 medal_colour( ui_context *ctx, u32 flags )
-{
-   if( flags & k_ent_route_flag_achieve_gold ) 
-      return ui_colour( ctx, k_ui_yellow );
-   else if( flags & k_ent_route_flag_achieve_silver )
-      return ui_colour( ctx, k_ui_fg );
-   else return 0;
-}
-
-static i32 menu_nav( i32 *p_row, int mv, i32 max )
-{
-   i32 row_prev = *p_row;
-
-   if( mv < 0 ) *p_row = vg_min( max, (*p_row) +1 );
-   if( mv > 0 ) *p_row = vg_max( 0,   (*p_row) -1 );
-
-   if( *p_row != row_prev )
-   {
-      audio_lock();
-      audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-      audio_unlock();
-   }
-
-   return *p_row;
-}
-
-static void menu_try_find_cam( i32 id )
-{
-   world_instance *world = &world_static.instances[0];
-   for( u32 i=0; i<mdl_arrcount(&world->ent_npc); i ++ )
-   {
-      ent_npc *fnpc = mdl_arritm( &world->ent_npc, i );
-      if( (fnpc->id == 50) && (fnpc->context == id) )
-      {
-         if( mdl_entity_id_type(fnpc->camera) == k_ent_camera )
-         {
-            u32 index = mdl_entity_id_id( fnpc->camera );
-            menu.bg_cam = mdl_arritm( &world->ent_camera, index );
-            menu.bg_blur = 0;
-         }
-      }
-   }
-}
-
-static void menu_link_modal( const char *url )
-{
-   menu.web_link = url;
-
-   /* Only reset the choice of browser if 'No' was selected. */
-   if( menu.web_choice == 2 )
-      menu.web_choice = 0;
-}
-
-void menu_gui( ui_context *ctx )
-{
-   if( button_down( k_srbind_mopen ) )
-   {
-      if( skaterift.activity == k_skaterift_default )
-      {
-         menu_open( k_menu_page_main );
-         return;
-      }
-   }
-
-   if( skaterift.activity != k_skaterift_menu ) 
-      return;
-
-   menu.bg_blur = 1;
-   menu.bg_cam = NULL;
-
-   /* get buttons inputs
-    * -------------------------------------------------------------------*/
-   int ml = button_press( k_srbind_mleft ),
-       mr = button_press( k_srbind_mright ),
-       mu = button_press( k_srbind_mup ),
-       md = button_press( k_srbind_mdown ),
-       mh = ml-mr,
-       mv = mu-md,
-       enter = button_down( k_srbind_maccept );
-
-   /* TAB CONTROL */
-   u8 lb_down = 0, rb_down = 0;
-   vg_exec_input_program( k_vg_input_type_button_u8, 
-         (vg_input_op[]){
-            vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, vg_end 
-         }, &rb_down );
-   vg_exec_input_program( k_vg_input_type_button_u8, 
-         (vg_input_op[]){
-            vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, vg_end 
-         }, &lb_down );
-
-   int mtab = (i32)rb_down - (i32)lb_down;
-
-   if( menu.repeater > 0.0f )
-   {
-      menu.repeater -= vg_minf( vg.time_frame_delta, 0.5f );
-      mv = 0;
-      mh = 0;
-      mtab = 0;
-   }
-   else
-   {
-      if( mv || mh || mtab )
-         menu.repeater += 0.2f;
-   }
-
-   if( vg_input.display_input_method == k_input_method_kbm )
-   {
-      ui_capture_mouse(ctx, 1);
-   }
-
-   if( skaterift.activity != k_skaterift_menu ) return;
-
-
-   if( menu.web_link )
-   {
-      menu_try_find_cam( 3 );
-
-      ui_rect panel = { 0,0, 800, 200 },
-              screen = { 0,0, vg.window_x,vg.window_y };
-      ui_rect_center( screen, panel );
-      ui_fill( ctx, panel, GUI_COL_DARK );
-      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
-      ui_rect_pad( panel, (ui_px[]){8,8} );
-
-      ui_rect title;
-      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
-      ctx->font = &vgf_default_title;
-      ui_text( ctx, title, "Open Link?", 1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
-      ctx->font = &vgf_default_large;
-      ui_text( ctx, title, menu.web_link, 1, k_ui_align_middle_center, 0 );
-
-      ui_rect end = { panel[0], panel[1] + panel[3] - 48, panel[2], 48 };
-
-      ui_rect a,b,c;
-      ui_split_ratio( end, k_ui_axis_v, 2.0/3.0, 2, a, c );
-      ui_split_ratio( a, k_ui_axis_v, 1.0/2.0, 2, a, b );
-
-      i32 R = menu_nav( &menu.web_choice, mh, 2 );
-
-      if( menu_button( ctx, a, R==0, "Steam Overlay" ) )
-      {
-         if( steam_ready )
-         {
-            ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
-            SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage( hSteamFriends,
-                  menu.web_link,
-                  k_EActivateGameOverlayToWebPageMode_Default );
-            menu.web_link = NULL;
-         }
-      }
-
-      if( menu_button( ctx, b, R==1, "Web Browser" ) )
-      {
-         char buf[512];
-         vg_str str;
-         vg_strnull( &str, buf, sizeof(buf) );
-#ifdef _WIN32
-         vg_strcat( &str, "start " );
-#else
-         vg_strcat( &str, "xdg-open " );
-#endif
-         vg_strcat( &str, menu.web_link );
-
-         if( vg_strgood(&str) )
-            system( buf );
-
-         menu.web_link = NULL;
-      }
-      
-      if( menu_button( ctx, c, R==2, "No" ) || button_down( k_srbind_mback ) )
-      {
-         audio_lock();
-         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-         audio_unlock();
-         menu.web_link = NULL;
-      }
-
-      goto menu_draw;
-   }
-
-
-   if( vg.settings_open )
-   {
-      if( button_down( k_srbind_mback ) )
-      {
-         audio_lock();
-         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-         audio_unlock();
-         vg_settings_close();
-         srinput.state = k_input_state_resume;
-      }
-
-      return;
-   }
-
-   if( menu.page == k_menu_page_credits )
-   {
-      ui_rect panel = { 0,0, 600, 400 },
-              screen = { 0,0, vg.window_x,vg.window_y };
-      ui_rect_center( screen, panel );
-      ui_fill( ctx, panel, GUI_COL_DARK );
-      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
-      ui_rect_pad( panel, (ui_px[]){8,8} );
-
-      ui_rect title;
-      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
-      ctx->font = &vgf_default_title;
-      ui_text( ctx, title, "Skate Rift - Credits", 
-               1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
-      ctx->font = &vgf_default_large;
-      ui_text( ctx, title, 
-               "Mt.Zero Software", 1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 8, 0, title, panel );
-      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
-      ctx->font = &vgf_default_title;
-      ui_text( ctx, title, "Free Software", 1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 8, 0, title, panel );
-      ctx->font = &vgf_default_large;
-      ui_text( ctx, panel, 
-            "Sam Lantinga       - SDL2 - libsdl.org\n"
-            "Hunter WB          - Anyascii\n"
-            "David Herberth     - GLAD\n"
-            "Dominic Szablewski - QOI - qoiformat.org\n"
-            "Sean Barrett       - stb_image, stb_vorbis,\n"
-            "                     stb_include\n"
-            "Khronos Group      - OpenGL\n"
-            , 1, k_ui_align_left, 0 );
-
-      ui_rect end = { panel[0], panel[1] + panel[3] - 64, panel[2], 64 };
-
-      if( menu_button( ctx, end, 1, "Back" ) || button_down( k_srbind_mback ) )
-      {
-         menu.page = k_menu_page_main;
-      }
-
-      goto menu_draw;
-   }
-   else if( menu.page == k_menu_page_starter )
-   {
-      i32 R = menu_nav( &menu.intro_row, mv, 3 );
-      ui_rect panel = { 0,0, 600, 400 },
-              screen = { 0,0, vg.window_x,vg.window_y };
-      ui_rect_center( screen, panel );
-      ui_fill( ctx, panel, ui_opacity( GUI_COL_DARK, 0.35f ) );
-      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
-      ui_rect_pad( panel, (ui_px[]){8,8} );
-
-      ui_rect title;
-      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
-      ctx->font = &vgf_default_title;
-      ui_text( ctx, title, 
-               "Welcome to Skate Rift", 1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
-      ctx->font = &vgf_default_large;
-
-      menu_checkbox( ctx, panel, R == 0, 
-            "Show controls overlay (good for new players)",
-            &control_overlay.enabled );
-      menu_checkbox( ctx, panel, R == 1, "Auto connect to global server",
-            &network_client.auto_connect );
-
-      ui_rect end = { panel[0], panel[1] + panel[3] - 100, panel[2], 100 };
-      menu_checkbox( ctx, end, R == 2, 
-                     "Don't show this again", &menu.skip_starter );
-      if( menu_button( ctx, end, R == 3, "OK" ) )
-      {
-         menu.page = k_menu_page_main;
-         skaterift.activity = k_skaterift_default;
-      }
-
-      menu_try_find_cam( 3 );
-      goto menu_draw;
-   }
-   else if( menu.page == k_menu_page_premium )
-   {
-      i32 R = menu_nav( &menu.prem_row, mh, 1 );
-      ui_rect panel = { 0,0, 600, 400+240 },
-              screen = { 0,0, vg.window_x,vg.window_y };
-      ui_rect_center( screen, panel );
-      ui_fill( ctx, panel, ui_opacity( GUI_COL_DARK, 0.35f ) );
-      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
-      ui_rect_pad( panel, (ui_px[]){8,8} );
-
-      ui_rect title;
-      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
-      ctx->font = &vgf_default_title;
-      ui_text( ctx, title, "Content is in the full game.", 
-               1, k_ui_align_middle_center, 0 );
-
-      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
-      ctx->font = &vgf_default_large;
-
-      ui_rect img;
-      ui_split( panel, k_ui_axis_h, 456, 0, img, panel );
-      ui_image( ctx, img, &menu.prem_tex );
-
-      ui_rect end = { panel[0], panel[1] + panel[3] - 48, panel[2], 48 }, a,b;
-      ui_split_ratio( end, k_ui_axis_v, 0.5f, 2, a, b );
-
-      if( menu_button( ctx, a, R == 0, "Store Page" ) )
-      {
-         if( steam_ready )
-            SteamAPI_ISteamFriends_ActivateGameOverlayToStore( 
-                  SteamAPI_SteamFriends(), 2103940, k_EOverlayToStoreFlag_None);
-      }
-
-      if( menu_button( ctx, b, R == 1, "Nah" ) || button_down( k_srbind_mback ) )
-      {
-         audio_lock();
-         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-         audio_unlock();
-         skaterift.activity = k_skaterift_default;
-         return;
-      }
-
-      goto menu_draw;
-   }
-
-   /*                              TOP BAR 
-    * -------------------------------------------------------------------*/
-
-   ctx->font = &vgf_default_title;
-   ui_px height = ctx->font->ch + 16;
-   ui_rect topbar = { 0, 0, vg.window_x, height };
-
-   const char *opts[] = {
-      [k_menu_main_main] = "Menu",
-      [k_menu_main_map]  = "Map",
-      [k_menu_main_settings ] = "Settings",
-      [k_menu_main_guide ] = "Guides"
-   };
-
-   if( mtab )
-   {
-      menu.main_index += mtab;
-
-      if( menu.main_index == -1 )
-         menu.main_index ++;
-
-      if( menu.main_index == VG_ARRAY_LEN(opts) )
-         menu.main_index --;
-
-      audio_lock();
-      audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-      audio_unlock();
-   }
-
-   ui_px x = 0, spacer = 8;
-   for( u32 draw=0; draw<2; draw ++ )
-   {
-      if( vg_input.display_input_method == k_input_method_controller )
-      {
-         if( draw )
-         {
-            ui_rect inf_lb = { x, 0, 32, height };
-            ui_fill( ctx, inf_lb, lb_down? GUI_COL_NORM: GUI_COL_NORM );
-            ui_text( ctx, inf_lb, "\x91", 1, k_ui_align_middle_center, 0 );
-         }
-         x += 32 + spacer;
-      }
-
-      for( i32 i=0; i<VG_ARRAY_LEN(opts); i ++ )
-      {
-         ui_rect box = { x, 0, ui_text_line_width(ctx, opts[i]) + 32, height };
-
-         if( draw )
-         {
-            enum ui_button_state state = ui_button_base( ctx, box );
-            if( state == k_ui_button_click )
-            {
-               ui_fill( ctx, box, GUI_COL_DARK );
-               menu.main_index = i;
-            }
-            else if( state == k_ui_button_holding_inside )
-            {
-               ui_fill( ctx, box, GUI_COL_DARK );
-            }
-            else if( state == k_ui_button_holding_outside )
-            {
-               ui_fill( ctx, box, GUI_COL_DARK );
-               ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
-            }
-            else if( state == k_ui_button_hover )
-            {
-               ui_fill( ctx, box, GUI_COL_NORM );
-               ui_outline( ctx, box, 1, GUI_COL_ACTIVE, 0 );
-            }
-            else 
-               ui_fill( ctx, box, 
-                        i==menu.main_index? GUI_COL_ACTIVE: GUI_COL_NORM );
-
-            ui_text( ctx, box, opts[i], 1, k_ui_align_middle_center, 0 );
-         }
-
-         x += box[2] + spacer;
-      }
-
-      if( vg_input.display_input_method == k_input_method_controller )
-      {
-         if( draw )
-         {
-            ui_rect inf_rb = { x, 0, 32, height };
-            ui_fill( ctx, inf_rb, rb_down? GUI_COL_NORM: GUI_COL_NORM );
-            ui_text( ctx, inf_rb, "\x92", 1, k_ui_align_middle_center, 0 );
-         }
-         x += 32;
-      }
-
-      if( draw )
-         ui_fill( ctx,
-                  (ui_rect){ x+8,0, vg.window_x-(x+8),height }, GUI_COL_NORM );
-      else
-      {
-         x = vg.window_x/2 - x/2;
-         ui_fill( ctx,
-                  (ui_rect){ 0, 0, x-8, height }, GUI_COL_NORM );
-      }
-   }
-
-   if( menu.main_index == k_menu_main_map )
-   {
-      menu.bg_blur = 0;
-      ctx->font = &vgf_default_large;
-      ui_rect title = { vg.window_x/2- 512/2, height+8, 512, 64 };
-
-      ui_px x = 8,
-            y = height+8;
-
-      struct ui_vert *vs = 
-         ui_fill( ctx, (ui_rect){ x,y, 0,height }, 
-               world_static.active_instance? GUI_COL_DARK: GUI_COL_ACTIVE );
-
-      char buf[64];
-      vg_str str;
-      vg_strnull( &str, buf, sizeof(buf) );
-      vg_strcat( &str, "Hub World" );
-      
-      if( world_static.active_instance )
-      {
-         vg_strcat( &str, " (" );
-         vg_input_string( &str, input_button_list[k_srbind_mhub], 1 );
-         vg_strcatch( &str, '\x06' );
-         vg_strcatch( &str, '\x00' );
-         vg_strcat( &str, "\x1B[0m)" );
-      }
-      
-      ui_px w = ui_text( ctx, (ui_rect){ x+8, y, 1000, height }, buf, 1,
-                         k_ui_align_middle_left, 0 );
-      w *= ctx->font->sx;
-      x += w + 16;
-
-      vs[1].co[0] = x + 8;
-      vs[2].co[0] = x;
-
-      x += 2;
-
-      world_instance *world = &world_static.instances[1];
-      if( world->status == k_world_status_loaded )
-      {
-         const char *world_name = 
-            mdl_pstr( &world->meta, world->info.pstr_name );
-
-         vg_strnull( &str, buf, sizeof(buf) );
-         vg_strcat( &str, world_name );
-
-         if( !world_static.active_instance &&
-               (vg_input.display_input_method == k_input_method_controller) )
-         {
-            vg_strcat( &str, " (" );
-            vg_input_string( &str, input_button_list[k_srbind_mhub], 1 );
-            vg_strcatch( &str, '\x06' );
-            vg_strcatch( &str, '\x00' );
-            vg_strcat( &str, "\x1B[0m)" );
-         }
-
-         vs = ui_fill( ctx, (ui_rect){ x,y, 1000,height }, 
-               world_static.active_instance? GUI_COL_ACTIVE: GUI_COL_DARK );
-         w = ui_text( ctx, (ui_rect){ x+16,y, 1000,height }, buf, 
-                      1, k_ui_align_middle_left, 0 );
-
-         w = w*ctx->font->sx + 8*3;
-         x += w;
-
-         if( button_down( k_srbind_mhub ) ||
-            ui_button_base( ctx, (ui_rect){0,y,x,height} ) == k_ui_button_click )
-         {
-            world_switch_instance( world_static.active_instance ^ 0x1 );
-            skaterift.activity = k_skaterift_default;
-            world_map.view_ready = 0;
-         }
-
-         vs[0].co[0] += 8;
-         vs[1].co[0] = x + 8;
-         vs[2].co[0] = x;
-      }
-
-      x = 8;
-      y += 8 + height;
-
-      if( world_static.active_instance )
-      {
-         ui_rect stat_panel = { x,y, 256,vg.window_y-y };
-         u32 c0 = ui_opacity( GUI_COL_DARK, 0.36f );
-         struct ui_vert *vs = ui_fill( ctx, stat_panel, c0 );
-
-         ui_rect_pad( stat_panel, (ui_px[2]){8,0} );
-
-         for( u32 i=0; i<mdl_arrcount( &world->ent_region ); i ++ )
-         {
-            ent_region *region = mdl_arritm( &world->ent_region, i );
-
-            if( !region->zone_volume )
-               continue;
-
-            const char *title = mdl_pstr( &world->meta, region->pstr_title );
-            ctx->font = &vgf_default_large;
-
-            ui_rect title_box;
-            menu_standard_widget( ctx, stat_panel, title_box, 1 );
-            
-            stat_panel[0] += 16;
-            stat_panel[2] -= 16;
-            ctx->font = &vgf_default_small;
-
-            ent_volume *volume = mdl_arritm(&world->ent_volume,
-                  mdl_entity_id_id(region->zone_volume));
-
-            u32 combined = k_ent_route_flag_achieve_gold | 
-                           k_ent_route_flag_achieve_silver;
-
-            char buf[128];
-            vg_str str;
-
-            for( u32 j=0; j<mdl_arrcount(&world->ent_route); j ++ )
-            {
-               ent_route *route = mdl_arritm(&world->ent_route, j );
-
-               v3f local;
-               m4x3_mulv( volume->to_local, route->board_transform[3], local );
-               if( !((fabsf(local[0]) <= 1.0f) &&
-                     (fabsf(local[1]) <= 1.0f) &&
-                     (fabsf(local[2]) <= 1.0f)) )
-               {
-                  continue;
-               }
-
-               combined &= route->flags;
-
-               vg_strnull( &str, buf, sizeof(buf) );
-               vg_strcat( &str, "(Race) " );
-               vg_strcat( &str, mdl_pstr(&world->meta, route->pstr_name));
-
-               if( route->flags & k_ent_route_flag_achieve_silver )
-                  vg_strcat( &str, " \xb3");
-               if( route->flags & k_ent_route_flag_achieve_gold )
-                  vg_strcat( &str, "\xb3");
-
-               ui_rect r;
-               ui_standard_widget( ctx, stat_panel, r, 1 );
-               ui_text( ctx, r, buf, 1, k_ui_align_middle_left, 
-                        medal_colour( ctx, route->flags ) );
-            }
-
-            for( u32 j=0; j<mdl_arrcount(&world->ent_challenge); j ++ )
-            {
-               ent_challenge *challenge = mdl_arritm( &world->ent_challenge, j );
-
-               v3f local;
-               m4x3_mulv( volume->to_local, challenge->transform.co, local );
-               if( !((fabsf(local[0]) <= 1.0f) &&
-                     (fabsf(local[1]) <= 1.0f) &&
-                     (fabsf(local[2]) <= 1.0f)) )
-               {
-                  continue;
-               }
-
-               vg_strnull( &str, buf, sizeof(buf) );
-               vg_strcat( &str, mdl_pstr(&world->meta, challenge->pstr_alias));
-
-               u32 flags = 0x00;
-               if( challenge->status )
-               {
-                  flags |= k_ent_route_flag_achieve_gold;
-                  flags |= k_ent_route_flag_achieve_silver;
-                  vg_strcat( &str, " \xb3\xb3" );
-               }
-
-               combined &= flags;
-
-               ui_rect r;
-               ui_standard_widget( ctx, stat_panel, r, 1 );
-               ui_text( ctx, r, buf, 1, 
-                        k_ui_align_middle_left, medal_colour( ctx, flags ) );
-            }
-
-            stat_panel[0] -= 16;
-            stat_panel[2] += 16;
-
-            u32 title_col = 0;
-            if( combined & k_ent_route_flag_achieve_gold ) 
-               title_col = ui_colour( ctx, k_ui_yellow );
-            else if( combined & k_ent_route_flag_achieve_silver )
-               title_col = ui_colour( ctx, k_ui_fg );
-
-            menu_heading( ctx, title_box, title, title_col );
-         }
-
-         vs[2].co[1] = stat_panel[1];
-         vs[3].co[1] = stat_panel[1];
-      }
-   }
-   else
-   {
-      if( button_down( k_srbind_mback ) )
-      {
-         audio_lock();
-         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
-         audio_unlock();
-         skaterift.activity = k_skaterift_default;
-         return;
-      }
-
-      ui_rect list0 = { vg.window_x/2 - 512/2, height+32, 
-                        512, vg.window_y-height-64 }, list;
-      rect_copy( list0, list );
-      ui_rect_pad( list, (ui_px[2]){8,8} );
-
-      /*                              MAIN / MAIN
-       * -------------------------------------------------------------------*/
-
-      if( menu.main_index == k_menu_main_main )
-      {
-         i32 R = menu_nav( &menu.main_row, mv, 2 );
-
-         if( menu_button( ctx, list, R == 0, "Resume" ) )
-         {
-            skaterift.activity = k_skaterift_default;
-            return;
-         }
-
-         if( menu_button( ctx, list, R == 1, "Credits" ) )
-         {
-            menu.page = k_menu_page_credits;
-         }
-
-         ui_rect end = { list[0], list[1]+list[3]-64, list[2], 72 };
-         if( menu_button( ctx, end, R == 2, "Quit Game" ) )
-         {
-            vg.window_should_close = 1;
-         }
-      }
-      else if( menu.main_index == k_menu_main_settings )
-      {
-         ui_fill( ctx, list0, ui_opacity( GUI_COL_DARK, 0.36f ) );
-         ui_outline( ctx, list0, 1, GUI_COL_NORM, 0 );
-         i32 R = menu_nav( &menu.settings_row, mv, 8 );
-
-         ctx->font = &vgf_default_large;
-         list[1] -= 8;
-         menu_heading( ctx, list, "Game", 0 );
-         menu_checkbox( ctx, list, R == 0, "Show controls overlay",
-               &control_overlay.enabled );
-         menu_checkbox( ctx, list, R == 1, "Auto connect to global server",
-               &network_client.auto_connect );
-
-         menu_heading( ctx, list, "Audio/Video", 0 );
-         menu_slider( ctx, list, R == 2, "Volume", 0, 100, 
-                      &vg_audio.external_global_volume, "%.f%%" );
-         menu_slider( ctx, list, R == 3, "Resolution", 0, 100,
-                      &k_render_scale, "%.f%%" );
-         menu_checkbox( ctx, list, R == 4, "Motion Blur", &k_blur_effect );
-
-         menu_heading( ctx, list, "Camera", 0 );
-         menu_slider( ctx, list, R == 5, "Fov", 97, 135,
-                      &k_fov, "%.1f\xb0" );
-         menu_slider( ctx, list, R == 6, "Cam Height", -0.4f, +1.4f,
-                      &k_cam_height, vg_lerpf(-0.4f,1.4f,k_cam_height)>=0.0f? 
-                                      "+%.2fm": "%.2fm" );
-         menu_checkbox( ctx, list, R == 7, "Invert Y Axis", &k_invert_y );
-
-
-         ui_rect end = { list[0], list[1]+list[3]-64, list[2], 72 };
-         ctx->font = &vgf_default_small;
-         menu_heading( ctx, end, "Advanced", 0 );
-         if( menu_button( ctx, end, R == 8, "Open Engine Settings" ) )
-         {
-            vg_settings_open();
-         }
-      }
-      else if( menu.main_index == k_menu_main_guide )
-      {
-         list0[0] = 8;
-         list[0] = 16;
-
-         ui_px w = 700,
-               marg = list0[0]+list0[2],
-               pw = vg.window_x - marg,
-               infx = pw/2 - w/2 + marg;
-         ui_rect inf = { infx, height +32, w, 800 };
-
-         struct ui_vert *vs = NULL;
-
-         if( menu.guide_sel && (menu.guide_sel <= 3) )
-         {
-            vs = ui_fill( ctx, inf, ui_opacity( GUI_COL_DARK, 0.20f ) );
-            ui_rect_pad( inf, (ui_px[]){8,8} );
-         }
-
-         ui_fill( ctx, list0, ui_opacity( GUI_COL_DARK, 0.36f ) );
-         ui_outline( ctx, list0, 1, GUI_COL_NORM, 0 );
-         i32 R = menu_nav( &menu.guides_row, mv, 6 );
-
-         ctx->font = &vgf_default_large;
-         list[1] -= 8;
-         menu_heading( ctx, list, "Info", 0 );
-         if( menu.guide_sel == 1 ) 
-         {
-            menu_try_find_cam( 1 );
-
-            ui_rect title;
-            ui_split( inf, k_ui_axis_h, 28*2, 0, title, inf );
-            ctx->font = &vgf_default_title;
-            ui_text( ctx, 
-                     title, "Where to go", 1, k_ui_align_middle_center, 0 );
-
-            ui_split( inf, k_ui_axis_h, 28, 0, title, inf );
-            ctx->font = &vgf_default_large;
-            ui_text( ctx, inf, 
-                  "Visit the sandcastles built by John Cockroach to be\n"
-                  "transported into the real location. Use the map in the\n"
-                  "menu to return back this hub island.\n"
-                  "\n"
-                  "You can begin training at the Volcano Island, where you\n"
-                  "can learn movement skills. Then travel to Mt.Zero Island\n"
-                  "or Bort Downtown to test them out. Finally the most\n"
-                  "challenging course can be taken on at the Valley.\n"
-                  "\n"
-                  "Also visit the Steam Workshop for community made\n"
-                  "locations to skate!"
-                  , 1, k_ui_align_left, 0 );
-
-            vs[2].co[1] = vs[0].co[1] + 84 + vgf_default_large.sy*11 + 16;
-            vs[3].co[1] = vs[2].co[1];
-         }
-         if( menu_button( ctx, list, R == 0, "Where to go" ) ) 
-            menu.guide_sel = 1;
-
-         if( menu.guide_sel == 3 )
-         {
-            menu_try_find_cam( 2 );
-
-            ui_rect title;
-            ui_split( inf, k_ui_axis_h, 28*2, 0, title, inf );
-            ctx->font = &vgf_default_title;
-            ui_text( ctx, title, "Online", 1, k_ui_align_middle_center, 0 );
-
-            ui_split( inf, k_ui_axis_h, 28, 0, title, inf );
-            ctx->font = &vgf_default_large;
-            ui_text( ctx, inf, 
-                  "Connection to the global server is managed by this radar\n"
-                  "dish in the hub island. Come back here to turn the\n"
-                  "connection on or off.\n"
-                  "\n"
-                  "You'll then see other players or your friends if you go\n"
-                  "to the same location, and your highscores will be saved\n"
-                  "onto the scoreboards."
-                  , 1, k_ui_align_left, 0 );
-
-            vs[2].co[1] = vs[0].co[1] + 84 + vgf_default_large.sy*7 + 16;
-            vs[3].co[1] = vs[2].co[1];
-         }
-         if( menu_button( ctx, list, R == 1, "Playing Online" ) ) 
-            menu.guide_sel = 3;
-
-         menu_heading( ctx, list, "Controls", 0 );
-         if( menu_button( ctx, list, R == 2, "Skating \xb2" ) )
-         {
-            menu.guide_sel = 0;
-            menu_link_modal( 
-                  "https://skaterift.com/index.php?page=movement" );
-         }
-         if( menu.guide_sel == 0 || menu.guide_sel > 3 ) menu_try_find_cam( 3 );
-
-         if( menu_button( ctx, list, R == 3, "Tricks \xb2" ) )
-         {
-            menu.guide_sel = 0;
-            menu_link_modal( 
-                  "https://skaterift.com/index.php?page=tricks" );
-         }
-
-         menu_heading( ctx, list, "Workshop", 0 );
-         if( menu_button( ctx, list, R == 4, "Create a Board \xb2" ) )
-         {
-            menu.guide_sel = 0;
-            menu_link_modal( 
-                  "https://skaterift.com/index.php?page=workshop_board" );
-         }
-         if( menu_button( ctx, list, R == 5, "Create a World \xb2" ) )
-         {
-            menu.guide_sel = 0;
-            menu_link_modal( 
-                  "https://skaterift.com/index.php?page=workshop_world" );
-         }
-         if( menu_button( ctx, list, R == 6, "Create a Playermodel \xb2" ) )
-         {
-            menu.guide_sel = 0;
-            menu_link_modal( 
-                  "https://skaterift.com/index.php?page=workshop_player" );
-         }
-      }
-   }
-
-menu_draw:
-
-   vg_ui.frosting = 0.015f;
-   ui_flush( ctx, k_ui_shader_colour, NULL );
-   vg_ui.frosting = 0.0f;
-   ctx->font = &vgf_default_small;
-}
diff --git a/menu.h b/menu.h
deleted file mode 100644 (file)
index 5a6dbc4..0000000
--- a/menu.h
+++ /dev/null
@@ -1,56 +0,0 @@
-#pragma once
-
-#define MENU_STACK_SIZE 8
-
-#include "vg/vg_engine.h"
-#include "entity.h"
-
-enum menu_page
-{
-   k_menu_page_any,
-   k_menu_page_starter,
-   k_menu_page_premium,
-   k_menu_page_main,
-   k_menu_page_credits,
-   k_menu_page_help,
-};
-
-enum menu_main_subpage
-{
-   k_menu_main_main = 0,
-   k_menu_main_map  = 1,
-   k_menu_main_settings = 2,
-   k_menu_main_guide = 3
-};
-
-struct global_menu
-{
-   int disable_open;
-   i32 skip_starter;
-   enum menu_page page;
-   i32 main_index, 
-       main_row,
-       settings_row,
-       guides_row,
-       intro_row,
-       guide_sel,
-       prem_row;
-   f32 mouse_dist;  /* used for waking up mouse */
-
-   f32 repeater;
-
-   bool bg_blur;
-   ent_camera *bg_cam;
-
-   const char *web_link;   /* if set; modal */
-   i32 web_choice;
-
-   GLuint prem_tex;
-}
-extern menu;
-
-void menu_init(void);
-void menu_at_begin(void);
-void menu_gui( ui_context *ctx );
-void menu_open( enum menu_page page );
-bool menu_viewing_map(void);
diff --git a/model.c b/model.c
deleted file mode 100644 (file)
index f3f7ab2..0000000
--- a/model.c
+++ /dev/null
@@ -1,648 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-
-#include "vg/vg_io.h"
-
-#ifdef VG_3D
-#include "vg/vg_async.h"
-#include "vg/vg_tex.h"
-#endif
-
-#include "vg/vg_msg.h"
-#include "vg/vg_string.h"
-#include <string.h>
-#include <stdlib.h>
-#include <errno.h>
-#include "model.h"
-#include "shader_props.h"
-
-static void mdl_load_fatal_corrupt( mdl_context *mdl )
-{
-   vg_fatal_condition();
-   vg_file_error_info( mdl->file );
-   fclose( mdl->file );
-   vg_fatal_exit();
-}
-
-/*
- * Model implementation
- */
-
-void mdl_fread_pack_file( mdl_context *mdl, mdl_file *info, void *dst )
-{
-   if( !info->pack_size )
-   {
-      vg_fatal_condition();
-      vg_info( "Packed file is only a header; it is not packed" );
-      vg_info( "path: %s\n", mdl_pstr( mdl, info->pstr_path ) );
-      vg_fatal_exit();
-   }
-
-   fseek( mdl->file, mdl->pack_base_offset+info->pack_offset, SEEK_SET );
-   u64 l = fread( dst, info->pack_size, 1, mdl->file );
-
-   if( l != 1 ) mdl_load_fatal_corrupt( mdl );
-}
-
-/* TODO: Rename these */
-static void mdl_load_array_file_buffer( mdl_context *mdl, mdl_array *arr, 
-                                        void *buffer, u32 stride )
-{
-   if( arr->item_count )
-   {
-      fseek( mdl->file, arr->file_offset, SEEK_SET );
-
-      if( stride == arr->item_size )
-      {
-         u64 l = fread( buffer, arr->item_size*arr->item_count, 1, mdl->file );
-         if( l != 1 ) mdl_load_fatal_corrupt( mdl );
-      }
-      else 
-      {
-         vg_warn( "Applying alignment fixup to array @%p [%u -> %u] x %u\n", 
-                  buffer, arr->item_size, stride, arr->item_count );
-
-         if( stride > arr->item_size )
-            memset( buffer, 0, stride*arr->item_count );
-
-         u32 read_size = VG_MIN( stride, arr->item_size );
-
-         for( u32 i=0; i<arr->item_count; i++ )
-         {
-            u64 l = fread( buffer+i*stride, read_size, 1, mdl->file );
-            if( stride < arr->item_size )
-               fseek( mdl->file, arr->item_size-stride, SEEK_CUR );
-
-            if( l != 1 ) mdl_load_fatal_corrupt( mdl );
-         }
-      }
-   }
-}
-
-static void mdl_load_array_file( mdl_context *mdl, mdl_array_ptr *ptr, 
-                                 mdl_array *arr, void *lin_alloc, u32 stride )
-{
-   if( arr->item_count )
-   {
-      u32 size = stride*arr->item_count;
-      ptr->data = lin_alloc? vg_linear_alloc( lin_alloc, vg_align8(size) ):
-                             malloc( size );
-      mdl_load_array_file_buffer( mdl, arr, ptr->data, stride );
-   }
-   else
-   {
-      ptr->data = NULL;
-   }
-   
-   ptr->stride = stride;
-   ptr->count = arr->item_count;
-}
-
-void *mdl_arritm( mdl_array_ptr *arr, u32 index )
-{
-   return ((u8 *)arr->data) + index*arr->stride;
-}
-
-u32 mdl_arrcount( mdl_array_ptr *arr )
-{
-   return arr->count;
-}
-
-static mdl_array *mdl_find_array( mdl_context *mdl, const char *name )
-{
-   for( u32 i=0; i<mdl_arrcount(&mdl->index); i++ )
-   {
-      mdl_array *arr = mdl_arritm( &mdl->index, i );
-      
-      if( !strncmp(arr->name,name,16) )
-         return arr;
-   }
-
-   return NULL;
-}
-
-int _mdl_load_array( mdl_context *mdl, mdl_array_ptr *ptr,
-                     const char *name, void *lin_alloc, u32 stride )
-{
-   mdl_array *arr = mdl_find_array( mdl, name );
-
-   if( arr )
-   {
-      mdl_load_array_file( mdl, ptr, arr, lin_alloc, stride );
-      return 1;
-   }
-   else
-   {
-      ptr->data = NULL;
-      ptr->count = 0;
-      ptr->stride = 0;
-      return 0;
-   }
-}
-
-int mdl_load_mesh_block( mdl_context *mdl, void *lin_alloc )
-{
-   int success = 1;
-
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->verts,    mdl_vert,   lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->indices,  mdl_indice, lin_alloc );
-
-   return success;
-}
-
-int mdl_load_metadata_block( mdl_context *mdl, void *lin_alloc )
-{
-   int success = 1;
-
-   success &= _mdl_load_array( mdl, &mdl->strings, "strings", lin_alloc, 1 );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->meshs,     mdl_mesh,     lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->submeshs,  mdl_submesh,  lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->textures,  mdl_texture,  lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->armatures, mdl_armature, lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->bones,     mdl_bone,     lin_alloc );
-   success &= MDL_LOAD_ARRAY( mdl, &mdl->animations,mdl_animation,lin_alloc );
-
-   success &= mdl_load_materials( mdl, lin_alloc );
-
-   return success;
-}
-
-int mdl_load_animation_block( mdl_context *mdl, void *lin_alloc )
-{
-   return MDL_LOAD_ARRAY( mdl, &mdl->keyframes, mdl_keyframe, lin_alloc );
-}
-
-void *mdl_shader_standard( vg_msg *msg, void *alloc )
-{
-   struct shader_props_standard *props = 
-      vg_linear_alloc( alloc, sizeof(struct shader_props_standard) );
-
-   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
-                     NULL );
-
-   return props;
-}
-
-void *mdl_shader_terrain( vg_msg *msg, void *alloc )
-{
-   struct shader_props_terrain *props = 
-      vg_linear_alloc( alloc, sizeof(struct shader_props_terrain) );
-
-   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
-                     NULL );
-   vg_msg_getkvvecf( msg, "sand_colour", k_vg_msg_v4f, 
-                     props->sand_colour, (v4f){ 0.79, 0.63, 0.48, 1.0 } );
-   vg_msg_getkvvecf( msg, "blend_offset", k_vg_msg_v2f,
-                     props->blend_offset, (v2f){ 0.5, 0.0 } );
-
-   return props;
-}
-
-void *mdl_shader_vertex_blend( vg_msg *msg, void *alloc )
-{
-   struct shader_props_vertex_blend *props = 
-      vg_linear_alloc( alloc, sizeof(struct shader_props_vertex_blend) );
-
-   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
-                     NULL );
-   vg_msg_getkvvecf( msg, "blend_offset", k_vg_msg_v2f,
-                     props->blend_offset, (v2f){ 0.5, 0.0 } );
-   return props;
-}
-
-void *mdl_shader_water( vg_msg *msg, void *alloc )
-{
-   struct shader_props_water *props = 
-      vg_linear_alloc( alloc, sizeof(struct shader_props_water) );
-
-   vg_msg_getkvvecf( msg, "shore_colour", k_vg_msg_v4f,
-                     props->shore_colour, (v4f){0.03,0.32,0.61,1.0} );
-   vg_msg_getkvvecf( msg, "deep_colour", k_vg_msg_v4f,
-                     props->deep_colour, (v4f){0.0,0.006,0.03,1.0} );
-   vg_msg_getkvintg( msg, "fog_scale", k_vg_msg_f32, &props->fog_scale,
-                     (f32[]){0.04} );
-   vg_msg_getkvintg( msg, "fresnel", k_vg_msg_f32, &props->fresnel,
-                     (f32[]){5.0} );
-   vg_msg_getkvintg( msg, "water_scale", k_vg_msg_f32, &props->water_sale,
-                     (f32[]){ 0.008 } );
-   vg_msg_getkvvecf( msg, "wave_speed", k_vg_msg_v4f,
-                     props->wave_speed, (v4f){0.008,0.006,0.003,0.03} );
-   return props;
-}
-
-void *mdl_shader_cubemapped( vg_msg *msg, void *alloc )
-{
-   struct shader_props_cubemapped *props = 
-      vg_linear_alloc( alloc, sizeof(struct shader_props_cubemapped) );
-
-   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
-                     NULL );
-   vg_msg_getkvintg( msg, "cubemap_entity", k_vg_msg_u32, 
-                     &props->cubemap_entity, NULL );
-   vg_msg_getkvvecf( msg, "tint", k_vg_msg_v4f,
-                     props->tint, (v4f){1.0,1.0,1.0,1.0} );
-   return props;
-}
-
-bool _mdl_legacy_v105_properties( struct mdl_material_v105 *mat, vg_msg *dst )
-{
-   vg_msg_wkvnum( dst, "tex_diffuse", k_vg_msg_u32, 1, &mat->tex_diffuse );
-
-   if( mat->shader == k_shader_cubemap )
-   {
-      vg_msg_wkvnum( dst, "cubemap", k_vg_msg_u32, 1, &mat->tex_none0 );
-      vg_msg_wkvnum( dst, "tint", k_vg_msg_f32, 4, mat->colour );
-   }
-   else if( mat->shader == k_shader_terrain_blend )
-   {
-      vg_msg_wkvnum( dst, "sand_colour", k_vg_msg_f32, 4, mat->colour );
-      vg_msg_wkvnum( dst, "blend_offset", k_vg_msg_f32, 2, mat->colour1 );
-   }
-   else if( mat->shader == k_shader_standard_vertex_blend )
-   {
-      vg_msg_wkvnum( dst, "blend_offset", k_vg_msg_f32, 2, mat->colour1 );
-   }
-   else if( mat->shader == k_shader_water )
-   {
-      vg_msg_wkvnum( dst, "shore_colour", k_vg_msg_f32, 4, mat->colour );
-      vg_msg_wkvnum( dst, "deep_colour", k_vg_msg_f32, 4, mat->colour1 );
-   }
-
-   return 1;
-}
-
-int mdl_load_materials( mdl_context *mdl, void *lin_alloc )
-{
-   MDL_LOAD_ARRAY( mdl, &mdl->materials, mdl_material, lin_alloc );
-
-#if (MDL_VERSION_MIN <= 105)
-   /* load legacy material data into scratch */
-   mdl_array_ptr legacy_materials;
-   if( mdl->info.version <= 105 )
-   {
-      _mdl_load_array( mdl, &legacy_materials, "mdl_material", vg_mem.scratch,
-                       sizeof(struct mdl_material_v105) );
-   }
-#endif
-
-   mdl_array_ptr data;
-   _mdl_load_array( mdl, &data, "shader_data", vg_mem.scratch, 1 );
-
-   if( !lin_alloc )
-      return 1;
-
-   for( u32 i=0; i<mdl_arrcount(&mdl->materials); i ++ )
-   {
-      mdl_material *mat = mdl_arritm( &mdl->materials, i );
-      vg_msg msg;
-
-#if (MDL_VERSION_MIN <= 105)
-      u8 legacy_buf[512];
-      if( mdl->info.version <= 105 )
-      {
-         vg_msg_init( &msg, legacy_buf, sizeof(legacy_buf) );
-         _mdl_legacy_v105_properties( mdl_arritm( &legacy_materials,i ), &msg );
-         vg_msg_init( &msg, legacy_buf, msg.cur.co );
-      }
-      else
-#endif
-      {
-         vg_msg_init( &msg, data.data + mat->props.kvs.offset, 
-                      mat->props.kvs.size );
-      }
-
-      if( mat->shader == k_shader_standard || 
-          mat->shader == k_shader_standard_cutout || 
-          mat->shader == k_shader_foliage ||
-          mat->shader == k_shader_fxglow )
-      {
-         mat->props.compiled = mdl_shader_standard( &msg, lin_alloc );
-      }
-      else if( mat->shader == k_shader_standard_vertex_blend )
-      {
-         mat->props.compiled = mdl_shader_vertex_blend( &msg, lin_alloc );
-      }
-      else if( mat->shader == k_shader_cubemap )
-      {
-         mat->props.compiled = mdl_shader_cubemapped( &msg, lin_alloc );
-      }
-      else if( mat->shader == k_shader_terrain_blend )
-      {
-         mat->props.compiled = mdl_shader_terrain( &msg, lin_alloc );
-      }
-      else if( mat->shader == k_shader_water )
-      {
-         mat->props.compiled = mdl_shader_water( &msg, lin_alloc );
-      }
-      else
-         mat->props.compiled = NULL;
-   }
-
-   return 1;
-}
-
-/*
- * if calling mdl_open, and the file does not exist, the game will fatal quit
- */
-void mdl_open( mdl_context *mdl, const char *path, void *lin_alloc )
-{
-   memset( mdl, 0, sizeof( mdl_context ) );
-   mdl->file = fopen( path, "rb" );
-
-   if( !mdl->file )
-   {
-      vg_fatal_condition();
-      vg_info( "mdl_open('%s'): %s\n", path, strerror(errno) );
-      vg_fatal_exit();
-   }
-
-   u64 l = fread( &mdl->info, sizeof(mdl_header), 1, mdl->file );
-   if( l != 1 )
-      mdl_load_fatal_corrupt( mdl );
-
-   if( mdl->info.version < MDL_VERSION_MIN )
-   {
-      vg_fatal_condition();
-      vg_info( "Legacy model version incompatable" );
-      vg_info( "For model: %s\n", path );
-      vg_info( "  version: %u (min: %u, current: %u)\n", 
-               mdl->info.version, MDL_VERSION_MIN, MDL_VERSION_NR );
-      vg_fatal_exit();
-   }
-
-   mdl_load_array_file( mdl, &mdl->index, &mdl->info.index, lin_alloc,
-                        sizeof(mdl_array) );
-
-   mdl_array *pack = mdl_find_array( mdl, "pack" );
-   if( pack ) mdl->pack_base_offset = pack->file_offset;
-   else mdl->pack_base_offset = 0;
-}
-
-/*
- * close file handle
- */
-void mdl_close( mdl_context *mdl )
-{
-   fclose( mdl->file );
-   mdl->file = NULL;
-}
-
-/* useful things you can do with the model */
-
-void mdl_transform_m4x3( mdl_transform *transform, m4x3f mtx )
-{
-   q_m3x3( transform->q, mtx );
-   v3_muls( mtx[0], transform->s[0], mtx[0] );
-   v3_muls( mtx[1], transform->s[1], mtx[1] );
-   v3_muls( mtx[2], transform->s[2], mtx[2] );
-   v3_copy( transform->co, mtx[3] );
-}
-
-const char *mdl_pstr( mdl_context *mdl, u32 pstr )
-{
-   return ((char *)mdl_arritm( &mdl->strings, pstr )) + 4;
-}
-
-
-int mdl_pstreq( mdl_context *mdl, u32 pstr, const char *str, u32 djb2 )
-{
-   u32 hash = *((u32 *)mdl_arritm( &mdl->strings, pstr ));
-   if( hash == djb2 ){
-      if( !strcmp( str, mdl_pstr( mdl, pstr ))) return 1;
-      else return 0;
-   }
-   else return 0;
-}
-
-/*
- * Simple mesh interface for OpenGL
- * ----------------------------------------------------------------------------
- */
-
-#ifdef VG_3D
-static void mesh_upload( glmesh *mesh,
-                            mdl_vert *verts, u32 vert_count,
-                            u32 *indices, u32 indice_count )
-{
-   glGenVertexArrays( 1, &mesh->vao );
-   glGenBuffers( 1, &mesh->vbo );
-   glGenBuffers( 1, &mesh->ebo );
-   glBindVertexArray( mesh->vao );
-
-   size_t stride = sizeof(mdl_vert);
-
-   glBindBuffer( GL_ARRAY_BUFFER, mesh->vbo );
-   glBufferData( GL_ARRAY_BUFFER, vert_count*stride, verts, GL_STATIC_DRAW );
-
-   glBindVertexArray( mesh->vao );
-   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh->ebo );
-   glBufferData( GL_ELEMENT_ARRAY_BUFFER, indice_count*sizeof(u32),
-         indices, GL_STATIC_DRAW );
-   
-   /* 0: coordinates */
-   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
-   glEnableVertexAttribArray( 0 );
-
-   /* 1: normal */
-   glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 
-         stride, (void *)offsetof(mdl_vert, norm) );
-   glEnableVertexAttribArray( 1 );
-
-   /* 2: uv */
-   glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 
-         stride, (void *)offsetof(mdl_vert, uv) );
-   glEnableVertexAttribArray( 2 );
-
-   /* 3: colour */
-   glVertexAttribPointer( 3, 4, GL_UNSIGNED_BYTE, GL_TRUE, 
-         stride, (void *)offsetof(mdl_vert, colour) );
-   glEnableVertexAttribArray( 3 );
-
-   /* 4: weights */
-   glVertexAttribPointer( 4, 4, GL_UNSIGNED_SHORT, GL_TRUE, 
-         stride, (void *)offsetof(mdl_vert, weights) );
-   glEnableVertexAttribArray( 4 );
-
-   /* 5: groups */
-   glVertexAttribIPointer( 5, 4, GL_UNSIGNED_BYTE,
-         stride, (void *)offsetof(mdl_vert, groups) );
-   glEnableVertexAttribArray( 5 );
-
-   mesh->indice_count = indice_count;
-   mesh->loaded = 1;
-}
-
-void mesh_bind( glmesh *mesh )
-{
-   glBindVertexArray( mesh->vao );
-}
-
-void mesh_drawn( u32 start, u32 count )
-{
-   glDrawElements( GL_TRIANGLES, count, GL_UNSIGNED_INT, 
-         (void *)(start*sizeof(u32)) );
-}
-
-void mesh_draw( glmesh *mesh )
-{
-   mesh_drawn( 0, mesh->indice_count );
-}
-
-void mesh_free( glmesh *mesh )
-{
-   if( mesh->loaded )
-   {
-      glDeleteVertexArrays( 1, &mesh->vao );
-      glDeleteBuffers( 1, &mesh->ebo );
-      glDeleteBuffers( 1, &mesh->vbo );
-      mesh->loaded = 0;
-   }
-}
-
-void mdl_draw_submesh( mdl_submesh *sm )
-{
-   mesh_drawn( sm->indice_start, sm->indice_count );
-}
-#endif
-
-mdl_mesh *mdl_find_mesh( mdl_context *mdl, const char *name )
-{
-   for( u32 i=0; i<mdl_arrcount( &mdl->meshs ); i++ )
-   {
-      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
-      if( !strcmp( name, mdl_pstr( mdl, mesh->pstr_name )))
-      {
-         return mesh;
-      }
-   }
-   return NULL;
-}
-
-mdl_submesh *mdl_find_submesh( mdl_context *mdl, const char *mesh_name )
-{
-   mdl_mesh *mesh = mdl_find_mesh( mdl, mesh_name );
-
-   if( !mesh ) return NULL;
-   if( !mesh->submesh_count ) return NULL;
-
-   return mdl_arritm( &mdl->submeshs, mesh->submesh_start );
-}
-
-#ifdef VG_3D
-struct payload_glmesh_load
-{
-   mdl_vert *verts;
-   u32 *indices;
-
-   u32 vertex_count,
-       indice_count;
-
-   glmesh *mesh;
-};
-
-static void _sync_mdl_load_glmesh( void *payload, u32 size )
-{
-   struct payload_glmesh_load *job = payload;
-   mesh_upload( job->mesh, job->verts, job->vertex_count,
-                           job->indices, job->indice_count );
-}
-
-void mdl_async_load_glmesh( mdl_context *mdl, glmesh *mesh, u32 *fixup_table )
-{
-   mdl_array *arr_vertices = mdl_find_array( mdl, "mdl_vert" );
-   mdl_array *arr_indices = mdl_find_array( mdl, "mdl_indice" );
-
-   if( arr_vertices && arr_indices )
-   {
-      u32 size_verts   = vg_align8(sizeof(mdl_vert)*arr_vertices->item_count),
-          size_indices = vg_align8(sizeof(mdl_indice)*arr_indices->item_count),
-          size_hdr     = vg_align8(sizeof(struct payload_glmesh_load)),
-          total        = size_hdr + size_verts + size_indices;
-
-      vg_async_item *call = vg_async_alloc( total );
-      struct payload_glmesh_load *job = call->payload;
-
-      u8 *payload = call->payload;
-
-      job->mesh = mesh;
-      job->verts = (void*)(payload + size_hdr);
-      job->indices = (void*)(payload + size_hdr + size_verts);
-      job->vertex_count = arr_vertices->item_count;
-      job->indice_count = arr_indices->item_count;
-
-      mdl_load_array_file_buffer( mdl, arr_vertices, 
-                                  job->verts, sizeof(mdl_vert) );
-      mdl_load_array_file_buffer( mdl, arr_indices, job->indices, 
-                                  sizeof(mdl_indice) );
-
-      if( fixup_table )
-      {
-         for( u32 i=0; i<job->vertex_count; i ++ )
-         {
-            mdl_vert *vert = &job->verts[i];
-            
-            for( u32 j=0; j<4; j++ )
-            {
-               vert->groups[j] = fixup_table[vert->groups[j]];
-            }
-         }
-      }
-
-      /*
-       * Unpack the indices (if there are meshes)
-       * ---------------------------------------------------------
-       */
-
-      if( mdl_arrcount( &mdl->submeshs ) )
-      {
-         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, 0 );
-         u32 offset = sm->vertex_count;
-
-         for( u32 i=1; i<mdl_arrcount( &mdl->submeshs ); i++ )
-         {
-            mdl_submesh *sm = mdl_arritm( &mdl->submeshs, i );
-            u32 *indices    = job->indices + sm->indice_start;
-
-            for( u32 j=0; j<sm->indice_count; j++ )
-               indices[j] += offset;
-
-            offset += sm->vertex_count;
-         }
-      }
-
-      /* 
-       * Dispatch 
-       * -------------------------
-       */
-
-      vg_async_dispatch( call, _sync_mdl_load_glmesh );
-   }
-   else
-   {
-      vg_fatal_condition();
-      vg_info( "No vertex/indice data in model file\n" );
-      vg_fatal_exit();
-   }
-}
-
-/* uploads the glmesh, and textures. everything is saved into the mdl_context */
-void mdl_async_full_load_std( mdl_context *mdl )
-{
-   mdl_async_load_glmesh( mdl, &mdl->mesh, NULL );
-   
-   for( u32 i=0; i<mdl_arrcount( &mdl->textures ); i ++ )
-   {
-      vg_linear_clear( vg_mem.scratch );
-      mdl_texture *tex = mdl_arritm( &mdl->textures, i );
-
-      void *data = vg_linear_alloc( vg_mem.scratch, tex->file.pack_size );
-      mdl_fread_pack_file( mdl, &tex->file, data );
-
-      vg_tex2d_load_qoi_async( data, tex->file.pack_size,
-                               VG_TEX2D_CLAMP|VG_TEX2D_NEAREST, &tex->glname );
-   }
-}
-#endif
diff --git a/model.h b/model.h
deleted file mode 100644 (file)
index aade46a..0000000
--- a/model.h
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-
-#define MDL_VERSION_MIN 101
-#define MDL_VERSION_NR 106
-
-enum mdl_shader{
-   k_shader_standard                = 0,
-   k_shader_standard_cutout         = 1,
-   k_shader_terrain_blend           = 2,
-   k_shader_standard_vertex_blend   = 3,
-   k_shader_water                   = 4,
-   k_shader_invisible               = 5,
-   k_shader_boundary                = 6,
-   k_shader_fxglow                  = 7,
-   k_shader_cubemap                 = 8,
-   k_shader_walking                 = 9,
-   k_shader_foliage                 = 10,
-   k_shader_override                = 30000
-};
-
-enum mdl_surface_prop{
-   k_surface_prop_concrete = 0,
-   k_surface_prop_wood     = 1,
-   k_surface_prop_grass    = 2,
-   k_surface_prop_tiles    = 3,
-   k_surface_prop_metal    = 4,
-   k_surface_prop_snow     = 5,
-   k_surface_prop_sand     = 6
-};
-
-enum material_flag{
-   k_material_flag_skate_target     = 0x0001,
-   k_material_flag_collision        = 0x0002,
-   k_material_flag_grow_grass       = 0x0004,
-   k_material_flag_grindable        = 0x0008,
-   k_material_flag_invisible        = 0x0010,
-   k_material_flag_boundary         = 0x0020,
-   k_material_flag_preview_visibile = 0x0040,
-   k_material_flag_walking          = 0x0080,
-
-   k_material_flag_ghosts      =
-      k_material_flag_boundary|
-      k_material_flag_invisible|
-      k_material_flag_walking
-};
-
-#pragma pack(push,1)
-
-/* 48 byte */
-struct mdl_vert
-{
-   v3f co,        /* 3*32 */
-       norm;      /* 3*32 */
-   v2f uv;        /* 2*32 */
-
-   u8  colour[4]; /* 4*8 */
-   u16 weights[4];/* 4*16 */
-   u8  groups[4]; /* 4*8 */
-};
-
-#pragma pack(pop)
-
-typedef u32 mdl_indice;
-
-typedef struct mdl_context mdl_context;
-typedef struct mdl_array_ptr mdl_array_ptr;
-typedef struct mdl_vert mdl_vert;
-typedef struct mdl_transform mdl_transform;
-typedef struct mdl_submesh mdl_submesh;
-typedef struct mdl_material mdl_material;
-typedef struct mdl_bone mdl_bone;
-typedef struct mdl_armature mdl_armature;
-typedef struct mdl_animation mdl_animation;
-typedef struct mdl_transform mdl_keyframe;
-typedef struct mdl_mesh mdl_mesh;
-typedef struct mdl_file mdl_file;
-typedef struct mdl_texture mdl_texture;
-typedef struct mdl_array mdl_array;
-typedef struct mdl_header mdl_header;
-
-typedef struct glmesh glmesh;
-struct glmesh
-{
-   u32 vao, vbo, ebo;
-   u32 indice_count;
-   u32 loaded;
-};
-
-struct mdl_transform
-{
-   v3f co, s;
-   v4f q;
-};
-
-static void transform_identity( mdl_transform *transform )
-{
-   v3_zero( transform->co );
-   q_identity( transform->q );
-   v3_fill( transform->s, 1.0f );
-}
-
-static void mdl_transform_vector( mdl_transform *transform, v3f vec, v3f dest )
-{
-   v3_mul( transform->s, vec, dest );
-   q_mulv( transform->q, dest, dest );
-}
-
-static void mdl_transform_point( mdl_transform *transform, v3f co, v3f dest )
-{
-   mdl_transform_vector( transform, co, dest );
-   v3_add( transform->co, dest, dest );
-}
-
-static void mdl_transform_mul( mdl_transform *a, mdl_transform *b, 
-                               mdl_transform *d )
-{
-   mdl_transform_point( a, b->co, d->co );
-   q_mul( a->q, b->q, d->q );
-   q_normalize( d->q );
-   v3_mul( a->s, b->s, d->s );
-}
-
-struct mdl_file
-{
-   u32 pstr_path,
-       pack_offset,
-       pack_size;
-};
-
-#if (MDL_VERSION_MIN <= 105)
-struct mdl_material_v105
-{
-   u32 pstr_name,
-       shader,
-       flags,
-       surface_prop;
-
-   v4f colour,
-       colour1;
-
-   u32 tex_diffuse, /* Indexes start from 1. 0 if missing. */
-       tex_none0,
-       tex_none1;
-};
-#endif
-
-struct mdl_material
-{
-   u32 pstr_name,
-       shader,
-       flags,
-       surface_prop;
-
-   union 
-   {
-      struct 
-      {
-         u32 offset, size;
-         /* -> vg_msg containing KV properties */
-      }
-      kvs;
-      void *compiled;  /* -> shader specific structure for render */
-   } 
-   props;
-};
-
-struct mdl_bone
-{
-   v3f co, end;
-   u32 parent,
-       collider,
-       ik_target,
-       ik_pole,
-       flags,
-       pstr_name;
-
-   boxf hitbox;
-   v3f conevx, conevy, coneva;
-   float conet;
-};
-
-enum bone_flag
-{
-   k_bone_flag_deform               = 0x00000001,
-   k_bone_flag_ik                   = 0x00000002,
-   k_bone_flag_cone_constraint      = 0x00000004
-};
-
-enum bone_collider
-{
-   k_bone_collider_none = 0,
-   k_bone_collider_box = 1,
-   k_bone_collider_capsule = 2
-};
-         
-struct mdl_armature
-{
-   mdl_transform transform;
-   u32 bone_start,
-       bone_count,
-       anim_start,
-       anim_count;
-};
-
-struct mdl_animation
-{
-   u32 pstr_name,
-       length;
-   float rate;
-   u32 offset;
-};
-
-struct mdl_submesh
-{
-   u32 indice_start,
-       indice_count,
-       vertex_start,
-       vertex_count;
-
-   boxf bbx;
-   u16 material_id, flags;
-};
-
-enum esubmesh_flags
-{
-   k_submesh_flag_none     = 0x0000,
-   k_submesh_flag_consumed = 0x0001
-};
-
-struct mdl_mesh
-{
-   mdl_transform transform;
-   u32 submesh_start,
-       submesh_count,
-       pstr_name,
-       entity_id,    /* upper 16 bits: type, lower 16 bits: index */
-       armature_id;
-};
-
-struct mdl_texture
-{
-   mdl_file file;
-   u32 glname;
-};
-
-struct mdl_array
-{
-   u32 file_offset,
-       item_count,
-       item_size;
-
-   char name[16];
-};
-
-struct mdl_header
-{
-   u32 version;
-   mdl_array index;
-};
-
-struct mdl_context
-{
-   FILE *file;
-   mdl_header info;
-
-   struct mdl_array_ptr
-   {
-      void *data;
-      u32 count, stride;
-   }
-   index,
-
-   /* metadata */
-   strings,
-   meshs,
-   submeshs,
-   materials,
-   textures,
-   armatures,
-   bones,
-   animations,
-
-   /* animation buffers */
-   keyframes,
-
-   /* mesh buffers */
-   verts,
-   indices;
-   u32 pack_base_offset;
-
-   /* runtime */
-   glmesh mesh;
-};
-
-void mesh_bind( glmesh *mesh );
-void mesh_drawn( u32 start, u32 count );
-void mesh_draw( glmesh *mesh );
-void mesh_free( glmesh *mesh );
-
-/* file context management */
-void mdl_open( mdl_context *mdl, const char *path, void *lin_alloc );
-void mdl_close( mdl_context *mdl );
-
-/* array loading */
-int _mdl_load_array( mdl_context *mdl, mdl_array_ptr *ptr,
-                     const char *name, void *lin_alloc, u32 stride );
-#define MDL_LOAD_ARRAY( MDL, PTR, STRUCT, ALLOCATOR ) \
-   _mdl_load_array( MDL, PTR, #STRUCT, ALLOCATOR, sizeof(STRUCT) )
-
-/* array access */
-void *mdl_arritm( mdl_array_ptr *arr, u32 index );
-u32 mdl_arrcount( mdl_array_ptr *arr );
-
-/* pack access */
-void mdl_fread_pack_file( mdl_context *mdl, mdl_file *info, void *dst );
-
-/* standard array groups */
-int mdl_load_animation_block( mdl_context *mdl, void *lin_alloc );
-int mdl_load_metadata_block( mdl_context *mdl, void *lin_alloc );
-int mdl_load_mesh_block( mdl_context *mdl, void *lin_alloc );
-int mdl_load_materials( mdl_context *mdl, void *lin_alloc );
-
-/* load mesh */
-void mdl_async_load_glmesh( mdl_context *mdl, glmesh *mesh, u32 *fixup_table );
-
-/* load textures and mesh */
-void mdl_async_full_load_std( mdl_context *mdl );
-
-/* rendering */
-void mdl_draw_submesh( mdl_submesh *sm );
-mdl_mesh *mdl_find_mesh( mdl_context *mdl, const char *name );
-mdl_submesh *mdl_find_submesh( mdl_context *mdl, const char *mesh_name );
-
-/* pstrs */
-const char *mdl_pstr( mdl_context *mdl, u32 pstr );
-int mdl_pstreq( mdl_context *mdl, u32 pstr, const char *str, u32 djb2 );
-#define MDL_CONST_PSTREQ( MDL, Q, CONSTSTR )\
-   mdl_pstreq( MDL, Q, CONSTSTR, vg_strdjb2( CONSTSTR ) )
-
-void mdl_transform_m4x3( mdl_transform *transform, m4x3f mtx );
-
diff --git a/network.c b/network.c
deleted file mode 100644 (file)
index 0869612..0000000
--- a/network.c
+++ /dev/null
@@ -1,789 +0,0 @@
-#include "skaterift.h"
-#include "vg/vg_steam.h"
-#include "vg/vg_steam_networking.h"
-#include "vg/vg_steam_auth.h"
-#include "vg/vg_steam_friends.h"
-#include "player.h"
-#include "network.h"
-#include "network_msg.h"
-#include "network_common.h"
-#include "player_remote.h"
-#include "world.h"
-#include "world_sfd.h"
-#include "world_routes.h"
-#include "vg/vg_ui/imgui.h"
-#include "gui.h"
-#include "ent_region.h"
-#include "vg/vg_loader.h"
-
-#ifdef _WIN32
- #include <winsock2.h>
- #include <ws2tcpip.h>
-#else
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <netdb.h>
-#endif
-
-struct network_client network_client =
-{
-   .auth_mode = eServerModeAuthentication,
-   .state = k_ESteamNetworkingConnectionState_None,
-   .last_intent_change = -99999.9
-};
-
-static void scores_update(void);
-
-int packet_minsize( SteamNetworkingMessage_t *msg, u32 size ){
-   if( msg->m_cbSize < size ) {
-      vg_error( "Invalid packet size (must be at least %u)\n", size );
-      return 0;
-   }
-   else{
-      return 1;
-   }
-}
-
-static void on_auth_ticket_recieved( void *result, void *context ){
-   EncryptedAppTicketResponse_t *response = result;
-
-   if( response->m_eResult == k_EResultOK ){
-      vg_info( "  New app ticket ready\n" );
-   }
-   else{
-      vg_warn( "  Could not request new encrypted app ticket (%u)\n",
-                  response->m_eResult );
-   }
-   
-   if( SteamAPI_ISteamUser_GetEncryptedAppTicket( hSteamUser, 
-            network_client.app_symmetric_key,
-            VG_ARRAY_LEN(network_client.app_symmetric_key),
-            &network_client.app_key_length )){
-      vg_success( "  Loaded app ticket\n" );
-   }
-   else{
-      vg_error( "  No ticket availible\n" );
-      network_client.app_key_length = 0;
-   }
-}
-
-static void request_auth_ticket(void){
-   /* 
-    * TODO Check for one thats cached on the disk and load it.
-    * This might be OK though because steam seems to cache the result 
-    */
-
-   vg_info( "Requesting new authorization ticket\n" );
-
-   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
-   call->userdata = NULL;
-   call->p_handler = on_auth_ticket_recieved;
-   call->id = 
-      SteamAPI_ISteamUser_RequestEncryptedAppTicket( hSteamUser, NULL, 0 );
-}
-
-static void network_send_username(void){
-   if( !network_connected() )
-      return;
-
-   netmsg_playerusername *update = alloca( sizeof(netmsg_playerusername)+
-                                           NETWORK_USERNAME_MAX );
-   update->inetmsg_id = k_inetmsg_playerusername;
-   update->index = 0xff;
-
-   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
-   const char *username = SteamAPI_ISteamFriends_GetPersonaName(hSteamFriends);
-   u32 chs = str_utf8_collapse( username, update->name, NETWORK_USERNAME_MAX );
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, network_client.remote, 
-         update, sizeof(netmsg_playerusername)+chs+1,
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-void network_send_region(void)
-{
-   if( !network_connected() )
-      return;
-   
-   netmsg_region *region = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
-
-   region->inetmsg_id = k_inetmsg_region;
-   region->client = 0;
-   region->flags = global_ent_region.flags;
-
-   u32 l = vg_strncpy( global_ent_region.location, region->loc, 
-                       NETWORK_REGION_MAX, k_strncpy_always_add_null );
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, network_client.remote, 
-         region, sizeof(netmsg_region)+l+1,
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-static void network_send_request( netmsg_request *req, vg_msg *body,
-                                  void (*callback)( 
-                                     netmsg_request *res, vg_msg *body, 
-                                     u64 userdata),
-                                  u64 userdata ){
-   u32 len = 0;
-   if( body ){
-      len = body->cur.co;
-      vg_info( "Request scoreboard. Info (%u):\n", body->cur.co );
-      vg_msg_print( body, len );
-
-      if( body->error != k_vg_msg_error_OK ){
-         vg_error( "Body not OK\n" );
-         return;
-      }
-   }
-
-   if( callback ){
-      req->id = vg_pool_lru( &network_client.request_pool );
-      if( req->id ){
-         vg_pool_watch( &network_client.request_pool, req->id );
-         struct network_request *pn = 
-            vg_pool_item( &network_client.request_pool, req->id );
-         pn->callback = callback;
-         pn->sendtime = vg.time_real;
-         pn->userdata = userdata;
-      }
-      else{
-         vg_error( "Unable to send request. Pool is full.\n" );
-         return;
-      }
-   }
-   else
-      req->id = 0;
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, network_client.remote, 
-         req, sizeof(netmsg_request)+len,
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-static void network_scoreboard_callback( netmsg_request *res, vg_msg *body,
-                                         u64 userdata ){
-   world_instance *world = world_current_instance();
-
-   world_routes_recv_scoreboard( world, body, userdata, res->status );
-   if( userdata == world_sfd.active_route_board )
-      world_sfd_compile_active_scores();
-}
-
-
-
-/* mod_uid: world mod uid,
- * route_uid: run name (just a string)
- * week: 
- *   0   ALL TIME
- *   1   CURRENT WEEK
- *   2   ALL TIME + CURRENT WEEK
- *   .
- *   10+ specific week index
- */
-void network_request_scoreboard( const char *mod_uid, 
-                                 const char *route_uid,
-                                 u32 week, u64 userdata ){
-   if( !network_connected() ) 
-      return;
-
-   netmsg_request *req = alloca( sizeof(netmsg_request) + 512 );
-   req->inetmsg_id = k_inetmsg_request;
-
-   vg_msg data;
-   vg_msg_init( &data, req->q, 512 );
-   vg_msg_wkvstr( &data, "endpoint", "scoreboard" );
-   vg_msg_wkvstr( &data, "mod", mod_uid );
-   vg_msg_wkvstr( &data, "route", route_uid );
-   vg_msg_wkvnum( &data, "week", k_vg_msg_u32, 1, &week );
-   network_send_request( req, &data, network_scoreboard_callback, userdata );
-}
-
-static void network_publish_callback( netmsg_request *res, vg_msg *body,
-                                      u64 userdata ){
-   if( res->status != k_request_status_ok ){
-      vg_error( "Publish laptime, server error #%d\n", (i32)res->status );
-   }
-}
-
-void network_publish_laptime( const char *mod_uid, 
-                              const char *route_uid, f64 lap_time ){
-   if( !network_connected() )
-      return;
-
-   i32 time_centiseconds = lap_time * 100.0;
-
-   netmsg_request *req = alloca( sizeof(netmsg_request) + 512 );
-   req->inetmsg_id = k_inetmsg_request;
-
-   vg_msg data;
-   vg_msg_init( &data, req->q, 512 );
-   vg_msg_wkvstr( &data, "endpoint", "setlap" );
-   vg_msg_wkvstr( &data, "mod", mod_uid );
-   vg_msg_wkvstr( &data, "route", route_uid );
-   vg_msg_wkvnum( &data, "time", k_vg_msg_i32, 1, &time_centiseconds );
-   network_send_request( req, &data, network_publish_callback, 0 );
-}
-
-static void network_request_rx_300_400( SteamNetworkingMessage_t *msg ){
-   netmsg_blank *tmp = msg->m_pData;
-
-   if( tmp->inetmsg_id == k_inetmsg_request ){
-      
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_response ){
-      netmsg_request *res = (netmsg_request *)msg->m_pData;
-
-      vg_msg *body = NULL;
-
-      vg_msg data;
-      if( res->status == k_request_status_ok ){
-         vg_msg_init( &data, res->q, msg->m_cbSize - sizeof(netmsg_request) );
-         vg_success( "Response to #%d:\n", (i32)res->id );
-         vg_msg_print( &data, data.max );
-         body = &data;
-      }
-      else {
-         vg_warn( "Server response to #%d: %d\n", (i32)res->id, res->status );
-      }
-
-      if( res->id ){
-         struct network_request *pn = 
-            vg_pool_item( &network_client.request_pool, res->id );
-         pn->callback( res, body, pn->userdata );
-         vg_pool_unwatch( &network_client.request_pool, res->id );
-      }
-   }
-}
-
-void network_send_item( enum netmsg_playeritem_type type )
-{
-   if( !network_connected() )
-      return;
-
-   netmsg_playeritem *item = 
-      alloca( sizeof(netmsg_playeritem) + ADDON_UID_MAX );
-   item->inetmsg_id = k_inetmsg_playeritem;
-   item->type_index = type;
-   item->client = 0;
-
-   if( (type == k_netmsg_playeritem_world0) ||
-       (type == k_netmsg_playeritem_world1) ){
-
-      enum world_purpose purpose = type - k_netmsg_playeritem_world0;
-      addon_reg *reg = world_static.instance_addons[ purpose ];
-
-      if( reg )
-         addon_alias_uid( &reg->alias, item->uid );
-      else 
-         item->uid[0] = '\0';
-   }
-   else{
-      u16 view_id = 0;
-      enum addon_type addon_type = k_addon_type_none;
-      if( type == k_netmsg_playeritem_board ){
-         view_id = localplayer.board_view_slot;
-         addon_type = k_addon_type_board;
-      }
-      else if( type == k_netmsg_playeritem_player ){
-         view_id = localplayer.playermodel_view_slot;
-         addon_type = k_addon_type_player;
-      }
-
-      struct addon_cache *cache = &addon_system.cache[addon_type];
-      vg_pool *pool = &cache->pool;
-
-      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-      addon_cache_entry *entry = vg_pool_item( pool, view_id );
-      addon_alias_uid( &entry->reg_ptr->alias, item->uid );
-      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-   }
-
-   vg_info( "send equip: [%u] %s\n", 
-            item->type_index, item->uid );
-   u32 chs = strlen(item->uid);
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, network_client.remote, 
-         item, sizeof(netmsg_playeritem)+chs+1,
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-static void network_disconnect(void){
-   SteamAPI_ISteamNetworkingSockets_CloseConnection( 
-         hSteamNetworkingSockets, network_client.remote, 0, NULL, 0 );
-   network_client.remote = 0;
-   network_client.state = k_ESteamNetworkingConnectionState_None;
-
-   for( int i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
-      netplayers.list[i].active = 0;
-   }
-}
-
-void network_status_string( vg_str *str, u32 *colour )
-{
-   if( skaterift.demo_mode ){
-      vg_strcat( str, "Offline" );
-      return;
-   }
-
-   if( steam_ready ){
-      if( network_client.user_intent == k_server_intent_offline ){
-         vg_strcat( str, "Offline" );
-      }
-      else {
-         ESteamNetworkingConnectionState state = network_client.state;
-
-         if( state == k_ESteamNetworkingConnectionState_None )
-            vg_strcat( str, "No Connection" );
-         else if( state == k_ESteamNetworkingConnectionState_Connecting )
-         {
-            vg_strcatf( str, "Connecting...\n%s", network_client.host_adress );
-
-            if( network_client.retries ){
-               vg_strcat( str, "\n(" );
-               vg_strcati32( str, network_client.retries );
-               vg_strcat( str, " retries)" );
-            }
-         }
-         else if( state == k_ESteamNetworkingConnectionState_Connected ){
-            vg_strcatf( str, "Connected to:\n%s", network_client.host_adress );
-            *colour = 0xff00a020;
-         }
-         else if( state == k_ESteamNetworkingConnectionState_ClosedByPeer )
-            vg_strcat( str, "Connection Closed" );
-         else if( state == k_ESteamNetworkingConnectionState_FindingRoute )
-            vg_strcat( str, "Finding Route" );
-         else if( state ==
-               k_ESteamNetworkingConnectionState_ProblemDetectedLocally){
-            vg_strcat( str, "Problem Detected\nLocally" );
-            *colour = 0xff0000a0;
-         }
-         else
-            vg_strcat( str, "???" );
-      }
-   }
-   else {
-      vg_strcat( str, "Steam Offline" );
-      *colour = 0xff0000a0;
-   }
-}
-
-void render_server_status_gui(void)
-{
-   vg_framebuffer_bind( g_render.fb_network_status, 1.0f );
-
-   vg_ui_set_screen( 128, 48 );
-   ui_context *ctx = &vg_ui.ctx;
-
-   /* HACK */
-       ctx->cur_vert = 0;
-       ctx->cur_indice = 0;
-   ctx->vert_start = 0;
-   ctx->indice_start = 0;
-
-   ui_rect r = { 0, 0, 128, 48 };
-   
-   char buf[128];
-   vg_str str;
-   vg_strnull( &str, buf, sizeof(buf) );
-
-   u32 bg = 0xff000000;
-   network_status_string( &str, &bg );
-
-   ui_fill( ctx, r, bg );
-   ui_text( ctx, r, buf, 1, k_ui_align_center, 0 );
-   ui_flush( ctx, k_ui_shader_colour, NULL );
-   
-   skaterift.rt_textures[ k_skaterift_rt_server_status ] =
-      g_render.fb_network_status->attachments[0].id;
-}
-
-static void on_server_connect_status( CallbackMsg_t *msg ){
-   SteamNetConnectionStatusChangedCallback_t *info = (void *)msg->m_pubParam;
-   vg_info( "  Connection status changed for %lu\n", info->m_hConn );
-   vg_info( "  %s -> %s\n", 
-         string_ESteamNetworkingConnectionState(info->m_eOldState),
-         string_ESteamNetworkingConnectionState(info->m_info.m_eState) );
-
-   if( info->m_hConn == network_client.remote ){
-      network_client.state = info->m_info.m_eState;
-
-      if( info->m_info.m_eState == 
-            k_ESteamNetworkingConnectionState_Connected ){
-         vg_success("  Connected to remote server.. authenticating\n");
-
-         /* send version info to server */
-         netmsg_version version;
-         version.inetmsg_id = k_inetmsg_version;
-         version.version = NETWORK_SKATERIFT_VERSION;
-         SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-               hSteamNetworkingSockets, network_client.remote, &version, 
-               sizeof(netmsg_version), k_nSteamNetworkingSend_Reliable, NULL );
-
-         /* TODO: We should really wait to see if the server is in auth mode
-          * first... */
-         u32 size = sizeof(netmsg_auth) + network_client.app_key_length;
-         netmsg_auth *auth = alloca(size);
-         auth->inetmsg_id = k_inetmsg_auth;
-         auth->ticket_length = network_client.app_key_length;
-         for( int i=0; i<network_client.app_key_length; i++ )
-            auth->ticket[i] = network_client.app_symmetric_key[i];
-
-         SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-               hSteamNetworkingSockets, network_client.remote, auth, size,
-               k_nSteamNetworkingSend_Reliable, NULL );
-      }
-      else if( info->m_info.m_eState == 
-            k_ESteamNetworkingConnectionState_ClosedByPeer ){
-
-         if( info->m_info.m_eEndReason == 
-               k_ESteamNetConnectionEnd_Misc_InternalError ){
-            network_client.retries = 40;
-         }
-         network_disconnect();
-      }
-      else if( info->m_info.m_eState == 
-            k_ESteamNetworkingConnectionState_ProblemDetectedLocally ){
-         network_disconnect();
-      }
-   }
-   else{
-      //vg_warn( "  Recieved signal from unknown connection\n" );
-   }
-
-   render_server_status_gui();
-}
-
-static void on_persona_state_change( CallbackMsg_t *msg ){
-   if( !network_connected() )
-      return;
-
-   PersonaStateChange_t *info = (void *)msg->m_pubParam;
-   ISteamUser *hSteamUser = SteamAPI_SteamUser();
-
-   vg_info( "User: %llu, change: %u\n", info->m_ulSteamID, 
-                                        info->m_nChangeFlags );
-
-   if( info->m_ulSteamID == SteamAPI_ISteamUser_GetSteamID(hSteamUser) ){
-      if( info->m_nChangeFlags & k_EPersonaChangeName ){
-         network_send_username();
-      }
-   }
-
-   if( info->m_nChangeFlags & k_EPersonaChangeRelationshipChanged ){
-      for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
-         struct network_player *rp = &netplayers.list[i];
-         if( rp->steamid == info->m_ulSteamID ){
-            player_remote_update_friendflags( rp );
-         }
-      }
-   }
-}
-
-void network_set_host( const char *host_str, const char *port_str )
-{
-   vg_strncpy( host_str, network_client.host_adress, 
-               sizeof(network_client.host_adress), k_strncpy_overflow_fatal );
-
-   memset( &network_client.ip, 0, sizeof(network_client.ip) );
-   network_client.ip_resolved = 0;
-
-   if( port_str )
-   {
-      vg_strncpy( port_str, network_client.host_port, 
-                  sizeof(network_client.host_port), k_strncpy_overflow_fatal );
-   }
-   else
-   {
-      vg_str str;
-      vg_strnull( &str, network_client.host_port, 
-                  sizeof(network_client.host_port) );
-      vg_strcati32( &str, NETWORK_PORT );
-   }
-
-   network_client.ip.m_port = atoi( network_client.host_port );
-}
-
-static void network_connect(void)
-{
-   VG_ASSERT( network_client.ip_resolved );
-
-   vg_info( "connecting...\n" );
-   network_client.remote = SteamAPI_ISteamNetworkingSockets_ConnectByIPAddress( 
-                  hSteamNetworkingSockets, &network_client.ip, 0, NULL );
-}
-
-static void network_sign_on_complete(void){
-   vg_success( "Sign on completed\n" );
-
-   /* send our init info */
-   network_send_username();
-   for( u32 i=0; i<k_netmsg_playeritem_max; i ++ ){
-      network_send_item(i);
-   }
-   network_send_region();
-}
-
-static void poll_remote_connection(void){
-   SteamNetworkingMessage_t *messages[32];
-   int len;
-
-   for( int i=0; i<10; i++ ){
-      len = SteamAPI_ISteamNetworkingSockets_ReceiveMessagesOnConnection(
-            hSteamNetworkingSockets, network_client.remote, 
-            messages, VG_ARRAY_LEN(messages));
-
-      if( len <= 0 )
-         return;
-
-      for( int i=0; i<len; i++ ){
-         SteamNetworkingMessage_t *msg = messages[i];
-
-         if( msg->m_cbSize < sizeof(netmsg_blank) ){
-            vg_warn( "Discarding message (too small: %d)\n", msg->m_cbSize );
-            continue;
-         }
-
-         netmsg_blank *tmp = msg->m_pData;
-
-         if( (tmp->inetmsg_id >= 200) && (tmp->inetmsg_id < 300) ){
-            player_remote_rx_200_300( msg );
-         }
-         else if( (tmp->inetmsg_id >= 300) && (tmp->inetmsg_id < 400) ){
-            network_request_rx_300_400( msg );
-         }
-         else {
-            if( tmp->inetmsg_id == k_inetmsg_version ){
-               netmsg_version *version = msg->m_pData;
-               if( version->version != NETWORK_SKATERIFT_VERSION ){
-                  network_disconnect();
-                  /* we dont want to connect to this server ever */
-                  network_client.retries = 999;
-                  network_client.last_attempt = 999999999.9;
-                  vg_error( "version mismatch with server\n" );
-               }
-               else {
-                  network_client.remote_version = version->version;
-                  network_sign_on_complete();
-               }
-            }
-         }
-
-         SteamAPI_SteamNetworkingMessage_t_Release( msg );
-      }
-   }
-}
-
-static void network_resolve_host_async( void *payload, u32 size )
-{
-   u32 *status = payload;
-   network_client.ip_resolved = *status;
-
-   char buf[256];
-   SteamAPI_SteamNetworkingIPAddr_ToString( &network_client.ip, buf, 256, 1 );
-   vg_info( "Resolved host address to: %s\n", buf );
-}
-
-static void network_resolve_host_thread( void *_ )
-{
-   vg_async_item *call = vg_async_alloc(8);
-   u32 *status = call->payload;
-   *status = 0;
-
-   if( (network_client.host_adress[0] >= '0') && 
-       (network_client.host_adress[0] <= '9') )
-   {
-      SteamAPI_SteamNetworkingIPAddr_ParseString( 
-            &network_client.ip, 
-            network_client.host_adress );
-      network_client.ip.m_port = atoi( network_client.host_port );
-      *status = 1;
-      goto end;
-   }
-
-   vg_info( "Resolving host.. %s (:%s)\n",
-             network_client.host_adress, network_client.host_port );
-
-   struct addrinfo  hints;
-   struct addrinfo  *result;
-
-   /* Obtain address(es) matching host/port. */
-
-   memset( &hints, 0, sizeof(hints) );
-   hints.ai_family = AF_INET6;
-   hints.ai_socktype = SOCK_DGRAM;
-   hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG;
-   hints.ai_protocol = 0;
-
-   int s = getaddrinfo( network_client.host_adress, network_client.host_port, 
-                        &hints, &result);
-   if( s != 0 ) 
-   {
-#ifndef _WIN32
-      vg_error( "getaddrinfo: %s\n", gai_strerror(s) );
-#endif
-
-      if( !strcmp( network_client.host_adress, "skaterift.com" ) )
-      {
-         vg_warn( "getaddrinfo failed for skaterift.com;\n "
-                  "falling back to a hardcoded IPv4\n" );
-         strcpy( network_client.host_adress, "46.101.34.155" );
-         SteamAPI_SteamNetworkingIPAddr_ParseString( 
-               &network_client.ip, 
-               network_client.host_adress );
-         network_client.ip.m_port = NETWORK_PORT;
-         *status = 1;
-      }
-
-      goto end;
-   }
-
-   struct sockaddr_in6 *inaddr = (struct sockaddr_in6 *)result->ai_addr;
-   memcpy( network_client.ip.m_ipv6, &inaddr->sin6_addr, 16 );
-   freeaddrinfo( result );
-
-   *status = 1;
-
-end: vg_async_dispatch( call, network_resolve_host_async );
-}
-
-void network_update(void)
-{
-   if( !steam_ready )
-      return;
-
-   ESteamNetworkingConnectionState state = network_client.state;
-
-   if( network_client.user_intent == k_server_intent_offline )
-   {
-      if( state != k_ESteamNetworkingConnectionState_None )
-         network_disconnect();
-
-      return;
-   }
-
-   if( state == k_ESteamNetworkingConnectionState_Connected )
-   {
-      poll_remote_connection();
-      f64 frame_delta = vg.time_real - network_client.last_frame;
-
-      if( frame_delta > NETWORK_FRAMERATE )
-      {
-         network_client.last_frame = vg.time_real;
-         remote_player_send_playerframe();
-         localplayer.sfx_buffer_count = 0;
-      }
-
-      remote_player_debug_update();
-   }
-   else 
-   {
-      if( (state == k_ESteamNetworkingConnectionState_Connecting) ||
-          (state == k_ESteamNetworkingConnectionState_FindingRoute) )
-      {
-         return;
-      }
-      else 
-      {
-         f64 waited = vg.time_real - network_client.last_attempt,
-             min_wait = 1.0;
-
-         if( network_client.retries > 5 )
-            min_wait = 60.0;
-
-         if( waited < min_wait )
-            return;
-
-         if( !network_client.ip_resolved )
-         {
-            if( vg_loader_availible() )
-            {
-               vg_loader_start( network_resolve_host_thread, NULL );
-            }
-            else return;
-         }
-         else
-            network_connect();
-         
-         network_client.retries ++;
-         network_client.last_attempt = vg.time_real;
-      }
-   }
-}
-
-void chat_send_message( const char *message )
-{
-   if( !network_connected() ){
-      return;
-   }
-
-   netmsg_chat *chat = alloca( sizeof(netmsg_chat) + NETWORK_MAX_CHAT );
-   chat->inetmsg_id = k_inetmsg_chat;
-   chat->client = 0;
-
-   u32 l = vg_strncpy( message, chat->msg, NETWORK_MAX_CHAT, 
-                       k_strncpy_always_add_null );
-
-   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-         hSteamNetworkingSockets, network_client.remote, 
-         chat, sizeof(netmsg_chat)+l+1,
-         k_nSteamNetworkingSend_Reliable, NULL );
-}
-
-static int cmd_network_send_message( int argc, const char *argv[] ){
-   char buf[ NETWORK_MAX_CHAT ];
-   vg_str str;
-   vg_strnull( &str, buf, NETWORK_MAX_CHAT );
-
-   for( int i=0; i<argc; i ++ ){
-      vg_strcat( &str, argv[i] );
-
-      if( i < argc-1 )
-         vg_strcatch( &str, ' ' );
-   }
-
-   chat_send_message( buf );
-   return 0;
-}
-
-void network_init(void)
-{
-   vg_console_reg_var( "network_info", &network_client.network_info,
-                       k_var_dtype_i32, VG_VAR_PERSISTENT );
-   vg_console_reg_var( "auto_connect", &network_client.auto_connect,
-                       k_var_dtype_i32, VG_VAR_PERSISTENT );
-   if( steam_ready ){
-      u32 alloc_size = sizeof(struct network_request)*NETWORK_MAX_REQUESTS;
-      network_client.request_buffer = 
-         vg_linear_alloc( vg_mem.rtmemory, alloc_size );
-      memset( network_client.request_buffer, 0, alloc_size );
-
-      vg_pool *pool = &network_client.request_pool;
-      pool->buffer = network_client.request_buffer;
-      pool->count = NETWORK_MAX_REQUESTS;
-      pool->stride = sizeof( struct network_request );
-      pool->offset = offsetof( struct network_request, poolnode );
-      vg_pool_init( pool );
-
-      steam_register_callback( k_iSteamNetConnectionStatusChangedCallBack,
-                               on_server_connect_status );
-      steam_register_callback( k_iPersonaStateChange, 
-                               on_persona_state_change );
-      request_auth_ticket();
-
-      vg_console_reg_cmd( "say", cmd_network_send_message, NULL );
-   }
-}
-
-void network_end(void)
-{
-   /* TODO: Send buffered highscores that were not already */
-   if( (network_client.state == k_ESteamNetworkingConnectionState_Connected) ||
-       (network_client.state == k_ESteamNetworkingConnectionState_Connecting) )
-   {
-      SteamAPI_ISteamNetworkingSockets_CloseConnection(
-            hSteamNetworkingSockets, network_client.remote, 0, NULL, 1 );
-   }
-}
diff --git a/network.h b/network.h
deleted file mode 100644 (file)
index 6af51af..0000000
--- a/network.h
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- * All trademarks are property of their respective owners
- */
-
-#pragma once
-#include "vg/vg_platform.h"
-#include "vg/vg_steam_networking.h"
-#include "vg/vg_mem_pool.h"
-#include "vg/vg_msg.h"
-#include "steam.h"
-#include "network_common.h"
-#include "network_msg.h"
-#include "addon_types.h"
-
-#define NETWORK_MAX_REQUESTS 8
-
-/* 
- * Interface
- */
-
-/* Call it at start; Connects us to the gameserver */
-void network_init(void);
-
-/* Run this from main loop */
-void network_update(void);
-
-/* Call it at shutdown */
-void network_end(void);
-
-/* 
- * Can buffer up a bunch of these by calling many times, they will be
- * sent at the next connection 
- */
-void network_submit_highscore( u32 trackid, u16 points, u16 time );
-
-/*
- * Game endpoints are provided with the same names to allow running without a
- * network connection.
- */
-
-struct network_client
-{
-   u8 app_symmetric_key[ 1024 ];
-   u32 app_key_length;
-   EServerMode auth_mode;
-
-   HSteamNetConnection remote;
-   ESteamNetworkingConnectionState state;
-   u32 remote_version;
-
-   f64 last_attempt, last_frame;
-   u32 retries;
-
-   i32 network_info;
-   i32 auto_connect;
-
-   struct network_request {
-      vg_pool_node poolnode;
-      void (*callback)( netmsg_request *res, vg_msg *body, u64 userdata );
-      f64 sendtime;
-      u64 userdata;
-   }
-   *request_buffer;
-   vg_pool request_pool;
-
-   SteamNetworkingIPAddr ip;
-   char host_port[8], host_adress[256];
-   bool ip_resolved;
-
-   enum server_intent {
-      k_server_intent_offline,
-      k_server_intent_online
-   }
-   user_intent;
-   f64 last_intent_change;
-   f32 fintent; /* yeah this shit really shouldnt be here but oh well */
-}
-extern network_client;
-
-int packet_minsize( SteamNetworkingMessage_t *msg, u32 size );
-void network_send_item( enum netmsg_playeritem_type type );
-void network_request_scoreboard( const char *mod_uid, 
-                                 const char *route_uid,
-                                 u32 week, u64 userdata );
-void network_publish_laptime( const char *mod_uid, 
-                              const char *route_uid, f64 lap_time );
-void chat_send_message( const char *message );
-void render_server_status_gui(void);
-void network_status_string( vg_str *str, u32 *colour );
-void network_send_region(void);
-void network_set_host( const char *host_str, const char *port_str );
-
-static inline int network_connected(void)
-{
-   if( network_client.remote_version != NETWORK_SKATERIFT_VERSION ) return 0;
-   return network_client.state == k_ESteamNetworkingConnectionState_Connected;
-}
diff --git a/network_common.h b/network_common.h
deleted file mode 100644 (file)
index ca46e47..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-#include "vg/vg_string.h"
-
-#define NETWORK_USERNAME_MAX 32
-#define NETWORK_MAX_PLAYERS 20
-#define NETWORK_FRAMERATE 0.1
-#define NETWORK_BUFFERFRAMES 6
-#define NETWORK_MAX_CHAT 128
-#define NETWORK_REGION_MAX 32
-#define NETWORK_SKATERIFT_VERSION 10
-#define NETWORK_REQUEST_MAX 2048
-
-#define NETWORK_LEADERBOARD_ALLTIME 0
-#define NETWORK_LEADERBOARD_CURRENT_WEEK 1
-#define NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK 2
-#define NETWORK_PORT 27403
-#define NETWORK_PORT_STR(STR, X) STR #X
-
-#include "addon_types.h"
-
-static u32 network_msgstring( const char *src, 
-                              u32 m_cbSize, u32 base_size,
-                              char *buf, u32 buf_size ){
-   
-   u32 string_len = VG_MIN( m_cbSize - base_size, buf_size );
-   return vg_strncpy( src, buf, string_len, k_strncpy_always_add_null );
-}
-
-static u32 network_pair_index( u32 _a, u32 _b ){
-   const u32 N = NETWORK_MAX_PLAYERS;
-
-   if( !((_a != _b) && (_a<N) && (_b<N) ) )
-   {
-      vg_fatal_error( "Programming error\n" );
-   }
-
-   u32 a = VG_MIN( _a, _b ),
-       b = VG_MAX( _a, _b );
-
-   return ((N-a)*((N-a)-1))/2 - b + a;
-}
diff --git a/network_compression.h b/network_compression.h
deleted file mode 100644 (file)
index f83f0f5..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-#ifndef NETWORK_COMPRESSION_H
-#define NETWORK_COMPRESSION_H
-
-#include "vg/vg_platform.h"
-#include "vg/vg_m.h"
-
-typedef struct bitpack_ctx bitpack_ctx;
-struct bitpack_ctx {
-   enum bitpack_mode {
-      k_bitpack_compress,
-      k_bitpack_decompress
-   }
-   mode;
-
-   u8 *buffer;
-   u32 bytes, buffer_len;
-};
-
-static void bitpack_bytes( bitpack_ctx *ctx, u32 bytes, void *data ){
-   u8 *ext = data;
-   for( u32 i=0; i<bytes; i++ ){
-      u32 index = ctx->bytes+i;
-      if( ctx->mode == k_bitpack_compress ){
-         if( index < ctx->buffer_len )
-            ctx->buffer[index] = ext[i];
-      }
-      else{
-         if( index < ctx->buffer_len )
-            ext[i] = ctx->buffer[index];
-         else
-            ext[i] = 0x00;
-      }
-   }
-   ctx->bytes += bytes;
-}
-
-static u32 bitpack_qf32( bitpack_ctx *ctx, u32 bits, 
-                         f32 min, f32 max, f32 *v ){
-   u32 mask = (0x1 << bits) - 1;
-
-   if( ctx->mode == k_bitpack_compress ){
-      u32 a = vg_quantf( *v, bits, min, max );
-      bitpack_bytes( ctx, bits/8, &a );
-      return a;
-   }
-   else {
-      u32 a = 0;
-      bitpack_bytes( ctx, bits/8, &a );
-      *v = vg_dequantf( a, bits, min, max );
-      return a;
-   }
-}
-
-static void bitpack_qv2f( bitpack_ctx *ctx, u32 bits,
-                          f32 min, f32 max, v2f v ){
-   for( u32 i=0; i<2; i ++ )
-      bitpack_qf32( ctx, bits, min, max, v+i );
-}
-
-static void bitpack_qv3f( bitpack_ctx *ctx, u32 bits,
-                          f32 min, f32 max, v3f v ){
-   for( u32 i=0; i<3; i ++ )
-      bitpack_qf32( ctx, bits, min, max, v+i );
-}
-
-static void bitpack_qv4f( bitpack_ctx *ctx, u32 bits,
-                          f32 min, f32 max, v4f v ){
-   for( u32 i=0; i<4; i ++ )
-      bitpack_qf32( ctx, bits, min, max, v+i );
-}
-
-static void bitpack_qquat( bitpack_ctx *ctx, v4f quat ){
-   const f32 k_domain = 0.70710678118f;
-
-   if( ctx->mode == k_bitpack_compress ){
-      v4f qabs;
-      for( u32 i=0; i<4; i++ )
-         qabs[i] = fabsf(quat[i]);
-
-      u32 lxy =  qabs[1]>qabs[0],
-          lzw = (qabs[3]>qabs[2])+2,
-          l   = qabs[lzw]>qabs[lxy]? lzw: lxy;
-
-      f32 sign = vg_signf(quat[l]);
-
-      u32 smallest[3];
-      for( u32 i=0, j=0; i<4; i ++ )
-         if( i != l )
-            smallest[j ++] = vg_quantf( quat[i]*sign, 10, -k_domain, k_domain );
-      
-      u32 comp = (smallest[0]<<2) | (smallest[1]<<12) | (smallest[2]<<22) | l;
-      bitpack_bytes( ctx, 4, &comp );
-   }
-   else {
-      u32 comp;
-      bitpack_bytes( ctx, 4, &comp );
-
-      u32 smallest[3] = {(comp>>2 )&0x3ff,
-                         (comp>>12)&0x3ff,
-                         (comp>>22)&0x3ff},
-          l = comp & 0x3;
-
-      f32 m = 1.0f;
-
-      for( u32 i=0, j=0; i<4; i ++ ){
-         if( i != l ){
-            quat[i] = vg_dequantf( smallest[j ++], 10, -k_domain, k_domain );
-            m -= quat[i]*quat[i];
-         }
-      }
-
-      quat[l] = sqrtf(m);
-      q_normalize( quat );
-   }
-}
-
-#endif /* NETWORK_COMPRESSION_H */
diff --git a/network_msg.h b/network_msg.h
deleted file mode 100644 (file)
index 4dfeb5e..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#ifndef NETWORK_MSG_H
-#define NETWORK_MSG_H
-
-#include "world_info.h"
-#include "vg/vg_platform.h"
-;
-
-#pragma pack(push,1)
-
-typedef struct netmsg_blank netmsg_blank;
-enum{ k_inetmsg_blank = 0 };
-struct netmsg_blank{
-   u16 inetmsg_id;
-};
-
-/* send after version */
-typedef struct netmsg_auth netmsg_auth;
-enum{ k_inetmsg_auth = 1 };
-struct netmsg_auth
-{
-   u16 inetmsg_id;
-
-   u32 ticket_length;
-   u8 ticket[];
-};
-
-/* version should be sent before auth */
-typedef struct netmsg_version netmsg_version;
-enum{ k_inetmsg_version = 2 };
-struct netmsg_version{
-   u16 inetmsg_id;
-   u32 version;
-};
-
-/* server control 100 */
-
-/* player updates 200 */
-
-#define NETMSG_BOUNDARY_BIT 0x8000
-#define NETMSG_GATE_BOUNDARY_BIT 0x4000
-#define NETMSG_BOUNDARY_MASK (NETMSG_BOUNDARY_BIT|NETMSG_GATE_BOUNDARY_BIT)
-#define NETMSG_PLAYERFRAME_INSTANCE_ID 0x3
-#define NETMSG_PLAYERFRAME_HAVE_GLIDER 0x4
-#define NETMSG_PLAYERFRAME_GLIDER_ORPHAN 0x8
-
-typedef struct netmsg_playerframe netmsg_playerframe;
-enum{ k_inetmsg_playerframe = 200 };
-struct netmsg_playerframe{
-   u16 inetmsg_id;
-   f64 timestamp;
-
-   u8 client, subsystem, 
-      flags, sound_effects;
-   u16 boundary_hash; /* used for animating correctly through gates, teleport..
-                         msb is a flip flop for teleporting
-                         second msb is flip flop for gate */
-
-   u8 animdata[];
-};
-
-typedef struct netmsg_playerjoin netmsg_playerjoin;
-enum{ k_inetmsg_playerjoin = 201 };
-struct netmsg_playerjoin{
-   u16 inetmsg_id;
-   u8 index;
-   u64 steamid;
-};
-
-typedef struct netmsg_playerleave netmsg_playerleave;
-enum{ k_inetmsg_playerleave = 202 };
-struct netmsg_playerleave{
-   u16 inetmsg_id;
-   u8 index;
-};
-
-typedef struct netmsg_playerusername netmsg_playerusername;
-enum{ k_inetmsg_playerusername = 203 };
-struct netmsg_playerusername{
-   u16 inetmsg_id;
-   u8 index;
-   char name[];
-};
-
-typedef struct netmsg_playeritem netmsg_playeritem;
-enum{ k_inetmsg_playeritem = 204 };
-struct netmsg_playeritem{
-   u16 inetmsg_id;
-   u8 client;
-   u8 type_index;
-   char uid[];
-};
-enum netmsg_playeritem_type {
-   k_netmsg_playeritem_board = 0,
-   k_netmsg_playeritem_player,
-   k_netmsg_playeritem_world0,
-   k_netmsg_playeritem_world1,
-   k_netmsg_playeritem_max
-};
-
-typedef struct netmsg_chat netmsg_chat;
-enum{ k_inetmsg_chat = 205 };
-struct netmsg_chat {
-   u16 inetmsg_id;
-   u8 client;
-   char msg[];
-};
-
-typedef struct netmsg_region netmsg_region;
-enum{ k_inetmsg_region = 206 };
-struct netmsg_region {
-   u16 inetmsg_id;
-   u8 client;
-   u32 flags;
-   char loc[];
-};
-
-/* requests 300 */
-typedef struct netmsg_request netmsg_request;
-enum{ k_inetmsg_request = 300, k_inetmsg_response = 301 };
-struct netmsg_request {
-   u16 inetmsg_id;
-   u8 id, status;
-   u8 q[];
-};
-
-enum request_status {
-  k_request_status_client_error = 0,
-  k_request_status_invalid_endpoint = 1,
-  k_request_status_unauthorized = 2,
-
-  k_request_status_server_error = 100,
-  k_request_status_out_of_memory = 101,
-  k_request_status_database_error = 102,
-
-  k_request_status_ok = 200,
-  k_request_status_not_found = 201
-};
-
-#pragma pack(pop)
-#endif /* NETWORK_MSG_H */
diff --git a/particle.c b/particle.c
deleted file mode 100644 (file)
index ec2960b..0000000
+++ /dev/null
@@ -1,187 +0,0 @@
-#include "vg/vg_lines.h"
-#include "vg/vg_async.h"
-#include "particle.h"
-#include "shaders/particle.h"
-
-struct particle_system particles_grind = {
-   .scale = 0.02f,
-   .velocity_scale = 0.001f,
-   .width = 0.0125f
-},
-particles_env = {
-   .scale = 0.04f,
-   .velocity_scale = 0.001f,
-   .width = 0.25f
-};
-
-void particle_spawn( particle_system *sys, v3f co, v3f v,
-                     f32 lifetime, u32 colour )
-{
-   if( sys->alive == sys->max ) return;
-
-   particle *p = &sys->array[ sys->alive ++ ];
-   v3_copy( co, p->co );
-   v3_copy( v, p->v );
-   p->life = lifetime;
-   p->colour = colour;
-}
-
-void particle_spawn_cone( particle_system *sys, 
-                          v3f co, v3f dir, f32 angle, f32 speed, 
-                          f32 lifetime, u32 colour )
-{
-   if( sys->alive == sys->max ) return;
-
-   particle *p = &sys->array[ sys->alive ++ ];
-
-   v3f tx, ty;
-   v3_tangent_basis( dir, tx, ty );
-
-   v3f rand;
-   vg_rand_cone( &vg.rand, rand, angle );
-   v3_muls(          tx,  rand[0]*speed, p->v );
-   v3_muladds( p->v, ty,  rand[1]*speed, p->v );
-   v3_muladds( p->v, dir, rand[2]*speed, p->v );
-
-   p->life = lifetime;
-   p->colour = colour;
-   v3_copy( co, p->co );
-}
-
-void particle_system_update( particle_system *sys, f32 dt )
-{
-   u32 i = 0;
-iter: if( i == sys->alive ) return;
-
-   particle *p = &sys->array[i];
-   p->life -= dt;
-
-   if( p->life < 0.0f ){
-      *p = sys->array[ -- sys->alive ];
-      goto iter;
-   }
-
-   v3_muladds( p->co, p->v, dt, p->co );
-   p->v[1] += -9.8f * dt;
-
-   i ++;
-   goto iter;
-}
-
-void particle_system_debug( particle_system *sys )
-{
-   for( u32 i=0; i<sys->alive; i ++ ){
-      particle *p = &sys->array[i];
-      v3f p1;
-      v3_muladds( p->co, p->v, 0.2f, p1 );
-      vg_line( p->co, p1, p->colour );
-   }
-}
-
-struct particle_init_args {
-   particle_system *sys;
-   u16 indices[];
-};
-
-static void async_particle_init( void *payload, u32 size ){
-   struct particle_init_args *args = payload;
-   particle_system *sys = args->sys;
-
-   glGenVertexArrays( 1, &sys->vao );
-   glGenBuffers( 1, &sys->vbo );
-   glGenBuffers( 1, &sys->ebo );
-   glBindVertexArray( sys->vao );
-
-   size_t stride = sizeof(particle_vert);
-
-   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
-   glBufferData( GL_ARRAY_BUFFER, sys->max*stride*4, NULL, GL_DYNAMIC_DRAW );
-   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, sys->ebo );
-   glBufferData( GL_ELEMENT_ARRAY_BUFFER, 
-                  sys->max*sizeof(u16)*6, args->indices, GL_STATIC_DRAW );
-
-   /* 0: coordinates */
-   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
-   glEnableVertexAttribArray( 0 );
-
-   /* 3: colour */
-   glVertexAttribPointer( 1, 4, GL_UNSIGNED_BYTE, GL_TRUE, 
-         stride, (void *)offsetof(particle_vert, colour) );
-   glEnableVertexAttribArray( 1 );
-}
-
-void particle_alloc( particle_system *sys, u32 max )
-{
-   size_t stride = sizeof(particle_vert);
-
-   sys->max = max;
-   sys->array = vg_linear_alloc( vg_mem.rtmemory, max*sizeof(particle) );
-   sys->vertices = vg_linear_alloc( vg_mem.rtmemory, max*stride*4 );
-
-   vg_async_item *call = 
-      vg_async_alloc( sizeof(particle_system *) + max*sizeof(u16)*6 );
-   struct particle_init_args *init = call->payload;
-   init->sys = sys;
-
-   for( u32 i=0; i<max; i ++ ){
-      init->indices[i*6+0] = i*4;
-      init->indices[i*6+1] = i*4+1;
-      init->indices[i*6+2] = i*4+2;
-      init->indices[i*6+3] = i*4;
-      init->indices[i*6+4] = i*4+2;
-      init->indices[i*6+5] = i*4+3;
-   }
-
-   vg_async_dispatch( call, async_particle_init );
-}
-
-void particle_system_prerender( particle_system *sys )
-{
-   for( u32 i=0; i<sys->alive; i ++ ){
-      particle *p = &sys->array[i];
-      particle_vert *vs = &sys->vertices[i*4];
-
-      v3f v, right;
-      v3_copy( p->v, v );
-   
-      f32 vm = v3_length( p->v );
-      v3_muls( v, 1.0f/vm, v );
-      v3_cross( v, (v3f){0,1,0}, right );
-
-      f32 l = (sys->scale+sys->velocity_scale*vm), 
-          w = sys->width;
-
-      v3f p0, p1;
-      v3_muladds( p->co, p->v,  l, p0 );
-      v3_muladds( p->co, p->v, -l, p1 );
-      
-      v3_muladds( p0, right,  w, vs[0].co );
-      v3_muladds( p1, right,  w, vs[1].co );
-      v3_muladds( p1, right, -w, vs[2].co );
-      v3_muladds( p0, right, -w, vs[3].co );
-
-      vs[0].colour = p->colour;
-      vs[1].colour = p->colour;
-      vs[2].colour = p->colour;
-      vs[3].colour = p->colour;
-   }
-
-   glBindVertexArray( sys->vao );
-
-   size_t stride = sizeof(particle_vert);
-   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
-   glBufferSubData( GL_ARRAY_BUFFER, 0, sys->alive*stride*4, sys->vertices );
-}
-
-void particle_system_render( particle_system *sys, vg_camera *cam )
-{
-   glDisable( GL_CULL_FACE );
-   glEnable( GL_DEPTH_TEST );
-
-   shader_particle_use();
-   shader_particle_uPv( cam->mtx.pv );
-   shader_particle_uPvPrev( cam->mtx_prev.pv );
-
-       glBindVertexArray( sys->vao );
-       glDrawElements( GL_TRIANGLES, sys->alive*6, GL_UNSIGNED_SHORT, NULL );
-}
diff --git a/particle.h b/particle.h
deleted file mode 100644 (file)
index 6858890..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-#include "skaterift.h"
-
-typedef struct particle_system particle_system;
-typedef struct particle particle;
-typedef struct particle_vert particle_vert;
-
-struct particle_system {
-   struct particle {
-      v3f co, v;
-      f32 life;
-      u32 colour;
-   }
-   *array;
-
-#pragma pack(push,1)
-   struct particle_vert {
-      v3f co;
-      u32 colour;
-   }
-   *vertices;
-#pragma pack(pop)
-
-   u32 alive, max;
-   GLuint vao, vbo, ebo;
-
-   /* render settings */
-   f32 scale, velocity_scale, width;
-}
-extern particles_grind, particles_env;
-
-void particle_alloc( particle_system *sys, u32 max );
-void particle_system_update( particle_system *sys, f32 dt );
-void particle_system_debug( particle_system *sys );
-void particle_system_prerender( particle_system *sys );
-void particle_system_render( particle_system *sys, vg_camera *cam );
-
-void particle_spawn( particle_system *sys, 
-                     v3f co, v3f v, f32 lifetime, u32 colour );
-void particle_spawn_cone( particle_system *sys, 
-                          v3f co, v3f dir, f32 angle, f32 speed, 
-                          f32 lifetime, u32 colour );
diff --git a/physics_test.h b/physics_test.h
deleted file mode 100644 (file)
index 243de36..0000000
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#ifndef PHYSICS_TEST_H
-#define PHYSICS_TEST_H
-
-#include "rigidbody.h"
-#include "player.h"
-
-rigidbody ground = { .type = k_rb_shape_box,
-                     .bbx = {{-100.0f,-1.0f,-100.0f},{100.0f,0.0f,100.0f}},
-                     .co = {0.0f, 0.0f, 0.0f},
-                     .q = {0.0f,0.0f,0.0f,1.0f},
-                     .is_world = 1 };
-
-rigidbody blocky = 
-   {
-      .type = k_rb_shape_box,
-      .bbx = {{-2.0f,-1.0f,-3.0f},{2.0f,1.0f,2.0f}},
-      .co = {30.0f,2.0f,30.0f},
-      .q = {0.0f,0.0f,0.0f,1.0f},
-      .is_world = 1
-   };
-
-rigidbody marko = 
-{
-   .type = k_rb_shape_box,
-   .bbx = {{-0.5f,-0.5f,-0.5f},{0.5f,0.5f,0.5f}},
-   .co = {-36.0f,8.0f,-36.0f},
-   .q = {0.0f,0.0f,0.0f,1.0f},
-   .is_world = 0
-};
-
-scene epic_scene;
-
-rigidbody epic_scene_rb = 
-{
-   .type = k_rb_shape_scene,
-   .co = {0.0f,0.0f,0.0f},
-   .q = {0.0f,0.0f,0.0f,1.0f},
-   .is_world = 1,
-   .inf.scene = { .pscene = &epic_scene }
-};
-
-rigidbody funnel[4] = {
-   {
-      .type = k_rb_shape_box,
-      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
-      .co = {-10.0f,5.0f,0.0f},
-      .is_world = 1
-   },
-   {
-      .type = k_rb_shape_box,
-      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
-      .co = { 10.0f,5.0f,0.0f},
-      .is_world = 1
-   },
-   {
-      .type = k_rb_shape_box,
-      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
-      .co = { 0.0f,5.0f,10.0f},
-      .is_world = 1
-   },
-   {
-      .type = k_rb_shape_box,
-      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
-      .co = {0.0f,5.0f,-10.0f},
-      .is_world = 1
-   }
-};
-
-rigidbody jeff1 = { .type = k_rb_shape_capsule,
-                    .inf.capsule = { .radius = 0.75f, .height = 3.0f },
-                    .co = {30.0f, 4.0f, 30.0f },
-                    .q = {1.0f,0.0f,0.0f,0.0f}
-};
-
-rigidbody ball = { .type = k_rb_shape_sphere,
-                   .inf.sphere = { .radius = 2.0f },
-                   .co = {0.0f,20.0f,2.0f},
-                   .q = {0.0f,0.0f,0.0f,1.0f}},
-
-          ball1= { .type = k_rb_shape_sphere,
-                   .inf.sphere = { .radius = 2.0f },
-                   .co = {0.1f,25.0f,0.2f},
-                   .q = {0.0f,0.0f,0.0f,1.0f}};
-
-rigidbody jeffs[16];
-
-static void reorg_jeffs(void)
-{
-   for( int i=0; i<vg_list_size(jeffs); i++ )
-   {
-      v3_copy( (v3f){ (vg_randf()-0.5f) * 10.0f,
-                      (vg_randf()-0.5f) * 10.0f + 17.0f,
-                      (vg_randf()-0.5f) * 10.0f }, jeffs[i].co );
-      v4_copy( (v4f){ vg_randf(), vg_randf(), vg_randf(), vg_randf() },
-               jeffs[i].q );
-      q_normalize( jeffs[i].q );
-      
-      jeffs[i].type = k_rb_shape_capsule;
-      jeffs[i].inf.capsule.radius = 0.75f;
-      jeffs[i].inf.capsule.height = 3.0f;
-
-      rb_init( &jeffs[i] );
-   }
-}
-
-static void physics_test_start(void)
-{
-   q_axis_angle( funnel[0].q, (v3f){1.0f,0.0f,0.0f},  0.6f );
-   q_axis_angle( funnel[1].q, (v3f){1.0f,0.0f,0.0f}, -0.6f );
-   q_axis_angle( funnel[2].q, (v3f){0.0f,0.0f,1.0f},  0.6f );
-   q_axis_angle( funnel[3].q, (v3f){0.0f,0.0f,1.0f}, -0.6f );
-
-   for( int i=0; i<4; i++ )
-      rb_init( &funnel[i] );
-
-   reorg_jeffs();
-
-   rb_init( &ground );
-   rb_init( &ball );
-   rb_init( &ball1 );
-   rb_init( &jeff1 );
-   rb_init( &blocky );
-
-   scene_init( &epic_scene );
-
-   mdl_header *mdl = mdl_load( "models/epic_scene.mdl" );
-
-   m4x3f transform;
-   m4x3_identity( transform );
-
-   for( int i=0; i<mdl->node_count; i++ )
-   {
-      mdl_node *pnode = mdl_node_from_id( mdl, i );
-
-      for( int j=0; j<pnode->submesh_count; j++ )
-      {
-         mdl_submesh *sm = mdl_node_submesh( mdl, pnode, j );
-         scene_add_submesh( &epic_scene, mdl, sm, transform );
-      }
-   }
-
-   vg_free( mdl );
-   scene_bh_create( &epic_scene );
-
-   rb_init( &epic_scene_rb );
-   rb_init( &marko );
-}
-
-static void physics_test_update(void)
-{
-   player_freecam();
-   player_camera_update();
-
-   for( int i=0; i<4; i++ )
-      rb_debug( &funnel[i], 0xff0060e0 );
-   rb_debug( &ground, 0xff00ff00 );
-   rb_debug( &ball, 0xffe00040 );
-   rb_debug( &ball1, 0xff00e050 );
-
-   rb_debug( &blocky, 0xffcccccc );
-   rb_debug( &jeff1, 0xff00ffff );
-
-   rb_debug( &epic_scene_rb, 0xffcccccc );
-   rb_debug( &marko, 0xffffcc00 );
-
-   {
-
-   rb_solver_reset();
-
-   for( int i=0; i<4; i++ )
-   {
-      rigidbody *fn = &funnel[i];
-      rb_collide( &ball, fn );
-      rb_collide( &ball1, fn );
-      rb_collide( &jeff1, fn );
-
-      for( int i=0; i<vg_list_size(jeffs); i++ )
-         rb_collide( jeffs+i, fn );
-   }
-
-   for( int i=0; i<vg_list_size(jeffs)-1; i++ )
-   {
-      for( int j=i+1; j<vg_list_size(jeffs); j++ )
-      {
-         rb_collide( jeffs+i, jeffs+j );
-      }
-   }
-
-   for( int i=0; i<vg_list_size(jeffs); i++ )
-   {
-      rb_collide( jeffs+i, &ground );
-      rb_collide( jeffs+i, &ball );
-      rb_collide( jeffs+i, &ball1 );
-      rb_collide( jeffs+i, &jeff1 );
-   }
-
-   rb_collide( &jeff1, &ground );
-   rb_collide( &jeff1, &blocky );
-   rb_collide( &jeff1, &ball );
-   rb_collide( &jeff1, &ball1 );
-
-   rb_collide( &ball, &ground );
-   rb_collide( &ball1, &ground );
-   rb_collide( &ball1, &ball );
-   rb_collide( &marko, &epic_scene_rb );
-
-   rb_presolve_contacts( rb_contact_buffer, rb_contact_count );
-   for( int i=0; i<8; i++ )
-      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
-
-
-   /* ITERATE */
-   {
-   for( int i=0; i<vg_list_size(jeffs); i++ )
-   {
-      rb_debug( &jeffs[i], (u32[]){ 0xff0000ff, 0xff00ff00, 0xff00ffff,
-                                   0xffff0000, 0xffff00ff, 0xffffff00,
-                                   }[i%6] );
-      rb_iter( jeffs+i );
-   }
-
-   rb_iter( &ball );
-   rb_iter( &ball1 );
-   rb_iter( &jeff1 );
-   rb_iter( &marko );
-   }
-   
-   /* POSITION OVERRIDE */
-   {
-   if(glfwGetKey( vg.window, GLFW_KEY_L ))
-   {
-      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, marko.co );
-      v3_zero( marko.v );
-      v3_zero( marko.w );
-   }
-   if(glfwGetKey( vg.window, GLFW_KEY_K ))
-   {
-      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, ball.co );
-      v3_zero( ball.v );
-      v3_zero( ball.w );
-   }
-   if(glfwGetKey( vg.window, GLFW_KEY_J ))
-   {
-      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, ball1.co );
-      v3_zero( ball1.v );
-      v3_zero( ball1.w );
-   }
-
-   if(glfwGetKey( vg.window, GLFW_KEY_H ))
-   {
-      reorg_jeffs();
-   }
-   }
-   
-   /* UPDATE TRANSFORMS */
-   for( int i=0; i<vg_list_size(jeffs); i++ )
-   {
-      rb_update_transform(jeffs+i);
-   }
-
-   rb_update_transform( &ball );
-   rb_update_transform( &ball1 );
-   rb_update_transform( &jeff1 );
-   rb_update_transform( &marko );
-
-   }
-}
-
-static void physics_test_render(void)
-{
-   m4x4f world_4x4;
-   m4x3_expand( player.camera_inverse, world_4x4 );
-
-   gpipeline.fov = 60.0f;
-   m4x4_projection( vg_pv, gpipeline.fov, 
-         (float)vg_window_x / (float)vg_window_y, 
-         0.1f, 2100.0f );
-
-   m4x4_mul( vg_pv, world_4x4, vg_pv );
-   glEnable( GL_DEPTH_TEST );
-
-   glDisable( GL_DEPTH_TEST );
-   vg_lines_drawall( (float *)vg_pv );
-}
-
-#endif /* PHYSICS_TEST_H */
diff --git a/player.c b/player.c
deleted file mode 100644 (file)
index 551b39b..0000000
--- a/player.c
+++ /dev/null
@@ -1,423 +0,0 @@
-#include "player.h"
-#include "addon.h"
-#include "player_model.h"
-#include "input.h"
-#include "world.h"
-#include "audio.h"
-#include "player_replay.h"
-#include "network.h"
-#include "network_common.h"
-#include "world_routes.h"
-#include "ent_miniworld.h"
-#include "gui.h"
-
-#include "shaders/model_entity.h"
-#include "shaders/model_character_view.h"
-#include "shaders/model_board_view.h"
-
-#include "player_walk.h"
-#include "player_dead.h"
-#include "player_drive.h"
-#include "player_skate.h"
-#include "player_basic_info.h"
-#include "player_glide.h"
-#include <string.h>
-
-i32 k_invert_y = 0;
-struct localplayer localplayer = 
-{
-   .rb = 
-   {
-      .co = { 0,0,0 },
-      .w = { 0,0,0 },
-      .v = { 0,0,0 },
-      .q = { 0,0,0,1 },
-      .to_world = M4X3_IDENTITY,
-      .to_local = M4X3_IDENTITY
-   }
-};
-
-struct player_subsystem_interface *player_subsystems[] = 
-{
-   [k_player_subsystem_walk]  = &player_subsystem_walk,
-   [k_player_subsystem_dead]  = &player_subsystem_dead,
-   [k_player_subsystem_drive] = &player_subsystem_drive,
-   [k_player_subsystem_skate] = &player_subsystem_skate,
-   [k_player_subsystem_basic_info]=&player_subsystem_basic_info,
-   [k_player_subsystem_glide] = &player_subsystem_glide,
-};
-
-int localplayer_cmd_respawn( int argc, const char *argv[] )
-{
-   ent_spawn *rp = NULL, *r;
-   world_instance *world = world_current_instance();
-
-   if( argc == 1 ){
-      rp = world_find_spawn_by_name( world, argv[0] );
-   }
-   else if( argc == 0 ){
-      rp = world_find_closest_spawn( world, localplayer.rb.co );
-   }
-
-   if( !rp )
-      return 0;
-
-   player__spawn( rp );
-   return 1;
-}
-
-void player_init(void)
-{
-   for( u32 i=0; i<k_player_subsystem_max; i++ )
-   {
-      struct player_subsystem_interface *sys = player_subsystems[i];
-      if( sys->system_register ) sys->system_register();
-   }
-
-   vg_console_reg_cmd( "respawn", localplayer_cmd_respawn, NULL );
-   VG_VAR_F32( k_cam_damp );
-   VG_VAR_F32( k_cam_spring );
-   VG_VAR_F32( k_cam_punch );
-   VG_VAR_F32( k_cam_shake_strength );
-   VG_VAR_F32( k_cam_shake_trackspeed );
-   VG_VAR_I32( k_player_debug_info, flags=VG_VAR_PERSISTENT );
-
-#if 0
-   vg_console_reg_var( "cinema", &k_cinema, k_var_dtype_f32, 0 );
-   vg_console_reg_var( "cinema_fixed", &k_cinema_fixed, k_var_dtype_i32, 0 );
-#endif
-   vg_console_reg_var( "invert_y", &k_invert_y,
-                        k_var_dtype_i32, VG_VAR_PERSISTENT );
-}
-
-void player__debugtext( ui_context *ctx, int size, const char *fmt, ... )
-{
-       char buffer[ 1024 ];
-
-   va_list args;
-   va_start( args, fmt );
-   vsnprintf( buffer, 1024, fmt, args );
-   va_end( args );
-
-   ui_text( ctx, g_player_debugger, buffer, size, k_ui_align_left, 0 );
-   g_player_debugger[1] += size*16;
-}
-
-/* 
- * Appearence
- */
-
-void player__use_model( u16 reg_id )
-{
-   addon_cache_unwatch( k_addon_type_player, 
-                        localplayer.playermodel_view_slot );
-   localplayer.playermodel_view_slot = 
-      addon_cache_create_viewer( k_addon_type_player, reg_id );
-}
-
-void player__bind(void)
-{
-   for( u32 i=0; i<k_player_subsystem_max; i++ )
-   {
-      struct player_subsystem_interface *sys = player_subsystems[i];
-
-      if( sys->bind ) sys->bind();
-   }
-}
-
-/*
- * Gameloop events
- * ----------------------------------------------------------------------------
- */
-
-void player__pre_update(void)
-{
-   if( button_down( k_srbind_camera ) && !localplayer.immobile &&
-       (localplayer.subsystem != k_player_subsystem_dead) ){
-      if( localplayer.cam_control.camera_mode == k_cam_firstperson )
-         localplayer.cam_control.camera_mode = k_cam_thirdperson;
-      else
-         localplayer.cam_control.camera_mode = k_cam_firstperson;
-   }
-
-   if( player_subsystems[ localplayer.subsystem ]->pre_update )
-      player_subsystems[ localplayer.subsystem ]->pre_update();
-}
-
-void player__update(void)
-{
-   if( player_subsystems[ localplayer.subsystem ]->update )
-      player_subsystems[ localplayer.subsystem ]->update();
-
-   if( localplayer.glider_orphan && 
-       (skaterift.activity != k_skaterift_replay) )
-      glider_physics( (v2f){0,0} );
-}
-
-void player__post_update(void)
-{
-   struct player_subsystem_interface *sys = 
-      player_subsystems[ localplayer.subsystem ];
-
-   if( sys->post_update ) sys->post_update();
-
-   SDL_AtomicLock( &air_audio_data.sl );
-   air_audio_data.speed = v3_length( localplayer.rb.v ) * vg.time_rate;
-   SDL_AtomicUnlock( &air_audio_data.sl );
-}
-
-/*
- * Applies gate transport to a player_interface
- */
-void player__pass_gate( u32 id )
-{
-   world_instance *world = world_current_instance();
-   skaterift_record_frame( &player_replay.local, 1 );
-
-   /* update boundary hash (network animation) */
-   u16 index = mdl_entity_id_id(id) & ~NETMSG_BOUNDARY_MASK;
-   localplayer.boundary_hash ^= NETMSG_GATE_BOUNDARY_BIT;
-   localplayer.boundary_hash &= ~NETMSG_BOUNDARY_MASK;
-   localplayer.boundary_hash |= index;
-   
-   ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
-   world_routes_fracture( world, gate, localplayer.rb.co, localplayer.rb.v );
-
-   localplayer.gate_waiting = gate;
-   localplayer.deferred_frame_record = 1;
-
-   struct player_cam_controller *cc = &localplayer.cam_control;
-   m4x3_mulv( gate->transport, cc->tpv_lpf, cc->tpv_lpf );
-   m3x3_mulv( gate->transport, cc->cam_velocity_smooth, 
-                               cc->cam_velocity_smooth );
-
-   m4x3_mulv( gate->transport, localplayer.cam.pos, localplayer.cam.pos );
-
-   if( gate->flags & k_ent_gate_nonlocal )
-   {
-      world_default_spawn_pos( world, world->player_co );
-      world_static.active_instance = gate->target;
-      player__clean_refs();
-
-      replay_clear( &player_replay.local );
-   }
-   else 
-   {
-      world_routes_activate_entry_gate( world, gate );
-   }
-   
-   v3f v0;
-   v3_angles_vector( localplayer.angles, v0 );
-   m3x3_mulv( gate->transport, v0, v0 );
-   v3_angles( v0, localplayer.angles );
-
-   audio_lock();
-   audio_oneshot( &audio_gate_pass, 1.0f, 0.0f );
-   audio_unlock();
-}
-
-void player_apply_transport_to_cam( m4x3f transport )
-{
-   /* Pre-emptively edit the camera matrices so that the motion vectors 
-    * are correct */
-   m4x3f transport_i;
-   m4x4f transport_4;
-   m4x3_invert_affine( transport, transport_i );
-   m4x3_expand( transport_i, transport_4 );
-   m4x4_mul( g_render.cam.mtx.pv, transport_4, g_render.cam.mtx.pv );
-   m4x4_mul( g_render.cam.mtx.v,  transport_4, g_render.cam.mtx.v );
-
-   /* we want the regular transform here no the inversion */
-   m4x3_expand( transport, transport_4 );
-   m4x4_mul( world_gates.cam.mtx.pv, transport_4, world_gates.cam.mtx.pv );
-   m4x4_mul( world_gates.cam.mtx.v,  transport_4, world_gates.cam.mtx.v );
-}
-
-void player__im_gui( ui_context *ctx )
-{
-   if( !k_player_debug_info ) return;
-
-   ui_rect box = {
-      vg.window_x - 300,
-      0, 
-      300,
-      vg.window_y
-   };
-
-   ui_fill( ctx, box, (ui_colour(ctx, k_ui_bg)&0x00ffffff)|0x50000000 );
-
-   g_player_debugger[0] = box[0];
-   g_player_debugger[1] = 0;
-   g_player_debugger[2] = 300;
-   g_player_debugger[3] = 32;
-
-   player__debugtext( ctx, 2, "instance #%u", world_static.active_instance );
-
-   char buf[96];
-   for( u32 i=0; i<k_world_max; i++ )
-   {
-      if( world_static.instance_addons[ i ] )
-         addon_alias_uid( &world_static.instance_addons[ i ]->alias, buf );
-      else
-         strcpy( buf, "none" );
-
-      player__debugtext( ctx, 1, "world #%u: %s", i, buf );
-   }
-
-   player__debugtext( ctx, 2, "director" );
-   player__debugtext( ctx, 1, "activity: %s", 
-                     (const char *[]){ [k_skaterift_menu]      = "menu",
-                                       [k_skaterift_replay]    = "replay",
-                                       [k_skaterift_ent_focus] = "ent_focus",
-                                       [k_skaterift_default]   = "default",
-                     } [skaterift.activity] );
-   player__debugtext( ctx, 1, "time_rate: %.4f", skaterift.time_rate );
-
-   player__debugtext( ctx, 2, "player" );
-   player__debugtext( ctx, 1, "angles: " PRINTF_v3f( localplayer.cam.angles ) );
-
-   if( player_subsystems[ localplayer.subsystem ]->im_gui )
-      player_subsystems[ localplayer.subsystem ]->im_gui( ctx );
-
-   skaterift_replay_debug_info( ctx );
-}
-
-void player__setpos( v3f pos )
-{
-   v3_copy( pos, localplayer.rb.co );
-   v3_zero( localplayer.rb.v );
-   rb_update_matrices( &localplayer.rb );
-}
-
-void player__clean_refs(void)
-{
-   replay_clear( &player_replay.local );
-   gui_helper_clear();
-
-   world_static.challenge_target = NULL;
-   world_static.challenge_timer = 0.0f;
-   world_static.active_trigger_volume_count = 0;
-   world_static.last_use = 0.0;
-   world_entity_exit_modal();
-   world_entity_clear_focus();
-
-   localplayer.boundary_hash ^= NETMSG_BOUNDARY_BIT;
-
-   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
-      world_instance *instance = &world_static.instances[i];
-      if( instance->status == k_world_status_loaded ){
-         world_routes_clear( instance );
-      }
-   }
-}
-
-void player__reset(void)
-{
-   v3_zero( localplayer.rb.v );
-   v3_zero( localplayer.rb.w );
-   
-   f32 l = v4_length( localplayer.rb.q );
-   if( (l < 0.9f) || (l > 1.1f) )
-      q_identity( localplayer.rb.q );
-
-   rb_update_matrices( &localplayer.rb );
-
-   localplayer.subsystem = k_player_subsystem_walk;
-   player__walk_reset();
-
-   localplayer.immobile = 0;
-   localplayer.gate_waiting = NULL;
-   localplayer.have_glider = 0;
-   localplayer.glider_orphan = 0;
-   localplayer.drowned = 0;
-
-   v3_copy( localplayer.rb.co, localplayer.cam_control.tpv_lpf );
-   player__clean_refs();
-}
-
-void player__spawn( ent_spawn *rp )
-{
-   player__setpos( rp->transform.co );
-   player__reset();
-}
-
-
-void player__kill(void)
-{
-}
-
-void player__begin_holdout( v3f offset )
-{
-   memcpy( &localplayer.holdout_pose, &localplayer.pose, 
-            sizeof(localplayer.pose) );
-   v3_copy( offset, localplayer.holdout_pose.root_co );
-   localplayer.holdout_time = 1.0f;
-}
-
-void net_sfx_exchange( bitpack_ctx *ctx, struct net_sfx *sfx )
-{
-   bitpack_bytes( ctx, 1, &sfx->system );
-   bitpack_bytes( ctx, 1, &sfx->priority );
-   bitpack_bytes( ctx, 1, &sfx->id );
-   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &sfx->subframe );
-   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &sfx->volume );
-   bitpack_qv3f( ctx, 16, -1024.0f, 1024.0f, sfx->location );
-}
-
-void net_sfx_play( struct net_sfx *sfx )
-{
-   if( sfx->system < k_player_subsystem_max ){
-      struct player_subsystem_interface *sys = player_subsystems[sfx->system];
-      if( sys->sfx_oneshot ){
-         sys->sfx_oneshot( sfx->id, sfx->location, sfx->volume );
-      }
-   }
-};
-
-static struct net_sfx *find_lower_priority_sfx( struct net_sfx *buffer, u32 len, 
-                                                u32 *count, u8 priority ){
-   struct net_sfx *p_sfx = NULL;
-   if( *count < len ){
-      p_sfx = &buffer[ *count ];
-      *count = *count+1;
-   }
-   else {
-      for( u32 i=0; i<len; i++ ){
-         struct net_sfx *a = &buffer[i];
-         if( a->priority < priority ){
-            p_sfx = a;
-            break;
-         }
-      }
-   }
-
-   return p_sfx;
-}
-
-void player__networked_sfx( u8 system, u8 priority, u8 id, 
-                            v3f pos, f32 volume )
-{
-   struct net_sfx sfx,
-         *p_net = find_lower_priority_sfx( 
-               localplayer.sfx_buffer, 4, 
-               &localplayer.sfx_buffer_count, priority ),
-         *p_replay = find_lower_priority_sfx(
-               localplayer.local_sfx_buffer, 2,
-               &localplayer.local_sfx_buffer_count, priority );
-
-   sfx.id = id;
-   sfx.priority = priority;
-   sfx.volume = volume;
-   v3_copy( pos, sfx.location );
-   sfx.system = system;
-
-   /* we only care about subframe in networked sfx. local replays run at a 
-    * high enough framerate. */
-   f32 t = (vg.time_real - network_client.last_frame) / NETWORK_FRAMERATE;
-   sfx.subframe = vg_clampf( t, 0.0f, 1.0f );
-
-   if( p_net ) *p_net = sfx;
-   if( p_replay ) *p_replay = sfx;
-
-   net_sfx_play( &sfx );
-}
diff --git a/player.h b/player.h
deleted file mode 100644 (file)
index ff8c2e1..0000000
--- a/player.h
+++ /dev/null
@@ -1,197 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-
-struct player_cam_controller {
-   enum camera_mode{
-      k_cam_firstperson = 1,
-      k_cam_thirdperson = 0
-   }
-   camera_mode;
-   f32 camera_type_blend;
-
-   v3f fpv_offset,         /* expressed relative to rigidbody */
-       tpv_offset,
-       tpv_offset_extra,
-       fpv_viewpoint,      /* expressed relative to neck bone inverse final*/
-       fpv_offset_smooth,
-       fpv_viewpoint_smooth,
-       tpv_offset_smooth,
-       tpv_lpf,
-       cam_velocity_smooth;
-};
-
-#include "player_common.h"
-#include "network_compression.h"
-#include "player_effects.h"
-#include "player_api.h"
-#include "player_ragdoll.h"
-#include "player_model.h"
-#include "player_render.h"
-
-struct player_subsystem_interface
-{
-   void(*system_register)(void);
-   void(*bind)(void);
-   void(*pre_update)(void);
-   void(*update)(void);
-   void(*post_update)(void);
-   void(*im_gui)( ui_context *ctx );
-   void(*animate)(void);
-   void(*pose)( void *animator, player_pose *pose );
-   void(*effects)( void *animator, m4x3f *final_mtx, struct player_board *board,
-                   struct player_effects_data *effect_data );
-   void(*post_animate)(void);
-
-   void(*network_animator_exchange)( bitpack_ctx *ctx, void *data );
-   void(*sfx_oneshot)( u8 id, v3f pos, f32 volume );
-
-   void(*sfx_comp)(void *animator);
-   void(*sfx_kill)(void);
-
-   void *animator_data;
-   u32 animator_size;
-
-   const char *name;
-};
-
-#define PLAYER_REWIND_FRAMES 60*4
-#define RESET_MAX_TIME 45.0
-
-extern i32 k_invert_y;
-struct localplayer
-{
-   /* transform definition */
-   rigidbody rb;
-   v3f angles;
-
-   bool have_glider, glider_orphan, drowned;
-
-   /*
-    * Camera management
-    * ---------------------------
-    */
-   vg_camera cam;
-   struct player_cam_controller cam_control;
-   f32 cam_trackshake;
-
-   float cam_velocity_influence,
-         cam_velocity_coefficient,
-         cam_velocity_constant,
-         cam_velocity_coefficient_smooth,
-         cam_velocity_constant_smooth,
-         cam_velocity_influence_smooth,
-         cam_dist,
-         cam_dist_smooth;
-
-   v3f cam_land_punch, cam_land_punch_v;
-   ent_gate *gate_waiting;
-   int deferred_frame_record;
-
-   int immobile;
-
-   int rewinded_since_last_gate;
-
-   /* 
-    * Network
-    * --------------------------------------------------
-    */
-   u16 boundary_hash;
-   struct net_sfx {
-      u8 system, priority, id;
-      f32 subframe, volume;
-      v3f location;
-   }
-   sfx_buffer[4],             /* large timeframe 1/10s; for networking */
-   local_sfx_buffer[2];       /* per framerate 1/30s; for replay */
-   u32 sfx_buffer_count, 
-       local_sfx_buffer_count;
-
-   /*
-    * Animation
-    * --------------------------------------------------
-    */
-
-   struct player_ragdoll  ragdoll;
-   struct player_model    fallback_model;
-   struct player_board    fallback_board;
-
-   u16 board_view_slot, playermodel_view_slot;
-
-   player_pose            pose;
-   player_pose            holdout_pose;
-   float                  holdout_time;
-
-   m4x3f                 *final_mtx;
-
-   /*
-    * Subsystems
-    * -------------------------------------------------
-    */
-
-   enum player_subsystem subsystem, 
-                         observing_system; 
-
-   /*
-    * Rendering
-    */
-   mdl_context skeleton_meta;
-   struct skeleton skeleton;
-
-   u8 id_hip,
-      id_chest,
-      id_ik_hand_l,
-      id_ik_hand_r,
-      id_ik_elbow_l,
-      id_ik_elbow_r,
-      id_head,
-      id_foot_l,
-      id_foot_r,
-      id_ik_foot_l,
-      id_ik_foot_r,
-      id_ik_knee_l,
-      id_ik_knee_r,
-      id_wheel_l,
-      id_wheel_r,
-      id_board,
-      id_eyes,
-      id_world;
-
-   u8 skeleton_mirror[32];
-
-   struct player_effects_data effect_data;
-}
-extern localplayer;
-extern struct player_subsystem_interface *player_subsystems[];
-
-/*
- * Gameloop tables
- * ---------------------------------------------------------
- */
-
-void player_init(void);
-void player__debugtext( ui_context *ctx, int size, const char *fmt, ... );
-void player__use_mesh( glmesh *mesh );
-void player__use_model( u16 reg_id );
-
-void player__bind(void);
-void player__pre_update(void);
-void player__update(void);
-void player__post_update(void);
-
-void player__pass_gate( u32 id );
-void player__im_gui( ui_context *ctx );
-void player__setpos( v3f pos );
-void player__spawn( ent_spawn *rp );
-void player__clean_refs(void);
-void player__reset(void);
-void player__kill(void);
-void player__begin_holdout( v3f offset );
-
-int localplayer_cmd_respawn( int argc, const char *argv[] );
-void player_apply_transport_to_cam( m4x3f transport );
-
-void player__clear_sfx_buffer(void);
-void player__networked_sfx( u8 system, u8 priority, u8 id, 
-                            v3f pos, f32 volume );
-void net_sfx_exchange( bitpack_ctx *ctx, struct net_sfx *sfx );
-void net_sfx_play( struct net_sfx *sfx );
diff --git a/player_api.h b/player_api.h
deleted file mode 100644 (file)
index f7f4785..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#pragma once
-#include "model.h"
-
-typedef struct player_instance player_instance;
-typedef struct player_pose player_pose;
-
-struct player_pose{
-   enum player_pose_type {
-      k_player_pose_type_ik,      /* regular IK animation */
-      k_player_pose_type_fk_2,
-   }
-   type;
-
-   v3f root_co;
-   v4f root_q;
-
-   mdl_keyframe keyframes[32];
-
-   struct player_board_pose {
-      f32 lean;
-   }
-   board;
-};
-
-enum player_subsystem{
-   k_player_subsystem_walk = 0,
-   k_player_subsystem_skate = 1,
-   k_player_subsystem_dead = 2,
-   k_player_subsystem_drive = 3,
-   k_player_subsystem_basic_info = 4,
-   k_player_subsystem_glide = 5,
-   k_player_subsystem_max,
-   k_player_subsystem_invalid = 255
-};
diff --git a/player_basic_info.c b/player_basic_info.c
deleted file mode 100644 (file)
index ffc7ae0..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#include "player_basic_info.h"
-#include "network_compression.h"
-
-struct player_basic_info player_basic_info;
-struct player_subsystem_interface player_subsystem_basic_info = 
-{
-   .pose = player__basic_info_pose,
-   .network_animator_exchange = player__basic_info_animator_exchange,
-   .animator_data = &player_basic_info.animator,
-   .animator_size = sizeof(player_basic_info.animator),
-   .name = "Basic Info"
-};
-
-void player__basic_info_animator_exchange(bitpack_ctx *ctx, void *data)
-{
-   struct player_basic_info_animator *animator = data;
-   /* TODO: This range needs to be standardized in a common header */
-   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
-}
-
-void player__basic_info_pose( void *_animator, player_pose *pose )
-{
-   struct player_basic_info_animator *animator = _animator;
-   v3_copy( animator->root_co, pose->root_co );
-   q_identity( pose->root_q );
-   pose->type = k_player_pose_type_fk_2;
-   pose->board.lean = 0.0f;
-
-   for( int i=0; i<localplayer.skeleton.bone_count; i ++ ){
-      v3_zero(pose->keyframes[i].co);
-      q_identity(pose->keyframes[i].q);
-      v3_fill(pose->keyframes[i].s,1.0f);
-   }
-}
diff --git a/player_basic_info.h b/player_basic_info.h
deleted file mode 100644 (file)
index 815be67..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-#include "player.h"
-#include "player_api.h"
-
-struct player_basic_info 
-{
-   struct player_basic_info_animator 
-   {
-      v3f root_co;
-   }
-   animator;
-}
-extern player_basic_info;
-extern struct player_subsystem_interface player_subsystem_basic_info;
-
-void player__basic_info_animator_exchange(bitpack_ctx *ctx, void *data);
-void player__basic_info_pose( void *_animator, player_pose *pose );
-
diff --git a/player_common.c b/player_common.c
deleted file mode 100644 (file)
index 1ecbae9..0000000
+++ /dev/null
@@ -1,290 +0,0 @@
-#include "ent_skateshop.h"
-#include "player.h"
-#include "input.h"
-#include "menu.h"
-#include "vg/vg_perlin.h"
-
-float player_get_heading_yaw(void)
-{
-   v3f xz;
-   q_mulv( localplayer.rb.q, (v3f){ 0.0f,0.0f,1.0f }, xz );
-   return atan2f( xz[0], xz[2] );
-}
-
-static void player_camera_portal_correction(void)
-{
-   if( localplayer.gate_waiting ){
-      /* construct plane equation for reciever gate */
-      v4f plane;
-      q_mulv( localplayer.gate_waiting->q[1], (v3f){0.0f,0.0f,1.0f}, plane );
-      plane[3] = v3_dot( plane, localplayer.gate_waiting->co[1] );
-
-      f32 pol = v3_dot( localplayer.cam.pos, plane ) - plane[3];
-
-      int cleared = (pol < 0.0f) || (pol > 5.0f);
-
-      if( cleared ){
-         vg_success( "Plane cleared\n" );
-      }
-
-      m4x3f inverse;
-      m4x3_invert_affine( localplayer.gate_waiting->transport, inverse );
-
-      /* de-transform camera and player back */
-      v3f v0;
-      m4x3_mulv( inverse, localplayer.cam.pos, localplayer.cam.pos );
-      v3_angles_vector( localplayer.cam.angles, v0 );
-      m3x3_mulv( inverse, v0, v0 );
-      v3_angles( v0, localplayer.cam.angles );
-
-      skeleton_apply_transform( &localplayer.skeleton, inverse, 
-                                 localplayer.final_mtx );
-
-      /* record and re-put things again */
-      if( cleared )
-      {
-         skaterift_record_frame( &player_replay.local, 1 );
-         localplayer.deferred_frame_record = 1;
-
-         skeleton_apply_transform( &localplayer.skeleton, 
-                                    localplayer.gate_waiting->transport,
-                                    localplayer.final_mtx );
-
-         m4x3_mulv( localplayer.gate_waiting->transport, 
-                    localplayer.cam.pos, localplayer.cam.pos );
-         v3_angles_vector( localplayer.cam.angles, v0 );
-         m3x3_mulv( localplayer.gate_waiting->transport, v0, v0 );
-         v3_angles( v0, localplayer.cam.angles );
-         player_apply_transport_to_cam( localplayer.gate_waiting->transport );
-         localplayer.gate_waiting = NULL;
-      }
-   }
-}
-
-void player__cam_iterate(void)
-{
-   struct player_cam_controller *cc = &localplayer.cam_control;
-
-   if( localplayer.subsystem == k_player_subsystem_walk ){
-      v3_copy( (v3f){-0.1f,1.8f,0.0f}, cc->fpv_viewpoint );
-      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
-      v3_copy( (v3f){0.0f,1.8f,0.0f}, cc->tpv_offset );
-   }
-   else if( localplayer.subsystem == k_player_subsystem_glide ){
-      v3_copy( (v3f){-0.15f,1.75f,0.0f}, cc->fpv_viewpoint );
-      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
-      v3_copy( (v3f){0.0f,-1.0f,0.0f}, cc->tpv_offset );
-      v3_add( cc->tpv_offset_extra, cc->tpv_offset, cc->tpv_offset );
-   }
-   else{
-      v3_copy( (v3f){-0.15f,1.75f,0.0f}, cc->fpv_viewpoint );
-      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
-
-      f32 h = vg_lerpf( 0.4f, 1.4f, k_cam_height );
-      v3_copy( (v3f){0.0f,h,0.0f}, cc->tpv_offset );
-      v3_add( cc->tpv_offset_extra, cc->tpv_offset, cc->tpv_offset );
-   }
-
-   localplayer.cam_velocity_constant = 0.25f;
-   localplayer.cam_velocity_coefficient = 0.7f;
-
-   /* lerping */
-
-   if( localplayer.cam_dist_smooth == 0.0f ){
-      localplayer.cam_dist_smooth = localplayer.cam_dist;
-   }
-   else {
-      localplayer.cam_dist_smooth = vg_lerpf(
-            localplayer.cam_dist_smooth,
-            localplayer.cam_dist,
-            vg.time_frame_delta * 8.0f );
-   }
-
-   localplayer.cam_velocity_influence_smooth = vg_lerpf(
-         localplayer.cam_velocity_influence_smooth, 
-         localplayer.cam_velocity_influence,
-         vg.time_frame_delta * 8.0f );
-
-   localplayer.cam_velocity_coefficient_smooth = vg_lerpf(
-         localplayer.cam_velocity_coefficient_smooth,
-         localplayer.cam_velocity_coefficient,
-         vg.time_frame_delta * 8.0f );
-
-   localplayer.cam_velocity_constant_smooth = vg_lerpf(
-         localplayer.cam_velocity_constant_smooth,
-         localplayer.cam_velocity_constant,
-         vg.time_frame_delta * 8.0f );
-
-   enum camera_mode target_mode = cc->camera_mode;
-
-   if( localplayer.subsystem == k_player_subsystem_dead )
-      target_mode = k_cam_thirdperson;
-
-   cc->camera_type_blend = 
-      vg_lerpf( cc->camera_type_blend, 
-               (target_mode == k_cam_firstperson)? 1.0f: 0.0f,
-                5.0f * vg.time_frame_delta );
-
-   v3_lerp( cc->fpv_viewpoint_smooth, cc->fpv_viewpoint,
-            vg.time_frame_delta * 8.0f, cc->fpv_viewpoint_smooth );
-
-   v3_lerp( cc->fpv_offset_smooth, cc->fpv_offset,
-            vg.time_frame_delta * 8.0f, cc->fpv_offset_smooth );
-
-   v3_lerp( cc->tpv_offset_smooth, cc->tpv_offset,
-            vg.time_frame_delta * 8.0f, cc->tpv_offset_smooth );
-
-   /* fov -- simple blend */
-   float fov_skate = vg_lerpf( 97.0f, 135.0f, k_fov ),
-         fov_walk  = vg_lerpf( 90.0f, 110.0f, k_fov );
-
-   localplayer.cam.fov = vg_lerpf( fov_walk, fov_skate, cc->camera_type_blend );
-
-   /* 
-    * first person camera
-    */
-
-   /* position */
-   v3f fpv_pos, fpv_offset;
-   m4x3_mulv( localplayer.final_mtx[ localplayer.id_head-1 ], 
-               cc->fpv_viewpoint_smooth, fpv_pos );
-   m3x3_mulv( localplayer.rb.to_world, cc->fpv_offset_smooth, fpv_offset );
-   v3_add( fpv_offset, fpv_pos, fpv_pos );
-
-   /* angles */
-   v3f velocity_angles;
-   v3_lerp( cc->cam_velocity_smooth, localplayer.rb.v, 4.0f*vg.time_frame_delta, 
-            cc->cam_velocity_smooth );
-
-   v3_angles( cc->cam_velocity_smooth, velocity_angles );
-   velocity_angles[1] *= localplayer.cam_velocity_coefficient_smooth;
-   velocity_angles[1] += localplayer.cam_velocity_constant_smooth;
-
-   float inf_fpv = localplayer.cam_velocity_influence_smooth * 
-                     cc->camera_type_blend,
-         inf_tpv = localplayer.cam_velocity_influence_smooth *
-                     (1.0f-cc->camera_type_blend);
-
-   vg_camera_lerp_angles( localplayer.angles, velocity_angles, 
-                        inf_fpv,
-                        localplayer.angles );
-
-   /*
-    * Third person camera
-    */
-
-   /* no idea what this technique is called, it acts like clamped position based 
-    * on some derivative of where the final camera would end up ....
-    *
-    * it is done in the local basis then transformed back */
-
-   v3f future;
-   v3_muls( localplayer.rb.v, 0.4f*vg.time_frame_delta, future );
-
-   v3f camera_follow_dir = 
-      { -sinf( localplayer.angles[0] ) * cosf( localplayer.angles[1] ),
-         sinf( localplayer.angles[1] ),
-         cosf( localplayer.angles[0] ) * cosf( localplayer.angles[1] ) };
-
-   v3f v0;
-   v3_sub( camera_follow_dir, future, v0 );
-
-   v3f follow_angles;
-   v3_copy( localplayer.angles, follow_angles );
-   follow_angles[0] = atan2f( -v0[0], v0[2] );
-   follow_angles[1] = 0.3f + velocity_angles[1] * 0.2f;
-
-   float ya = atan2f( -cc->cam_velocity_smooth[1], 30.0f );
-
-   follow_angles[1] = 0.3f + ya;
-   vg_camera_lerp_angles( localplayer.angles, follow_angles,
-                        inf_tpv,
-                        localplayer.angles );
-
-   v3f pco;
-   v4f pq;
-   rb_extrapolate( &localplayer.rb, pco, pq );
-   v3_muladds( pco, localplayer.holdout_pose.root_co, 
-               localplayer.holdout_time, pco );
-   v3_lerp( cc->tpv_lpf, pco, 20.0f*vg.time_frame_delta, cc->tpv_lpf );
-
-   /* now move into world */
-   v3f tpv_pos, tpv_offset, tpv_origin;
-
-   /* TODO: whats up with CC and not CC but both sets of variables are doing
-    *       the same ideas just saved in different places?
-    */
-   /* origin */
-   q_mulv( pq, cc->tpv_offset_smooth, tpv_origin );
-   v3_add( tpv_origin, cc->tpv_lpf, tpv_origin );
-
-   /* offset */
-   v3_muls( camera_follow_dir, localplayer.cam_dist_smooth, tpv_offset );
-   v3_muladds( tpv_offset, cc->cam_velocity_smooth, -0.025f, tpv_offset );
-
-   v3_add( tpv_origin, tpv_offset, tpv_pos );
-
-#if 0
-   if( localplayer.subsystem == k_player_subsystem_walk )
-   {
-      v3f fwd, right;
-      v3_angles_vector( localplayer.angles, fwd );
-      v3_cross( fwd, (v3f){0,1.001f,0}, right );
-      right[1] = 0.0f;
-      v3_normalize( right );
-      v3_muladds( tpv_pos, right, 0.5f, tpv_pos );
-   }
-#endif
-
-   /* 
-    * Blend cameras 
-    */
-   v3_lerp( tpv_pos, fpv_pos, cc->camera_type_blend, localplayer.cam.pos );
-   v3_copy( localplayer.angles, localplayer.cam.angles );
-
-   /* Camera shake */
-   f32 speed = v3_length(localplayer.rb.v),
-       strength = k_cam_shake_strength * speed;
-   localplayer.cam_trackshake += 
-      speed*k_cam_shake_trackspeed*vg.time_frame_delta;
-
-   v2f rnd = {vg_perlin_fract_1d( localplayer.cam_trackshake, 1.0f, 4, 20 ),
-              vg_perlin_fract_1d( localplayer.cam_trackshake, 1.0f, 4, 63 ) };
-   v2_muladds( localplayer.cam.angles, rnd, strength, localplayer.cam.angles );
-
-   v3f Fd, Fs, F;
-   v3_muls( localplayer.cam_land_punch_v, -k_cam_damp, Fd );
-   v3_muls( localplayer.cam_land_punch, -k_cam_spring, Fs );
-   v3_muladds( localplayer.cam_land_punch, localplayer.cam_land_punch_v,
-               vg.time_frame_delta, localplayer.cam_land_punch );
-   v3_add( Fd, Fs, F );
-   v3_muladds( localplayer.cam_land_punch_v, F, vg.time_frame_delta,
-               localplayer.cam_land_punch_v );
-   v3_add( localplayer.cam_land_punch, localplayer.cam.pos, 
-           localplayer.cam.pos );
-
-   /* portal transitions */
-   player_camera_portal_correction();
-}
-
-void player_look( v3f angles, float speed )
-{
-   if( vg_ui.ctx.wants_mouse ) return;
-
-   angles[2] = 0.0f;
-
-   v2f mouse_input;
-   v2_copy( vg.mouse_delta, mouse_input );
-   if( k_invert_y ) mouse_input[1] *= -1.0f;
-   v2_muladds( angles, mouse_input, 0.0025f * speed, angles );
-
-   v2f jlook;
-   joystick_state( k_srjoystick_look, jlook );
-
-   angles[0] += jlook[0] * vg.time_frame_delta * 4.0f * speed;
-   float input_y = jlook[1] * vg.time_frame_delta * 4.0f;
-   if( k_invert_y ) input_y *= -1.0f;
-
-   angles[1] += input_y * speed;
-   angles[1] = vg_clampf( angles[1], -VG_PIf*0.5f, VG_PIf*0.5f );
-}
diff --git a/player_common.h b/player_common.h
deleted file mode 100644 (file)
index b32faee..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-#include "player_api.h"
-
-static float
-   k_cam_spring            =  20.0f,
-   k_cam_damp              =  6.7f,
-   k_cam_punch             = -1.0f,
-   k_cam_shake_strength    =  0.0001f,
-   k_cam_shake_trackspeed  =  0.2f;
-
-static i32 k_player_debug_info = 0;
-static ui_rect g_player_debugger;
-
-void player_look( v3f angles, float speed );
-void player__cam_iterate(void);
-f32 player_get_heading_yaw(void);
diff --git a/player_dead.c b/player_dead.c
deleted file mode 100644 (file)
index 43b6211..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-#include "skaterift.h"
-#include "player_dead.h"
-#include "gui.h"
-
-struct player_dead player_dead;
-struct player_subsystem_interface player_subsystem_dead = {
-   .update = player__dead_update,
-   .post_update = player__dead_post_update,
-   .animate = player__dead_animate,
-   .pose = player__dead_pose,
-   .post_animate = player__dead_post_animate,
-   .im_gui = player__dead_im_gui,
-   .bind = player__dead_bind,
-
-   .animator_data = &player_dead.animator,
-   .animator_size = sizeof(player_dead.animator),
-   .network_animator_exchange = player__dead_animator_exchange,
-   .name = "Dead"
-};
-
-void player__dead_update(void)
-{
-   player_ragdoll_iter( &localplayer.ragdoll );
-
-   world_instance *world = world_current_instance();
-   world_water_player_safe( world, 0.2f );
-}
-
-void player__dead_post_update(void){
-   struct ragdoll_part *part = 
-      &localplayer.ragdoll.parts[ localplayer.id_hip-1 ];
-   struct player_dead *d = &player_dead;
-
-   v3f ext_co;
-   v4f ext_q;
-   rb_extrapolate( &part->rb, ext_co, ext_q );
-
-   v3_lerp( d->co_lpf, ext_co, vg.time_frame_delta*4.0f, d->co_lpf );
-   v3_lerp( d->v_lpf,  part->rb.v,  vg.time_frame_delta*4.0f, d->v_lpf );
-   v3_lerp( d->w_lpf,  part->rb.w,  vg.time_frame_delta*4.0f, d->w_lpf );
-   
-   v3_copy( d->co_lpf, localplayer.rb.co );
-   v3_zero( localplayer.rb.v );
-   v3_zero( localplayer.rb.w );
-
-   if( (skaterift.activity == k_skaterift_default) && 
-         button_down(k_srbind_dead_respawn) ){
-      ent_spawn *spawn = world_find_closest_spawn( 
-            world_current_instance(), localplayer.rb.co );
-
-      if( spawn ){
-         v3_copy( spawn->transform.co, localplayer.rb.co );
-         player__reset();
-         srinput.state = k_input_state_resume;
-      }
-      else {
-         vg_error( "No spawns!\n" );
-      }
-   }
-}
-
-void player__dead_animate(void){
-   struct player_dead *d = &player_dead;
-   struct player_dead_animator *animator = &d->animator;
-   struct player_ragdoll *rd = &localplayer.ragdoll;
-   struct skeleton *sk = &localplayer.skeleton;
-
-   m4x3f transforms[ 32 ];
-
-   /* root transform */
-   q_m3x3( localplayer.rb.q, transforms[0] );
-   v3_copy( localplayer.rb.co, transforms[0][3] );
-
-   v4_copy( localplayer.rb.q, animator->transforms[0].q );
-   v3_copy( localplayer.rb.co, animator->transforms[0].co );
-
-   /* colliders with bones transforms */
-   for( int i=0; i<rd->part_count; i++ ){
-      struct ragdoll_part *part = &rd->parts[i];
-
-      m4x3f mtx;
-
-      v4f q_int;
-      v3f co_int;
-
-      float substep = vg.time_fixed_extrapolate;
-      v3_lerp( part->prev_co, part->rb.co, substep, co_int );
-      q_nlerp( part->prev_q, part->rb.q, substep, q_int );
-      v4_copy( part->rb.q, q_int );
-
-      q_m3x3( q_int, mtx );
-      v3_copy( co_int, mtx[3] );
-
-      m4x3_mul( mtx, part->inv_collider_mtx, transforms[part->bone_id] );
-   }
-
-   /* bones without colliders transforms */
-   for( u32 i=1; i<sk->bone_count; i++ ){
-      struct skeleton_bone *sb = &sk->bones[i];
-
-      if( sb->parent && !sb->collider ){
-         v3f delta;
-         v3_sub( sk->bones[i].co, sk->bones[sb->parent].co, delta );
-
-         m4x3f posemtx;
-         m3x3_identity( posemtx );
-         v3_copy( delta, posemtx[3] );
-
-         /* final matrix */
-         m4x3_mul( transforms[sb->parent], posemtx, transforms[i] );
-      }
-   }
-
-   /* measurements */
-   for( u32 i=1; i<sk->bone_count; i++ ){
-      struct skeleton_bone *sb = &sk->bones[i];
-
-      v3_zero( animator->transforms[i].co );
-      q_identity( animator->transforms[i].q );
-
-      m4x3f parent, inverse, local;
-      m3x3_identity( parent );
-      v3_sub( sk->bones[i].co, sk->bones[sb->parent].co, parent[3] );
-      m4x3_mul( transforms[ sb->parent ], parent, parent );
-      m4x3_invert_affine( parent, inverse );
-
-      v3f _s;
-      m4x3_mul( inverse, transforms[i], local );
-      m4x3_decompose( local, animator->transforms[i].co, 
-                             animator->transforms[i].q, _s );
-   }
-}
-
-void player__dead_pose( void *_animator, player_pose *pose )
-{
-   struct player_dead_animator *animator = _animator;
-   struct player_ragdoll *rd = &localplayer.ragdoll;
-   struct skeleton *sk = &localplayer.skeleton;
-
-   pose->type = k_player_pose_type_fk_2;
-   pose->board.lean = 0.0f;
-
-   v3_copy( animator->transforms[0].co, pose->root_co );
-   v4_copy( animator->transforms[0].q, pose->root_q );
-
-   for( u32 i=1; i<sk->bone_count; i++ ){
-      v3_copy( animator->transforms[i].co, pose->keyframes[i-1].co );
-      v4_copy( animator->transforms[i].q, pose->keyframes[i-1].q );
-      v3_fill( pose->keyframes[i-1].s, 1.0f );
-   }
-}
-
-void player__dead_post_animate(void)
-{
-   localplayer.cam_velocity_influence = 1.0f;
-}
-
-void player__dead_im_gui( ui_context *ctx )
-{
-}
-
-void player__dead_transition( enum player_die_type type )
-{
-   if( localplayer.subsystem == k_player_subsystem_dead )
-      return;
-
-   localplayer.subsystem = k_player_subsystem_dead;
-   copy_localplayer_to_ragdoll( &localplayer.ragdoll, type );
-
-   struct ragdoll_part *part = 
-      &localplayer.ragdoll.parts[ localplayer.id_hip-1 ];
-   v3_copy( part->rb.co, player_dead.co_lpf );
-   v3_copy( part->rb.v,  player_dead.v_lpf );
-   v3_copy( part->rb.w,  player_dead.w_lpf );
-
-   gui_helper_clear();
-   vg_str str;
-
-   struct gui_helper *h;
-   if( (h = gui_new_helper(input_button_list[k_srbind_reset], &str) )){
-      vg_strcat( &str, "Rewind" );
-
-      if( world_static.active_instance == k_world_purpose_hub )
-         h->greyed = 1;
-   }
-
-   if( gui_new_helper(input_button_list[k_srbind_dead_respawn], &str ))
-      vg_strcat( &str, "Spawn" );
-}
-
-void player__dead_animator_exchange( bitpack_ctx *ctx, void *data )
-{
-   struct player_dead_animator *animator = data;
-
-   for( u32 i=0; i<localplayer.skeleton.bone_count; i ++ ){
-      bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->transforms[i].co );
-      bitpack_qquat( ctx, animator->transforms[i].q );
-   }
-}
-
-void player__dead_bind(void)
-{
-   struct skeleton *sk = &localplayer.skeleton;
-   player_dead.anim_bail = skeleton_get_anim( sk, "pose_bail_ball" );
-}
diff --git a/player_dead.h b/player_dead.h
deleted file mode 100644 (file)
index 93b0cc3..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-#pragma once
-#include "player.h"
-#include "player_api.h"
-
-struct player_dead
-{
-   v3f co_lpf, v_lpf, w_lpf;
-
-   struct player_dead_animator{
-      struct {
-         v3f co;
-         v4f q;
-      }
-      transforms[ 32 ];
-   }
-   animator;
-
-   struct skeleton_anim *anim_bail;
-}
-extern player_dead;
-extern struct player_subsystem_interface player_subsystem_dead;
-
-void player__dead_update      (void);
-void player__dead_post_update (void);
-void player__dead_animate     (void);
-void player__dead_pose        (void *animator, player_pose *pose);
-void player__dead_post_animate(void);
-void player__dead_im_gui      ( ui_context *ctx );
-void player__dead_bind        (void);
-void player__dead_transition  ( enum player_die_type type );
-void player__dead_animator_exchange( bitpack_ctx *ctx, void *data );
-
diff --git a/player_drive.c b/player_drive.c
deleted file mode 100644 (file)
index 0462037..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-#include "player_drive.h"
-#include "input.h"
-
-struct player_drive player_drive;
-struct player_subsystem_interface player_subsystem_drive = 
-{
-   .pre_update = player__drive_pre_update,
-   .update = player__drive_update,
-   .post_update = player__drive_post_update,
-   .animate = player__drive_animate,
-   .pose = player__drive_pose,
-   .post_animate = player__drive_post_animate,
-   .im_gui = player__drive_im_gui,
-   .bind = player__drive_bind,
-
-   .animator_data = NULL,
-   .animator_size = 0,
-   .name = "Drive"
-};
-
-void player__drive_pre_update(void)
-{
-   drivable_vehicle *vehc = player_drive.vehicle;
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   vehc->steer = vg_lerpf( vehc->steer, steer[0] * 0.4f, 
-                           vg.time_fixed_delta * 8.0f );
-   vehc->drive = steer[1];
-}
-
-void player__drive_update(void){}
-
-void player__drive_post_update(void)
-{
-   v3_copy( player_drive.vehicle->rb.co,localplayer.rb.co );
-   v3_copy( player_drive.vehicle->rb.v, localplayer.rb.v );
-   v4_copy( player_drive.vehicle->rb.q, localplayer.rb.q );
-   v3_copy( player_drive.vehicle->rb.w, localplayer.rb.w );
-}
-
-void player__drive_animate(void){}
-
-void player__drive_pose( void *animator, player_pose *pose )
-{
-   struct skeleton *sk = &localplayer.skeleton;
-
-   skeleton_sample_anim( sk, player_drive.anim_drive, 0.0f, pose->keyframes );
-   v3_copy( localplayer.rb.co, pose->root_co );
-   v4_copy( localplayer.rb.q, pose->root_q );
-}
-
-void player__drive_post_animate(void)
-{
-   if( localplayer.cam_control.camera_mode == k_cam_firstperson )
-      localplayer.cam_velocity_influence = 0.0f;
-   else
-      localplayer.cam_velocity_influence = 1.0f;
-
-   rigidbody *rb = &gzoomer.rb;
-   float yaw = atan2f( -rb->to_world[2][0], rb->to_world[2][2] ),
-       pitch = atan2f
-               ( 
-                   -rb->to_world[2][1], 
-                   sqrtf
-                   (
-                     rb->to_world[2][0]*rb->to_world[2][0] + 
-                     rb->to_world[2][2]*rb->to_world[2][2]
-                   )
-               );
-
-   localplayer.angles[0] = yaw;
-   localplayer.angles[1] = pitch;
-}
-
-void player__drive_im_gui( ui_context *ctx )
-{
-   player__debugtext( ctx, 1, "Nothing here" );
-}
-
-void player__drive_bind(void)
-{
-   struct skeleton *sk = &localplayer.skeleton;
-   player_drive.vehicle = &gzoomer;
-   player_drive.anim_drive = skeleton_get_anim( sk, "idle_cycle+y" );
-}
diff --git a/player_drive.h b/player_drive.h
deleted file mode 100644 (file)
index 9a5649d..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma once
-#include "player.h"
-#include "vehicle.h"
-
-struct player_drive 
-{
-   drivable_vehicle *vehicle;
-   struct skeleton_anim *anim_drive;
-}
-extern player_drive;
-extern struct player_subsystem_interface player_subsystem_drive;
-
-void player__drive_pre_update(void);
-void player__drive_update(void);
-void player__drive_post_update(void);
-void player__drive_animate(void);
-void player__drive_pose( void *animator, player_pose *pose );
-
-void player__drive_post_animate(void);
-void player__drive_im_gui( ui_context *ctx );
-void player__drive_bind(void);
diff --git a/player_effects.c b/player_effects.c
deleted file mode 100644 (file)
index 981c232..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-#include "player.h"
-#include "player_effects.h"
-#include "player_render.h"
-#include "particle.h"
-
-void effect_blink_apply( effect_blink *ef, player_pose *pose, f32 dt )
-{
-   if( ef->t < 0.0f ){
-      ef->t = (1.0f-powf(vg_randf64(&vg.rand),4.0f))*4.0f;
-      ef->l = 0.08f;
-   }
-
-   pose->keyframes[ localplayer.id_eyes-1 ].s[1] = ef->l > 0.0f? 0.2f: 1.0f;
-
-   ef->t -= dt;
-   ef->l -= dt;
-}
-
-void effect_spark_apply( effect_spark *ef, v3f co, v3f v, f32 dt )
-{
-   if( !ef->colour ) return;
-
-   if( ef->t < 0.0f ){
-      ef->t += 0.05f+vg_randf64(&vg.rand)*0.1f;
-
-      v3f dir;
-      v3_copy( v, dir );
-      dir[1] += 1.0f;
-      f32 l = v3_length(dir);
-      v3_muls( dir, 1.0f/l, dir );
-
-      particle_spawn_cone( &particles_grind, co, dir, VG_PIf/2.0f, l, 
-                           4.0f, ef->colour );
-   }
-   else
-      ef->t -= dt;
-}
diff --git a/player_effects.h b/player_effects.h
deleted file mode 100644 (file)
index f148dbc..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-#include "player_render.h"
-
-typedef struct effect_blink effect_blink;
-typedef struct effect_spark effect_spark;
-
-struct effect_blink 
-{
-   f32 t, l;
-};
-
-struct effect_spark 
-{
-   u32 colour;
-   f32 t;
-};
-
-void effect_blink_apply( effect_blink *ef, player_pose *pose, f32 dt );
-void effect_spark_apply( effect_spark *ef, v3f co, v3f v, f32 dt );
-
-struct player_effects_data 
-{
-   effect_blink blink;
-   effect_spark spark, sand;
-};
diff --git a/player_glide.c b/player_glide.c
deleted file mode 100644 (file)
index d9977ce..0000000
+++ /dev/null
@@ -1,706 +0,0 @@
-#include "player_glide.h"
-#include "vg/vg_rigidbody.h"
-#include "scene_rigidbody.h"
-#include "shaders/model_board_view.h"
-#include "shaders/model_entity.h"
-#include "input.h"
-#include "skaterift.h"
-
-#include "player_dead.h"
-#include "player_skate.h"
-
-trail_system trails_glider[] = {
-   {
-      .width = 0.035f,
-      .lifetime = 5.0f,
-      .min_dist = 0.5f
-   },
-   {
-      .width = 0.035f,
-      .lifetime = 5.0f,
-      .min_dist = 0.5f
-   },
-};
-
-struct player_glide player_glide =
-{
-   .parts = {
-      {
-         .co    = { 1.0f, 0.5f, -1.0f },
-         .euler = { VG_TAUf*0.25f,  VG_TAUf*0.125f, 0.0f },
-         .shape = k_rb_shape_capsule,
-         .inf   = { .h = 2.82842712475f, .r = 0.25f },
-      },
-      {
-         .co    = { -1.0f, 0.5f, -1.0f },
-         .euler = { VG_TAUf*0.25f, -VG_TAUf*0.125f, 0.0f },
-         .shape = k_rb_shape_capsule,
-         .inf   = { .h = 2.82842712475f, .r = 0.25f },
-      },
-      {
-         .co    = {  0.0f, 0.5f, 1.0f },
-         .euler = { VG_TAUf*0.25f, VG_TAUf*0.25f, 0.0f },
-         .shape = k_rb_shape_capsule,
-         .inf   = { .h = 6.0f, .r = 0.25f },
-      },
-      {
-         .co    = {  0.0f, -0.5f, 0.0f },
-         .euler = { VG_TAUf*0.25f, VG_TAUf*0.25f, 0.0f },
-         .shape = k_rb_shape_capsule,
-         .inf   = { .h = 2.0f, .r = 0.25f },
-         .is_damage = 1,
-      },
-   }
-};
-
-struct player_subsystem_interface player_subsystem_glide = 
-{
-   .pre_update = player_glide_pre_update,
-   .update = player_glide_update,
-   .post_update = player_glide_post_update,
-   .animate = player_glide_animate,
-   .pose = player_glide_pose,
-   .post_animate = player_glide_post_animate,
-   .network_animator_exchange = player_glide_animator_exchange,
-   .im_gui = player_glide_im_gui,
-   .bind = player_glide_bind,
-
-   .animator_data = &player_glide.animator,
-   .animator_size = sizeof(player_glide.animator),
-   .name = "Glide"
-};
-
-static f32 k_glide_steer = 2.0f,
-           k_glide_cl = 0.04f,
-           k_glide_cs = 0.02f,
-           k_glide_drag = 0.0001f,
-           k_glide_slip_yaw = 0.1f,
-           k_glide_lift_pitch = 0.0f,
-           k_glide_wing_orient = -0.1f,
-           k_glide_balance = 1.0f;
-
-static i32 k_glide_pause = 0;
-
-void player_glide_pre_update(void)
-{
-   if( button_down(k_srbind_use) ){
-      localplayer.subsystem = k_player_subsystem_skate;
-      localplayer.glider_orphan = 1;
-
-      player_skate.state.activity = k_skate_activity_air;
-      player_skate.state.activity_prev = k_skate_activity_air;
-
-      q_mulv( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, player_skate.state.cog );
-      v3_add( player_skate.state.cog, localplayer.rb.co, 
-              player_skate.state.cog );
-      v3_copy( localplayer.rb.v, player_skate.state.cog_v );
-
-      player__begin_holdout( (v3f){0.0f,0.0f,0.0f} );
-      player__skate_reset_animator();
-      player__skate_clear_mechanics();
-      v3_copy( (v3f){0.0f,0.0f,0.0f}, player_skate.state.trick_euler );
-
-      player__approximate_best_trajectory();
-   }
-}
-
-static void massless_accel( rigidbody *rb, v3f delta, v3f impulse ){
-   /* linear */
-   v3_muladds( rb->v, impulse, vg.time_fixed_delta, rb->v );
-   
-   /* Angular velocity */
-   v3f wa;
-   v3_cross( delta, impulse, wa );
-   v3_muladds( rb->w, wa, vg.time_fixed_delta, rb->w );
-}
-
-static void calculate_lift( v3f vl, f32 aoa_bias, 
-                            v3f axis, v3f back, f32 power,
-                            v3f out_force ){
-   v3f up;
-   v3_cross( back, axis, up );
-
-   v3f wind;
-   v3_muladds( vl, axis, -v3_dot(axis,vl), wind );
-   
-   f32 windv2 = v3_length2(wind),
-       aoa    = atan2f( v3_dot( up, wind ), v3_dot( back, wind ) ) + aoa_bias,
-       cl     = aoa / VG_PIf,
-       L      = windv2 * cl * power;
-
-   v3f lift_dir;
-   v3_normalize( wind );
-   v3_cross( wind, axis, lift_dir );
-
-   /* this is where induced drag (from the flappy things) would go */
-   
-   v3_muls( lift_dir, L, out_force );
-}
-
-static void calculate_drag( v3f vl, f32 cd, v3f out_force ){
-   f32 v2 = v3_length2( vl );
-   v3f dir;
-   v3_copy( vl, dir );
-   v3_normalize( dir );
-   v3_muls( vl, -cd*v2, out_force );
-}
-
-/*
- * Returns true if the bottom sphere is hit 
- */
-bool glider_physics( v2f steer )
-{
-   rigidbody *rb = &player_glide.rb;
-
-   /* lift */
-   v3f vl, wl;
-   m3x3_mulv( rb->to_local, rb->v, vl );
-   m3x3_mulv( rb->to_local, rb->w, wl );
-   
-   v3f F, Flift, Fslip, Fdrag, FslipW, FliftW;
-
-   calculate_lift( vl, steer[1]*k_glide_steer, 
-                  (v3f){1,0,0},
-                  (v3f){0,sinf(k_glide_wing_orient),cosf(k_glide_wing_orient)},
-                  k_glide_cl, Flift );
-   v3_copy( Flift, player_glide.info_lift );
-   v3_cross( (v3f){0,0,0}, Flift, FliftW );
-   
-   calculate_lift( vl, 0.0f,
-                  (v3f){0,1,0},(v3f){0,0,1},
-                  k_glide_cs, Fslip );
-   v3_copy( Fslip, player_glide.info_slip );
-   v3_cross( (v3f){0,k_glide_lift_pitch,k_glide_slip_yaw}, Fslip, FslipW );
-
-   calculate_drag( vl, k_glide_drag, Fdrag );
-   v3_copy( Fdrag, player_glide.info_drag );
-
-   v3f balance = {0.0f,-k_glide_balance,0.0f};
-   m3x3_mulv( rb->to_local, balance, balance );
-
-   v3f Fw = {
-       steer[1]*k_glide_steer - balance[2],
-       0.0f,
-      -steer[0]*k_glide_steer + balance[0],
-   };
-
-   if( player_glide.ticker ){
-      player_glide.ticker --;
-      return 0;
-   }
-   player_glide.ticker += k_glide_pause;
-
-   /* apply forces */
-   v3_add( Flift, Fslip, F );
-   v3_add( F, Fdrag, F );
-
-   m3x3_mulv( rb->to_world, F, F );
-   v3_muladds( rb->v, F, vg.time_fixed_delta, rb->v );
-
-   v3_add( Fw, FslipW, Fw );
-   v3_add( Fw, FliftW, Fw );
-   m3x3_mulv( rb->to_world, Fw, Fw );
-   v3_muladds( rb->w, Fw, vg.time_fixed_delta, rb->w );
-
-
-   /* 
-    * collisions & constraints
-    */
-   world_instance *world = world_current_instance();
-   rb_solver_reset();
-
-   bool bottom_hit = 0;
-
-   rigidbody _null = {0};
-   _null.inv_mass = 0.0f;
-   m3x3_zero( _null.iI );
-   for( u32 i=0; i < VG_ARRAY_LEN(player_glide.parts); i ++ ){
-      m4x3f mmdl;
-      m4x3_mul( rb->to_world, player_glide.parts[i].mdl, mmdl );
-
-      if( player_glide.parts[i].shape == k_rb_shape_capsule ){
-         vg_line_capsule( mmdl,
-                          player_glide.parts[i].inf.r,
-                          player_glide.parts[i].inf.h,
-                          VG__BLACK );
-      }
-      else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
-         vg_line_sphere( mmdl, player_glide.parts[i].r, 0 );
-      }
-
-      if( rb_global_has_space() ){
-         rb_ct *buf = rb_global_buffer();
-
-         u32 l = 0;
-         
-         if( player_glide.parts[i].shape == k_rb_shape_capsule ){
-            l = rb_capsule__scene( mmdl, &player_glide.parts[i].inf,
-                                   NULL, world->geo_bh, buf, 
-                                   k_material_flag_ghosts );
-         }
-         else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
-            l = rb_sphere__scene( mmdl, player_glide.parts[i].r,
-                                  NULL, world->geo_bh, buf,
-                                  k_material_flag_ghosts );
-         }
-
-         if( player_glide.parts[i].is_damage && l ){
-            bottom_hit = 1;
-         }
-
-         for( u32 j=0; j<l; j ++ ){
-            buf[j].rba = rb;
-            buf[j].rbb = &_null;
-         }
-
-         rb_contact_count += l;
-      }
-   }
-
-   rb_presolve_contacts( rb_contact_buffer, 
-                         vg.time_fixed_delta, rb_contact_count );
-   for( u32 i=0; i<10; i ++ )
-      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
-   
-   rb_iter( rb );
-   rb_update_matrices( rb );
-
-   return bottom_hit;
-}
-
-void player_glide_update(void)
-{
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   if( glider_physics( steer ) )
-   {
-      vg_info( "player fell off due to glider hitting ground\n" );
-      player__dead_transition( k_player_die_type_generic );
-      localplayer.glider_orphan = 1;
-   }
-
-   if( !world_water_player_safe( world_current_instance(), 1.0f ) )
-      return;
-}
-
-void player_glide_post_update(void)
-{
-   v3_copy( player_glide.rb.co, localplayer.rb.co );
-   v4_copy( player_glide.rb.q, localplayer.rb.q );
-   v3_copy( player_glide.rb.v, localplayer.rb.v );
-   v3_copy( player_glide.rb.w, localplayer.rb.w );
-   rb_update_matrices( &localplayer.rb );
-}
-
-void player_glide_animate(void)
-{
-   struct player_glide *g = &player_glide;
-   struct player_glide_animator *animator = &g->animator;
-   rb_extrapolate( &localplayer.rb, animator->root_co, animator->root_q );
-}
-
-void player_glide_pose( void *_animator, player_pose *pose )
-{
-   struct skeleton *sk = &localplayer.skeleton;
-   struct player_glide_animator *animator = _animator;
-   pose->type = k_player_pose_type_ik;
-   pose->board.lean = 0.0f;
-
-   skeleton_sample_anim( sk, player_glide.anim_glide, 0.0f, pose->keyframes );
-
-   v3f temp;
-   q_mulv( animator->root_q, (v3f){0,-0.5f,0}, temp );
-   v3_add( animator->root_co, temp, pose->root_co );
-
-   v4_copy( animator->root_q, pose->root_q );
-}
-
-void player_glide_post_animate(void)
-{
-   if( localplayer.cam_control.camera_mode == k_cam_firstperson )
-      localplayer.cam_velocity_influence = 0.0f;
-   else
-      localplayer.cam_velocity_influence = 0.0f;
-
-   v3f fwd;
-   v3_muls( localplayer.rb.to_world[2], -1.0f, fwd );
-   v3_angles( fwd, localplayer.angles );
-
-   localplayer.cam_dist = 2.0f + v3_length( localplayer.rb.v )*0.2f;
-}
-
-void player_glide_animator_exchange( bitpack_ctx *ctx, void *data )
-{
-   struct player_glide_animator *animator = data;
-
-   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
-   bitpack_qquat( ctx, animator->root_q );
-}
-
-void player_glide_remote_animator_exchange( bitpack_ctx *ctx, void *data )
-{
-   struct remote_glider_animator *animator = data;
-
-   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
-   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->s );
-   bitpack_qquat( ctx, animator->root_q );
-}
-
-void player_glide_im_gui( ui_context *ctx )
-{
-   player__debugtext( ctx, 1, " lift: %.2f %.2f %.2f", 
-                           player_glide.info_lift[0],
-                           player_glide.info_lift[1],
-                           player_glide.info_lift[2] );
-   player__debugtext( ctx, 1, " slip: %.2f %.2f %.2f", 
-                           player_glide.info_slip[0],
-                           player_glide.info_slip[1],
-                           player_glide.info_slip[2] );
-   player__debugtext( ctx, 1, " drag: %.2f %.2f %.2f", 
-                           player_glide.info_drag[0],
-                           player_glide.info_drag[1],
-                           player_glide.info_drag[2] );
-}
-
-void player_glide_equip_glider(void)
-{
-   if( !localplayer.have_glider ){
-      localplayer.have_glider = 1;
-      localplayer.glider_orphan = 0;
-      player_glide.t = -1.0f;
-   }
-}
-
-static int ccmd_player_glider_spawn( int argc, const char *argv[] ){
-   if( vg_console.cheats ){
-      player_glide_equip_glider();
-   }
-   else {
-      vg_error( "Can't spawn without cheats enabled.\n" );
-   }
-   return 0;
-}
-
-void player_glide_bind(void)
-{
-   u32 mask = VG_VAR_CHEAT|VG_VAR_PERSISTENT;
-   VG_VAR_F32( k_glide_steer, flags=mask );
-   VG_VAR_F32( k_glide_cl, flags=mask );
-   VG_VAR_F32( k_glide_cs, flags=mask );
-   VG_VAR_F32( k_glide_drag, flags=mask );
-   VG_VAR_F32( k_glide_slip_yaw, flags=mask );
-   VG_VAR_F32( k_glide_lift_pitch, flags=mask );
-   VG_VAR_I32( k_glide_pause, flags=mask );
-   VG_VAR_F32( k_glide_balance, flags=mask );
-   VG_VAR_F32( k_glide_wing_orient, flags=mask );
-
-   vg_console_reg_cmd( "spawn_glider", ccmd_player_glider_spawn, NULL );
-
-   f32 mass = 0.0f,k_density = 8.0f;
-   m3x3f I;
-   m3x3_zero( I );
-
-   for( u32 i=0; i<VG_ARRAY_LEN(player_glide.parts); i ++ ){
-      /* create part transform matrix */
-      v4f qp, qy, qr, q;
-      q_axis_angle( qp, (v3f){1,0,0}, player_glide.parts[i].euler[0] );
-      q_axis_angle( qy, (v3f){0,1,0}, player_glide.parts[i].euler[1] );
-      q_axis_angle( qr, (v3f){0,0,1}, player_glide.parts[i].euler[2] );
-
-      q_mul( qr, qy, q );
-      q_mul( q, qp, q );
-
-      q_m3x3( q, player_glide.parts[i].mdl );
-      v3_copy( player_glide.parts[i].co, player_glide.parts[i].mdl[3] );
-
-      /* add it to inertia model */
-
-      if( player_glide.parts[i].shape == k_rb_shape_capsule ){
-         f32 r  = player_glide.parts[i].inf.r,
-             h  = player_glide.parts[i].inf.h,
-             pv = vg_capsule_volume( r, h ),
-             pm = pv * k_density;
-
-         mass += pm;
-
-         m3x3f pI;
-         vg_capsule_inertia( r, h, pm, pI );
-         vg_rotate_inertia( pI, player_glide.parts[i].mdl );
-         vg_translate_inertia( pI, pm, player_glide.parts[i].co );
-         m3x3_add( I, pI, I );
-      }
-      else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
-         f32 r  = player_glide.parts[i].r,
-             pv = vg_sphere_volume( r ),
-             pm = pv * k_density;
-
-         mass += pm;
-         m3x3f pI;
-         vg_sphere_inertia( r, pm, pI );
-         vg_translate_inertia( pI, pm, player_glide.parts[i].co );
-         m3x3_add( I, pI, I );
-      }
-   }
-
-   /* set inverses */
-   m3x3_inv( I, player_glide.rb.iI );
-   player_glide.rb.inv_mass = 1.0f / mass;
-
-   /* resources */
-   struct skeleton *sk = &localplayer.skeleton;
-   player_glide.anim_glide = skeleton_get_anim( sk, "glide_pose" );
-
-   void *alloc = vg_mem.rtmemory;
-   mdl_context *mdl = &player_glide.glider;
-
-   mdl_open( mdl, "models/glider.mdl", alloc );
-   mdl_load_metadata_block( mdl, alloc );
-   mdl_async_full_load_std( mdl );
-
-   /* load trail positions */
-   mdl_array_ptr markers;
-   MDL_LOAD_ARRAY( mdl, &markers, ent_marker, vg_mem.scratch );
-   mdl_close( mdl );
-
-   for( u32 i=0; i<mdl_arrcount( &markers ); i ++ )
-   {
-      ent_marker *marker = mdl_arritm( &markers, i );
-      v3_copy( marker->transform.co, 
-               player_glide.trail_positions[ player_glide.trail_count ++ ] );
-
-      if( player_glide.trail_count == VG_ARRAY_LEN(trails_glider) )
-         break;
-   }
-
-   /* allocate effects */
-   for( u32 i=0; i<VG_ARRAY_LEN(trails_glider); i ++ )
-   {
-      trail_alloc( &trails_glider[i], 200 );
-   }
-}
-
-void player_glide_transition(void)
-{
-   localplayer.subsystem = k_player_subsystem_glide;
-   localplayer.have_glider = 0;
-   world_static.challenge_target = NULL;
-   world_static.challenge_timer = 0.0f;
-   world_static.focused_entity = 0;
-   world_static.last_use = 0.0;
-   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
-      world_instance *instance = &world_static.instances[i];
-      if( instance->status == k_world_status_loaded ){
-         world_routes_clear( instance );
-      }
-   }
-
-   v3_copy( localplayer.rb.co, player_glide.rb.co );
-
-   f32 dir = v3_dot( localplayer.rb.v, localplayer.rb.to_world[2] );
-
-   if( dir > 0.0f ){
-      v4f qyaw;
-      q_axis_angle( qyaw, (v3f){0,1,0}, VG_TAUf*0.5f );
-      q_mul( qyaw, localplayer.rb.q, player_glide.rb.q );
-      q_normalize( player_glide.rb.q );
-   }
-   else 
-      v4_copy( localplayer.rb.q,  player_glide.rb.q );
-
-   v3_copy( localplayer.rb.v,  player_glide.rb.v );
-   v3_copy( localplayer.rb.w,  player_glide.rb.w );
-   rb_update_matrices( &player_glide.rb );
-
-   player__begin_holdout( (v3f){0,0,0} );
-}
-
-void render_glider_model( vg_camera *cam, world_instance *world,
-                          m4x3f mmdl, enum board_shader shader )
-{
-   u32 current_mat = 0xffffffff;
-   glActiveTexture( GL_TEXTURE0 );
-
-   mdl_context *mdl = &player_glide.glider;
-   mesh_bind( &player_glide.glider.mesh );
-
-   for( u32 i=0; i<mdl_arrcount(&mdl->meshs); i ++ )
-   {
-      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
-
-      m4x3f mmmdl;
-      mdl_transform_m4x3( &mesh->transform, mmmdl );
-      m4x3_mul( mmdl, mmmdl, mmmdl );
-
-      if( shader == k_board_shader_player )
-         shader_model_board_view_uMdl( mmmdl );
-      else if( shader == k_board_shader_entity )
-      {
-         m4x4f m4mmmdl;
-         m4x3_expand( mmmdl, m4mmmdl );
-         m4x4_mul( cam->mtx_prev.pv, m4mmmdl, m4mmmdl );
-
-         shader_model_entity_uMdl( mmmdl );
-         shader_model_entity_uPvmPrev( m4mmmdl );
-      }
-
-      for( u32 j=0; j<mesh->submesh_count; j ++ )
-      {
-         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, mesh->submesh_start+j );
-         if( !sm->material_id ) 
-         {
-            vg_error( "Invalid material ID 0\n" );
-            continue;
-         }
-
-         if( sm->material_id != current_mat )
-         {
-            mdl_material *mat = mdl_arritm( &mdl->materials,sm->material_id-1 );
-            GLuint tex = vg.tex_missing;
-
-            if( mat->shader == k_shader_standard )
-            {
-               struct shader_props_standard *props = mat->props.compiled;
-
-               u32 index = props->tex_diffuse-1;
-               mdl_texture *ptex = mdl_arritm( &mdl->textures, index );
-               tex = ptex->glname;
-            }
-
-            glBindTexture( GL_TEXTURE_2D, tex );
-            current_mat = sm->material_id;
-         }
-
-         mdl_draw_submesh( sm );
-      }
-   }
-}
-
-/*
- * TODO: more flexible way to call
- *      - this depends on the current state, but we need to pass a struct in
- *        that can hold that information instead so we can save it into 
- *        the replay
- */
-void player_glide_render( vg_camera *cam, world_instance *world,
-                          player_pose *pose )
-{
-   if( !((localplayer.subsystem == k_player_subsystem_glide) ||
-         (localplayer.observing_system == k_player_subsystem_glide) ||
-          localplayer.have_glider ||
-          localplayer.glider_orphan) )
-      return;
-
-   shader_model_board_view_use();
-   shader_model_board_view_uTexMain( 0 );
-   shader_model_board_view_uCamera( cam->transform[3] );
-   shader_model_board_view_uPv( cam->mtx.pv );
-   shader_model_board_view_uDepthMode(1);
-   depth_compare_bind(
-      shader_model_board_view_uTexSceneDepth,
-      shader_model_board_view_uInverseRatioDepth,
-      shader_model_board_view_uInverseRatioMain,
-      cam );
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_board_view );
-
-   mdl_keyframe kf_res;
-   if( localplayer.glider_orphan ){
-      rb_extrapolate( &player_glide.rb, kf_res.co, kf_res.q );
-      v3_fill( kf_res.s, 1.0f );
-
-      v3f temp;
-      q_mulv( kf_res.q, (v3f){0,-0.5f,0}, temp );
-      v3_add( temp, kf_res.co, kf_res.co );
-   }
-   else {
-      f32 target;
-      if( localplayer.subsystem == k_player_subsystem_glide ) target = 1.0f;
-      else target = 0.0f;
-
-      /* TODO: TEMP */
-      if( skaterift.activity != k_skaterift_replay )
-         vg_slewf( &player_glide.t, target, vg.time_frame_delta * 4.0f );
-
-      mdl_keyframe kf_backpack;
-
-      struct skeleton *sk = &localplayer.skeleton;
-      m4x3_mulv( localplayer.final_mtx[localplayer.id_chest ],
-            sk->bones[localplayer.id_chest].co, 
-            kf_backpack.co );
-
-      v4f qyaw, qpitch, qchest, q;
-      q_axis_angle( qyaw,   (v3f){0,1,0}, VG_TAUf*0.25f );
-      q_axis_angle( qpitch, (v3f){1,0,0}, VG_TAUf*0.25f );
-      m3x3_q( localplayer.final_mtx[ localplayer.id_chest ], qchest );
-
-      q_mul( qyaw, qpitch, q );
-      q_mul( qchest, q, kf_backpack.q );
-      q_normalize( kf_backpack.q );
-
-      f32 scale;
-      if( player_glide.t <= 0.0f ){
-         f32 st  = player_glide.t + 1.0f,
-             sst = vg_smoothstepf(st),
-             isst= 1.0f - sst;
-         scale = vg_lerpf( 0.0f, 0.2f, sst );
-
-         v4f qspin;
-         q_axis_angle( qspin, (v3f){0,0,1}, VG_TAUf * isst * 0.5f );
-         q_mul( kf_backpack.q, qspin, kf_backpack.q );
-         kf_backpack.co[1] += isst * 1.0f;
-         v3_muladds( kf_backpack.co, 
-                     localplayer.final_mtx[ localplayer.id_chest ][0],
-                     isst * 0.25f,
-                     kf_backpack.co );
-      }
-      else{
-         scale = vg_lerpf( 0.2f, 1.0f, vg_smoothstepf(player_glide.t) );
-      }
-
-
-      v3_fill( kf_backpack.s, scale );
-
-      v3_copy( pose->root_co, kf_res.co );
-      v4_copy( pose->root_q, kf_res.q );
-      v3_fill( kf_res.s, scale );
-
-      f32 blend = vg_smoothstepf( vg_maxf( 0, player_glide.t ) );
-      keyframe_lerp( &kf_backpack, &kf_res, blend, &kf_res );
-   }
-      
-   m4x3f mmdl;
-   q_m3x3( kf_res.q, mmdl );
-   m3x3_scale( mmdl, kf_res.s );
-   v3_copy( kf_res.co, mmdl[3] );
-
-   render_glider_model( cam, world, mmdl, k_board_shader_player );
-
-   /* totally FUCKED */
-   v4_copy( kf_res.q, player_glide.remote_animator.root_q );
-   v3_copy( kf_res.co, player_glide.remote_animator.root_co );
-   player_glide.remote_animator.s = kf_res.s[0];
-}
-
-void player_glide_render_effects( vg_camera *cam )
-{
-   v3f co, temp;
-   v4f q;
-   rb_extrapolate( &player_glide.rb, co, q );
-   q_mulv( q, (v3f){0,-0.5f,0}, temp );
-   v3_add( temp, co, co );
-
-   f32 alpha = vg_maxf( (fabsf(player_glide.info_lift[2])-1.0f), 0.0f ) /18.0f;
-
-   for( u32 i=0; i<player_glide.trail_count; i ++ ){
-      v3f vvert;
-      q_mulv( q, player_glide.trail_positions[i], vvert );
-      v3_add( co, vvert, vvert );
-      
-      trail_system_update( &trails_glider[i], vg.time_delta, vvert,
-                           localplayer.rb.to_world[1], alpha );
-                           
-      trail_system_prerender( &trails_glider[i] );
-      trail_system_render( &trails_glider[i], &g_render.cam );
-   }
-}
diff --git a/player_glide.h b/player_glide.h
deleted file mode 100644 (file)
index 03c8260..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-#pragma once
-#include "player.h"
-#include "player_render.h"
-#include "trail.h"
-
-struct player_glide 
-{
-   struct skeleton_anim *anim_glide;
-
-   struct player_glide_animator 
-   {
-      v3f root_co;
-      v4f root_q;
-   }
-   animator;
-
-   /* this sucks */
-   struct remote_glider_animator 
-   {
-      v3f root_co;
-      v4f root_q;
-      f32 s;
-   }
-   remote_animator;
-
-   v3f info_lift,
-       info_slip,
-       info_drag;
-
-   u32 ticker;
-
-   rigidbody rb;
-
-   f32 t;
-
-   struct {
-      v3f co, euler;
-      m4x3f mdl;
-      
-      union {
-         rb_capsule inf;
-         f32 r;
-      };
-
-      enum rb_shape shape;
-      bool is_damage;
-   }
-   parts[4];
-
-   u32 trail_count;
-   v3f trail_positions[2];
-
-   mdl_context glider;
-}
-extern player_glide;
-extern struct player_subsystem_interface player_subsystem_glide;
-
-void player_glide_pre_update(void);
-void player_glide_update(void);
-void player_glide_post_update(void);
-void player_glide_animate(void);
-void player_glide_pose( void *animator, player_pose *pose );
-
-void player_glide_post_animate(void);
-void player_glide_im_gui( ui_context *ctx );
-void player_glide_bind(void);
-void player_glide_transition(void);
-bool glider_physics( v2f steer );
-void player_glide_animator_exchange( bitpack_ctx *ctx, void *data );
-void player_glide_render( vg_camera *cam, world_instance *world,
-                          player_pose *pose );
-void render_glider_model( vg_camera *cam, world_instance *world,
-                          m4x3f mmdl, enum board_shader shader );
-void player_glide_remote_animator_exchange( bitpack_ctx *ctx, void *data );
-void player_glide_equip_glider(void);
-void player_glide_render_effects( vg_camera *cam );
-
diff --git a/player_model.h b/player_model.h
deleted file mode 100644 (file)
index 8d5dd80..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-#include "model.h"
-#include "skeleton.h"
-#include "player_ragdoll.h"
-
-#include "shaders/model_character_view.h"
diff --git a/player_ragdoll.c b/player_ragdoll.c
deleted file mode 100644 (file)
index 1527d10..0000000
+++ /dev/null
@@ -1,630 +0,0 @@
-#pragma once
-#include "vg/vg_rigidbody.h"
-#include "vg/vg_rigidbody_collision.h"
-#include "vg/vg_rigidbody_constraints.h"
-#include "scene_rigidbody.h"
-
-#include "player.h"
-#include "player_dead.h"
-#include "audio.h"
-
-static float k_ragdoll_floatyiness = 20.0f,
-             k_ragdoll_floatydrag  = 1.0f,
-             k_ragdoll_limit_scale = 1.0f,
-             k_ragdoll_spring = 127.0f,
-             k_ragdoll_dampening = 15.0f,
-             k_ragdoll_correction = 0.5f,
-             k_ragdoll_angular_drag = 0.08f,
-             k_ragdoll_active_threshold = 5.0f;
-
-static int   k_ragdoll_div = 1,
-             ragdoll_frame = 0,
-             k_ragdoll_debug_collider = 1,
-             k_ragdoll_debug_constraints = 0;
-
-static int dev_ragdoll_saveload(int argc, const char *argv[]){
-   if( argc != 2 ){
-      vg_info( "Usage: ragdoll load/save filepath\n" );
-      return 1;
-   }
-
-   if( !strcmp(argv[0],"save") ){
-      FILE *fp = fopen( argv[1], "wb" );
-      if( !fp ){
-         vg_error( "Failed to open file\n" );
-         return 1;
-      }
-      fwrite( &localplayer.ragdoll.parts, 
-               sizeof(localplayer.ragdoll.parts), 1, fp );
-      fclose( fp );
-   }
-   else if( !strcmp(argv[0],"load") ){
-      FILE *fp = fopen( argv[1], "rb" );
-      if( !fp ){
-         vg_error( "Failed to open file\n" );
-         return 1;
-      }
-
-      fread( &localplayer.ragdoll.parts, 
-              sizeof(localplayer.ragdoll.parts), 1, fp );
-      fclose( fp );
-   }
-   else {
-      vg_error( "Unknown command: %s (options are: save,load)\n", argv[0] );
-      return 1;
-   }
-   
-   return 0;
-}
-
-void player_ragdoll_init(void)
-{
-   VG_VAR_F32( k_ragdoll_active_threshold );
-   VG_VAR_F32( k_ragdoll_angular_drag );
-   VG_VAR_F32( k_ragdoll_correction );
-   VG_VAR_F32( k_ragdoll_limit_scale );
-   VG_VAR_F32( k_ragdoll_spring );
-   VG_VAR_F32( k_ragdoll_dampening );
-   VG_VAR_I32( k_ragdoll_div );
-   VG_VAR_I32( k_ragdoll_debug_collider );
-   VG_VAR_I32( k_ragdoll_debug_constraints );
-   vg_console_reg_cmd( "ragdoll", dev_ragdoll_saveload, NULL );
-}
-
-void player_init_ragdoll_bone_collider( struct skeleton_bone *bone,
-                                        struct ragdoll_part *rp )
-{
-   f32 k_density = 8.0f,
-       k_inertia_scale = 2.0f;
-
-   m4x3_identity( rp->collider_mtx );
-
-   rp->type = bone->collider;
-   if( bone->collider == k_bone_collider_box ){
-      v3f delta;
-      v3_sub( bone->hitbox[1], bone->hitbox[0], delta );
-      v3_muls( delta, 0.5f, delta );
-      v3_add( bone->hitbox[0], delta, rp->collider_mtx[3] );
-
-      v3_muls( delta, -1.0f, rp->inf.box[0] );
-      v3_copy( delta,        rp->inf.box[1] );
-
-      rp->colour = 0xffcccccc;
-
-      rb_setbody_box( &rp->rb, rp->inf.box, k_density, k_inertia_scale );
-   }
-   else if( bone->collider == k_bone_collider_capsule ){
-      v3f v0, v1, tx, ty;
-      v3_sub( bone->hitbox[1], bone->hitbox[0], v0 );
-
-      int major_axis = 0;
-      float largest = -1.0f;
-
-      for( int i=0; i<3; i ++ ){
-         if( fabsf( v0[i] ) > largest ){
-            largest = fabsf( v0[i] );
-            major_axis = i;
-         }
-      }
-      
-      v3_zero( v1 );
-      v1[ major_axis ] = 1.0f;
-      v3_tangent_basis( v1, tx, ty );
-      
-      rp->inf.capsule.r = (fabsf(v3_dot(tx,v0)) + fabsf(v3_dot(ty,v0))) * 0.25f;
-      rp->inf.capsule.h = fabsf(v0[ major_axis ]);
-
-      /* orientation */
-      v3_muls( tx, -1.0f, rp->collider_mtx[0] );
-      v3_muls( v1, -1.0f, rp->collider_mtx[1] );
-      v3_muls( ty, -1.0f, rp->collider_mtx[2] );
-      v3_add( bone->hitbox[0], bone->hitbox[1], rp->collider_mtx[3] );
-      v3_muls( rp->collider_mtx[3], 0.5f, rp->collider_mtx[3] );
-
-      rp->colour = 0xff000000 | (0xff << (major_axis*8));
-      
-      rb_setbody_capsule( &rp->rb, rp->inf.capsule.r, rp->inf.capsule.h, 
-                           k_density, k_inertia_scale );
-   }
-   else{
-      vg_warn( "type: %u\n", bone->collider );
-      vg_fatal_error( "Invalid bone collider type" );
-   }
-
-   m4x3_invert_affine( rp->collider_mtx, rp->inv_collider_mtx );
-
-   /* Position collider into rest */
-   m3x3_q( rp->collider_mtx, rp->rb.q );
-   v3_add( rp->collider_mtx[3], bone->co, rp->rb.co );
-   v3_zero( rp->rb.v );
-   v3_zero( rp->rb.w );
-   rb_update_matrices( &rp->rb );
-}
-
-/*
- * Get parent index in the ragdoll
- */
-u32 ragdoll_bone_parent( struct player_ragdoll *rd, u32 bone_id )
-{
-   for( u32 j=0; j<rd->part_count; j++ )
-      if( rd->parts[ j ].bone_id == bone_id )
-         return j;
-
-   vg_fatal_error( "Referenced parent bone does not have a rigidbody" );
-   return 0;
-}
-
-/*
- * Setup ragdoll colliders from skeleton
- */
-void setup_ragdoll_from_skeleton( struct skeleton *sk,
-                                  struct player_ragdoll *rd )
-{
-   rd->part_count = 0;
-
-   if( !sk->collider_count )
-      return;
-
-   rd->position_constraints_count = 0;
-   rd->cone_constraints_count = 0;
-
-   for( u32 i=1; i<sk->bone_count; i ++ ){
-      struct skeleton_bone *bone = &sk->bones[i];
-
-      /*
-       * Bones with colliders
-       */
-      if( !(bone->collider) )
-         continue;
-
-      if( rd->part_count > VG_ARRAY_LEN(rd->parts) )
-         vg_fatal_error( "Playermodel has too many colliders" );
-
-      u32 part_id = rd->part_count;
-      rd->part_count ++;
-
-      struct ragdoll_part *rp = &rd->parts[ part_id ];
-      rp->bone_id = i;
-      rp->parent = 0xffffffff;
-
-      player_init_ragdoll_bone_collider( bone, rp );
-      
-      /*
-       * Bones with collider and parent
-       */
-      if( !bone->parent )
-         continue;
-
-      rp->parent = ragdoll_bone_parent( rd, bone->parent );
-      
-      if( bone->orig_bone->flags & k_bone_flag_cone_constraint ){
-         u32 conid = rd->position_constraints_count;
-         rd->position_constraints_count ++;
-
-         struct rb_constr_pos *c = &rd->position_constraints[ conid ];
-
-         struct skeleton_bone *bj = &sk->bones[rp->bone_id];
-         struct ragdoll_part  *pp = &rd->parts[rp->parent];
-         struct skeleton_bone *bp = &sk->bones[pp->bone_id];
-         
-         rd->constraint_associations[conid][0] = rp->parent;
-         rd->constraint_associations[conid][1] = part_id;
-
-         /* Convention: rba -- parent, rbb -- child */
-         c->rba = &pp->rb;
-         c->rbb = &rp->rb;
-
-         v3f delta;
-         v3_sub( bj->co, bp->co, delta );
-         m4x3_mulv( rp->inv_collider_mtx, (v3f){0.0f,0.0f,0.0f}, c->lcb );
-         m4x3_mulv( pp->inv_collider_mtx, delta, c->lca );
-
-
-         mdl_bone *inf = bone->orig_bone;
-
-         struct rb_constr_swingtwist *a = 
-            &rd->cone_constraints[ rd->cone_constraints_count ++ ];
-
-         a->rba = &pp->rb;
-         a->rbb = &rp->rb;
-         a->conet = cosf( inf->conet )-0.0001f;
-         
-         /* Store constraint in local space vectors */
-         m3x3_mulv( c->rba->to_local, inf->conevx, a->conevx );
-         m3x3_mulv( c->rba->to_local, inf->conevy, a->conevy );
-         m3x3_mulv( c->rbb->to_local, inf->coneva, a->coneva );
-         v3_copy( c->lca, a->view_offset );
-         
-         v3_cross( inf->coneva, inf->conevy, a->conevxb );
-         m3x3_mulv( c->rbb->to_local, a->conevxb, a->conevxb );
-
-         v3_normalize( a->conevxb );
-         v3_normalize( a->conevx );
-         v3_normalize( a->conevy );
-         v3_normalize( a->coneva );
-
-         a->conevx[3] = v3_length( inf->conevx );
-         a->conevy[3] = v3_length( inf->conevy );
-
-         rp->use_limits = 1;
-      }
-   }
-}
-
-/*
- * Make avatar copy the ragdoll
- */
-void copy_ragdoll_pose_to_localplayer( struct player_ragdoll *rd )
-{
-   for( int i=0; i<rd->part_count; i++ ){
-      struct ragdoll_part *part = &rd->parts[i];
-
-      m4x3f mtx;
-
-      v4f q_int;
-      v3f co_int;
-
-      float substep = vg.time_fixed_extrapolate;
-      v3_lerp( part->prev_co, part->rb.co, substep, co_int );
-      q_nlerp( part->prev_q, part->rb.q, substep, q_int );
-
-      q_m3x3( q_int, mtx );
-      v3_copy( co_int, mtx[3] );
-
-      m4x3_mul( mtx, part->inv_collider_mtx, 
-                localplayer.final_mtx[part->bone_id] );
-   }
-
-   for( u32 i=1; i<localplayer.skeleton.bone_count; i++ ){
-      struct skeleton_bone *sb = &localplayer.skeleton.bones[i];
-
-      if( sb->parent && !sb->collider ){
-         v3f delta;
-         v3_sub( localplayer.skeleton.bones[i].co, 
-                 localplayer.skeleton.bones[sb->parent].co, delta );
-
-         m4x3f posemtx;
-         m3x3_identity( posemtx );
-         v3_copy( delta, posemtx[3] );
-
-         /* final matrix */
-         m4x3_mul( localplayer.final_mtx[sb->parent], posemtx, 
-                   localplayer.final_mtx[i] );
-      }
-   }
-
-   skeleton_apply_inverses( &localplayer.skeleton, localplayer.final_mtx );
-}
-
-/*
- * Make the ragdoll copy the player model
- */
-void copy_localplayer_to_ragdoll( struct player_ragdoll *rd, 
-                                  enum player_die_type type )
-{
-   v3f centroid;
-
-   v3f *bone_mtx = localplayer.final_mtx[localplayer.id_hip];
-   m4x3_mulv( bone_mtx, 
-              localplayer.skeleton.bones[localplayer.id_hip].co, centroid );
-
-   for( int i=0; i<rd->part_count; i++ ){
-      struct ragdoll_part *part = &rd->parts[i];
-
-      v3f pos, offset;
-      u32 bone = part->bone_id;
-
-      v3f *bone_mtx = localplayer.final_mtx[bone];
-
-      m4x3_mulv( bone_mtx, localplayer.skeleton.bones[bone].co, pos );
-      m3x3_mulv( bone_mtx, part->collider_mtx[3], offset );
-      v3_add( pos, offset, part->rb.co );
-
-      m3x3f r;
-      m3x3_mul( bone_mtx, part->collider_mtx, r );
-      m3x3_q( r, part->rb.q );
-
-      v3f ra, v;
-      v3_sub( part->rb.co, centroid, ra );
-      v3_cross( localplayer.rb.w, ra, v );
-      v3_add( localplayer.rb.v, v, part->rb.v );
-
-      if( type == k_player_die_type_feet ){
-         if( (bone == localplayer.id_foot_l) || 
-             (bone == localplayer.id_foot_r) ){
-            v3_zero( part->rb.v );
-         }
-      }
-
-      v3_copy( localplayer.rb.w, part->rb.w );
-
-      v3_copy( part->rb.co, part->prev_co );
-      v4_copy( part->rb.q, part->prev_q );
-      
-      rb_update_matrices( &part->rb );
-   }
-}
-
-/*
- * Ragdoll physics step
- */
-void player_ragdoll_iter( struct player_ragdoll *rd )
-{
-   world_instance *world = world_current_instance();
-
-   int run_sim = 0;
-   ragdoll_frame ++;
-
-   if( ragdoll_frame >= k_ragdoll_div ){
-      ragdoll_frame = 0;
-      run_sim = 1;
-   }
-
-   rb_solver_reset();
-   
-   float contact_velocities[256];
-
-   rigidbody _null = {0};
-   _null.inv_mass = 0.0f;
-   m3x3_zero( _null.iI );
-
-   for( int i=0; i<rd->part_count; i ++ ){
-      v4_copy( rd->parts[i].rb.q, rd->parts[i].prev_q );
-      v3_copy( rd->parts[i].rb.co, rd->parts[i].prev_co );
-
-      if( rb_global_has_space() ){
-         rb_ct *buf = rb_global_buffer();
-
-         int l;
-         
-         if( rd->parts[i].type == k_bone_collider_capsule ){
-            l = rb_capsule__scene( rd->parts[i].rb.to_world,
-                                   &rd->parts[i].inf.capsule,
-                                   NULL, world->geo_bh, buf,
-                                   k_material_flag_ghosts );
-         }
-         else if( rd->parts[i].type == k_bone_collider_box ){
-            l = rb_box__scene( rd->parts[i].rb.to_world,
-                               rd->parts[i].inf.box,
-                               NULL, world->geo_bh, buf,
-                               k_material_flag_ghosts );
-         }
-         else continue;
-
-         for( int j=0; j<l; j++ ){
-            buf[j].rba = &rd->parts[i].rb;
-            buf[j].rbb = &_null;
-         }
-
-         rb_contact_count += l;
-      }
-   }
-
-   /* 
-    * self-collision
-    */
-   for( int i=0; i<rd->part_count-1; i ++ ){
-      for( int j=i+1; j<rd->part_count; j ++ ){
-         if( rd->parts[j].parent != i ){
-            if( !rb_global_has_space() )
-               break;
-
-            if( rd->parts[j].type != k_bone_collider_capsule )
-               continue;
-
-            if( rd->parts[i].type != k_bone_collider_capsule )
-               continue;
-
-            rb_ct *buf = rb_global_buffer();
-
-            int l = rb_capsule__capsule( rd->parts[i].rb.to_world,
-                                         &rd->parts[i].inf.capsule,
-                                         rd->parts[j].rb.to_world,
-                                         &rd->parts[j].inf.capsule,
-                                         buf );
-
-            for( int k=0; k<l; k++ ){
-               buf[k].rba = &rd->parts[i].rb;
-               buf[k].rbb = &rd->parts[j].rb;
-            }
-
-            rb_contact_count += l;
-         }
-      }
-   }
-
-   if( localplayer.drowned )
-   {
-      for( int j=0; j<rd->part_count; j++ )
-      {
-         struct ragdoll_part *pj = &rd->parts[j];
-
-         if( run_sim )
-         {
-            rb_effect_simple_bouyency( &pj->rb, world->water.plane,
-                                        k_ragdoll_floatyiness,
-                                        k_ragdoll_floatydrag );
-         }
-      }
-   }
-
-   /*
-    * PRESOLVE
-    */
-   for( u32 i=0; i<rb_contact_count; i++ ){
-      rb_ct *ct = &rb_contact_buffer[i];
-
-      v3f rv, ra, rb;
-      v3_sub( ct->co, ct->rba->co, ra );
-      v3_sub( ct->co, ct->rbb->co, rb );
-      rb_rcv( ct->rba, ct->rbb, ra, rb, rv );
-      float     vn = v3_dot( rv, ct->n );
-
-      contact_velocities[i] = vn;
-   }
-
-   rb_presolve_contacts( rb_contact_buffer, vg.time_fixed_delta, 
-                         rb_contact_count );
-   rb_presolve_swingtwist_constraints( rd->cone_constraints,
-                                       rd->cone_constraints_count );
-
-   /* 
-    * DEBUG
-    */
-   if( k_ragdoll_debug_collider ){
-      for( u32 i=0; i<rd->part_count; i ++ ){
-         struct ragdoll_part *rp = &rd->parts[i];
-
-         if( rp->type == k_bone_collider_capsule ){
-            vg_line_capsule( rp->rb.to_world, 
-                             rp->inf.capsule.r, rp->inf.capsule.h, rp->colour );
-         }
-         else if( rp->type == k_bone_collider_box ){
-            vg_line_boxf_transformed( rp->rb.to_world, 
-                                      rp->inf.box, rp->colour );
-         }
-      }
-   }
-
-   if( k_ragdoll_debug_constraints ){
-      rb_debug_position_constraints( rd->position_constraints, 
-                                     rd->position_constraints_count );
-
-      rb_debug_swingtwist_constraints( rd->cone_constraints,
-                                       rd->cone_constraints_count );
-   }
-
-   /*
-    * SOLVE CONSTRAINTS  & Integrate
-    */
-   if( run_sim ){
-      /* the solver is not very quickly converging so... */
-      for( int i=0; i<40; i++ ){
-         if( i<20 ){
-            rb_solve_contacts( rb_contact_buffer, rb_contact_count );
-            rb_solve_swingtwist_constraints( rd->cone_constraints, 
-                                             rd->cone_constraints_count );
-            rb_postsolve_swingtwist_constraints( rd->cone_constraints, 
-                                                 rd->cone_constraints_count );
-         }
-         rb_solve_position_constraints( rd->position_constraints, 
-                                        rd->position_constraints_count );
-      }
-
-      rb_correct_position_constraints( rd->position_constraints,
-                                       rd->position_constraints_count, 
-                                       k_ragdoll_correction * 0.5f );
-      rb_correct_swingtwist_constraints( rd->cone_constraints, 
-                                         rd->cone_constraints_count,
-                                         k_ragdoll_correction * 0.25f );
-
-      for( int i=0; i<rd->part_count; i++ ){
-         rb_iter( &rd->parts[i].rb );
-
-         v3f w;
-         v3_copy( rd->parts[i].rb.w, w );
-         if( v3_length2( w ) > 0.00001f ){
-            v3_normalize( w );
-            v3_muladds( rd->parts[i].rb.w, w, -k_ragdoll_angular_drag,
-                        rd->parts[i].rb.w );
-         }
-      }
-
-      for( int i=0; i<rd->part_count; i++ )
-         rb_update_matrices( &rd->parts[i].rb );
-   }
-
-   rb_ct *stress = NULL;
-   float max_stress = 1.0f;
-
-   for( u32 i=0; i<rb_contact_count; i++ ){
-      rb_ct *ct = &rb_contact_buffer[i];
-
-      v3f rv, ra, rb;
-      v3_sub( ct->co, ct->rba->co, ra );
-      v3_sub( ct->co, ct->rbb->co, rb );
-      rb_rcv( ct->rba, ct->rbb, ra, rb, rv );
-      float vn = v3_dot( rv, ct->n );
-
-      float s = fabsf(vn - contact_velocities[i]);
-      if( s > max_stress ){
-         stress = ct;
-         max_stress = s;
-      }
-   }
-
-   static u32 temp_filter = 0;
-
-   /* 
-    * motorized joints 
-    */
-   if( run_sim && 
-         (v3_length2(player_dead.v_lpf)>(k_ragdoll_active_threshold*
-                                         k_ragdoll_active_threshold)) ){
-      mdl_keyframe anim[32];
-      skeleton_sample_anim( &localplayer.skeleton, player_dead.anim_bail,
-                            0.0f, anim );
-
-      for( u32 i=0; i<rd->cone_constraints_count; i ++ ){
-         rb_constr_swingtwist *st = &rd->cone_constraints[i];
-         rb_constr_pos *pc = &rd->position_constraints[i];
-
-         v3f va, vap;
-
-         m3x3_mulv( st->rbb->to_world, st->coneva, va );
-
-         /* calculate va as seen in rest position, from the perspective of the
-          * parent object, mapped to pose world space using the parents 
-          * transform. thats our target */
-
-         u32 id_p = rd->constraint_associations[i][0],
-             id_a = rd->constraint_associations[i][1];
-
-         struct ragdoll_part *pa = &rd->parts[ id_a ],
-                             *pp = &rd->parts[ id_p ];
-
-         mdl_keyframe *kf = &anim[ pa->bone_id-1 ];
-         m3x3_mulv( pa->collider_mtx, st->coneva, vap );
-         q_mulv( kf->q, vap, vap );
-
-         /* This could be a transfer function */
-         m3x3_mulv( pp->inv_collider_mtx, vap, vap );
-         m3x3_mulv( st->rba->to_world, vap, vap );
-
-         f32 d = v3_dot( vap, va ),
-             a = acosf( vg_clampf( d, -1.0f, 1.0f ) );
-
-         v3f axis;
-         v3_cross( vap, va, axis );
-
-         f32 Fs = -a * k_ragdoll_spring,
-             Fd = -v3_dot( st->rbb->w, axis ) * k_ragdoll_dampening,
-             F  = Fs+Fd;
-
-         v3f torque;
-         v3_muls( axis, F, torque );
-         v3_muladds( st->rbb->w, torque, vg.time_fixed_delta, st->rbb->w );
-
-         /* apply a adjustment to keep velocity at joint 0 */
-#if 0
-         v3f wcb, vcb;
-         m3x3_mulv( st->rbb->to_world, pc->lcb, wcb );
-         v3_cross( torque, wcb, vcb );
-         v3_muladds( st->rbb->v, vcb, vg.time_fixed_delta, st->rbb->v );
-#endif
-      }
-   }
-
-   if( temp_filter ){
-      temp_filter --;
-      return;
-   }
-
-   if( stress ){
-      temp_filter = 20;
-      audio_lock();
-      audio_oneshot_3d( &audio_hits[vg_randu32(&vg.rand)%5], 
-                        stress->co, 20.0f, 1.0f );
-      audio_unlock();
-   }
-}
diff --git a/player_ragdoll.h b/player_ragdoll.h
deleted file mode 100644 (file)
index 08ab5e7..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-#pragma once
-
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
- *
- * Ragdoll system
- */
-
-#include "player_api.h"
-#include "skeleton.h"
-#include "vg/vg_rigidbody.h"
-#include "vg/vg_rigidbody_constraints.h"
-
-struct player_ragdoll{
-   struct ragdoll_part{
-      u32 bone_id;
-      
-      /* Collider transform relative to bone */
-      m4x3f collider_mtx,
-            inv_collider_mtx;
-
-      v4f prev_q;
-      v3f prev_co;
-
-      u32 use_limits;
-      v3f limits[2];
-
-      u32 parent;
-      u32 colour;
-
-      rigidbody rb;
-      enum bone_collider type;
-
-      union {
-         rb_capsule capsule;
-         boxf box;
-      }
-      inf;
-   }
-   parts[32];
-   u32 part_count;
-
-   rb_constr_pos  position_constraints[32];
-   u32            position_constraints_count;
-
-   rb_constr_swingtwist cone_constraints[32];
-   u32                  cone_constraints_count;
-
-   /* TODO: Fix duplicated data */
-   u32 constraint_associations[32][2];
-   int shoes[2];
-};
-
-enum player_die_type {
-   k_player_die_type_generic,
-   k_player_die_type_head,
-   k_player_die_type_feet
-};
-
-void player_ragdoll_init(void);
-void player_init_ragdoll_bone_collider( struct skeleton_bone *bone,
-                                           struct ragdoll_part *rp );
-u32 ragdoll_bone_parent( struct player_ragdoll *rd, u32 bone_id );
-void setup_ragdoll_from_skeleton( struct skeleton *sk,
-                                  struct player_ragdoll *rd );
-void copy_ragdoll_pose_to_localplayer( struct player_ragdoll *rd );
-void copy_localplayer_to_ragdoll( struct player_ragdoll *rd, 
-                                  enum player_die_type type );
-                                   
-void player_debug_ragdoll(void);
-void player_ragdoll_iter( struct player_ragdoll *rd );
diff --git a/player_remote.c b/player_remote.c
deleted file mode 100644 (file)
index 6fdf04f..0000000
+++ /dev/null
@@ -1,1159 +0,0 @@
-#include "player_remote.h"
-#include "skeleton.h"
-#include "player_render.h"
-#include "player_api.h"
-#include "network_common.h"
-#include "addon.h"
-#include "font.h"
-#include "gui.h"
-#include "ent_miniworld.h"
-#include "ent_region.h"
-#include "shaders/model_entity.h"
-#include "vg/vg_steam_friends.h"
-
-struct global_netplayers netplayers;
-
-static i32 k_show_own_name = 0;
-
-static void player_remote_clear( struct network_player *player )
-{
-   addon_cache_unwatch( k_addon_type_player, player->playermodel_view_slot );
-   addon_cache_unwatch( k_addon_type_board, player->board_view_slot );
-
-   memset( player, 0, sizeof(*player) );
-   strcpy( player->username, "unknown" );
-   player->subsystem = k_player_subsystem_invalid;
-}
-
-/* 
- * re-attatches addon_reg pointers on the remote client if we have the same
- * world loaded.
- */
-static void relink_remote_player_worlds( u32 client_id ){
-   struct network_player *player = &netplayers.list[client_id];
-
-   addon_alias q[2];
-   addon_uid_to_alias( player->items[k_netmsg_playeritem_world0], &q[0] );
-   addon_uid_to_alias( player->items[k_netmsg_playeritem_world1], &q[1] );
-
-   /*
-    * currently in 10.23, the hub world will always be the same.
-    * this might but probably wont change in the future
-    */
-   for( u32 i=0; i<k_world_max; i++ ){
-      addon_reg *reg = world_static.instance_addons[ i ];
-
-      player->world_match[i] = 0;
-
-      if( reg ){
-         if( addon_alias_eq( &q[i], &world_static.instance_addons[i]->alias ) )
-            player->world_match[i] = 1;
-      }
-   }
-}
-
-/* 
- * re-attatches addon_reg pointers on the remote client if we have the mod 
- * installed locally.
- *
- * Run if local worlds change
- */
-void relink_all_remote_player_worlds(void)
-{
-   for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
-      struct network_player *player = &netplayers.list[i];
-      if( player->active )
-         relink_remote_player_worlds(i);
-   }
-}
-
-void player_remote_update_friendflags( struct network_player *remote )
-{
-   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
-   remote->isfriend = SteamAPI_ISteamFriends_HasFriend( hSteamFriends,
-                        remote->steamid, k_EFriendFlagImmediate );
-   remote->isblocked = SteamAPI_ISteamFriends_HasFriend( hSteamFriends,
-                        remote->steamid, k_EFriendFlagBlocked );
-}
-
-void player_remote_rx_200_300( SteamNetworkingMessage_t *msg )
-{
-   netmsg_blank *tmp = msg->m_pData;
-
-   if( tmp->inetmsg_id == k_inetmsg_playerjoin ){
-      netmsg_playerjoin *playerjoin = msg->m_pData;
-      if( !packet_minsize( msg, sizeof(*playerjoin) )) return;
-
-      if( playerjoin->index < VG_ARRAY_LEN(netplayers.list) ){
-         struct network_player *player = &netplayers.list[ playerjoin->index ];
-         player_remote_clear( player );
-         player->active = 1;
-         player->steamid = playerjoin->steamid;
-         player_remote_update_friendflags( player );
-
-         /* TODO: interpret the uids */
-         player->board_view_slot = 0;
-         player->playermodel_view_slot = 0;
-
-         struct interp_buffer *buf = &netplayers.interp_data[playerjoin->index];
-         buf->t = -99999999.9;
-         for( u32 i=0; i<VG_ARRAY_LEN(buf->frames); i ++ ){
-            buf->frames[i].active = 0;
-         }
-
-         vg_info( "#%u joined friend: %d, blocked: %d\n", 
-                  playerjoin->index, player->isfriend, player->isblocked );
-      }
-      else {
-         vg_error( "inetmsg_playerjoin: player index out of range\n" );
-      }
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playerleave ){
-      netmsg_playerleave *playerleave = msg->m_pData;
-      if( !packet_minsize( msg, sizeof(*playerleave) )) return;
-      
-      if( playerleave->index < VG_ARRAY_LEN(netplayers.list) ){
-         struct network_player *player = &netplayers.list[ playerleave->index ];
-         player_remote_clear( player );
-         player->active = 0;
-         vg_info( "player leave (%d)\n", playerleave->index );
-      }
-      else {
-         vg_error( "inetmsg_playerleave: player index out of range\n" );
-      }
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playerusername ){
-      netmsg_playerusername *update = msg->m_pData;
-      if( !packet_minsize( msg, sizeof(*update) )) return;
-
-      if( update->index < VG_ARRAY_LEN(netplayers.list) ){
-         struct network_player *player = &netplayers.list[ update->index ];
-
-         network_msgstring( update->name, msg->m_cbSize, sizeof(*update),
-                            player->username, sizeof(player->username) );
-
-         vg_info( "#%u changed username to: %s\n", 
-                  update->index, player->username );
-      }
-      else {
-         vg_error( "inetmsg_playerleave: player index out of range\n" );
-      }
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playerframe ){
-      u32 datasize = msg->m_cbSize - sizeof(netmsg_playerframe);
-      
-      if( datasize > sizeof(union interp_animdata) ){
-         vg_error( "Player frame data exceeds animdata size\n" );
-         return;
-      }
-      
-      netmsg_playerframe *frame = msg->m_pData;
-
-      if( frame->client >= VG_ARRAY_LEN(netplayers.list) ){
-         vg_error( "inetmsg_playerframe: player index out of range\n" );
-         return;
-      }
-
-      if( frame->subsystem >= k_player_subsystem_max ){
-         vg_error( "inetmsg_playerframe: subsystem out of range\n" );
-         return;
-      }
-
-      struct interp_buffer *ib = &netplayers.interp_data[ frame->client ];
-      struct interp_frame *dest = NULL;
-
-      f64 min_time = INFINITY;
-      for( u32 i=0; i<VG_ARRAY_LEN(ib->frames); i++ ){
-         struct interp_frame *ifr = &ib->frames[i];
-
-         if( !ifr->active ){
-            dest = ifr;
-            break;
-         }
-
-         if( ifr->timestamp < min_time ){
-            min_time = ifr->timestamp;
-            dest = ifr;
-         }
-      }
-
-      dest->active = 1;
-      dest->subsystem = frame->subsystem;
-      dest->flags = frame->flags;
-
-      bitpack_ctx ctx = {
-         .mode = k_bitpack_decompress,
-         .buffer = frame->animdata,
-         .buffer_len = datasize,
-         .bytes = 0,
-      };
-      
-      /* animation 
-       * -------------------------------------------------------------*/
-
-      dest->timestamp = frame->timestamp;
-      dest->boundary_hash = frame->boundary_hash;
-
-      struct network_player *player = &netplayers.list[ frame->client ];
-      struct player_subsystem_interface *sys = 
-         player_subsystems[ frame->subsystem ];
-
-      memset( &dest->data, 0, sys->animator_size );
-      if( sys->network_animator_exchange )
-         sys->network_animator_exchange( &ctx, &dest->data );
-      else 
-         bitpack_bytes( &ctx, sys->animator_size, sys->animator_data );
-
-      /* sfx
-       * -------------------------------------------------------------*/
-      
-      for( u32 i=0; i<frame->sound_effects; i ++ ){
-         struct net_sfx sfx;
-         net_sfx_exchange( &ctx, &sfx );
-
-         f64 t = (frame->timestamp - NETWORK_FRAMERATE) + 
-                 (sfx.subframe*NETWORK_FRAMERATE);
-
-         f32 remaining = t - ib->t;
-
-         if( remaining <= 0.0f )
-            net_sfx_play( &sfx );
-         else{
-            struct net_sfx *dst = NULL;
-
-            for( u32 j=0; j<NETWORK_SFX_QUEUE_LENGTH; j ++ ){
-               struct net_sfx *sj = &netplayers.sfx_queue[j];
-               if( sj->system == k_player_subsystem_invalid ){
-                  dst = sj;
-                  break;
-               }
-
-               if( sj->priority < sfx.priority )
-                  dst = sj;
-            }
-
-            *dst = sfx;
-            dst->subframe = remaining;
-         }
-      }
-
-      /* glider
-       * -------------------------------------------------------------*/
-
-      memset( &dest->data_glider, 0, sizeof(struct remote_glider_animator) );
-      if( dest->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
-                         NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
-         player_glide_remote_animator_exchange( &ctx, &dest->data_glider );
-      }
-
-      player->subsystem = frame->subsystem;
-      player->down_bytes += msg->m_cbSize;
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_playeritem ){
-      netmsg_playeritem *item = msg->m_pData;
-      if( !packet_minsize( msg, sizeof(*item)+1 )) return;
-
-      if( item->client >= VG_ARRAY_LEN(netplayers.list) ){
-         vg_error( "inetmsg_playerframe: player index out of range\n" );
-         return;
-      }
-
-      if( item->type_index >= k_netmsg_playeritem_max ){
-         vg_warn( "Client #%d invalid equip type %u\n", 
-                  (i32)item->client, (u32)item->type_index );
-         return;
-      }
-
-      vg_info( "Client #%d equiped: [%s] %s\n", 
-               item->client,
-               (const char *[]){[k_netmsg_playeritem_board]="board",
-                                [k_netmsg_playeritem_player]="player",
-                                [k_netmsg_playeritem_world0]="world0",
-                                [k_netmsg_playeritem_world1]="world1"
-               }[item->type_index], item->uid );
-
-      struct network_player *player = &netplayers.list[ item->client ];
-      char *uid = player->items[ item->type_index ];
-
-      network_msgstring( item->uid, msg->m_cbSize, sizeof(*item),
-                         uid, ADDON_UID_MAX );
-
-      if( item->type_index == k_netmsg_playeritem_board ){
-         addon_cache_unwatch( k_addon_type_board, player->board_view_slot );
-         player->board_view_slot = 
-            addon_cache_create_viewer_from_uid( k_addon_type_board, uid );
-      }
-      else if( item->type_index == k_netmsg_playeritem_player ){
-         addon_cache_unwatch( k_addon_type_player, 
-                              player->playermodel_view_slot );
-         player->playermodel_view_slot =
-            addon_cache_create_viewer_from_uid( k_addon_type_player, uid );
-      }
-      else if( (item->type_index == k_netmsg_playeritem_world0) ||
-               (item->type_index == k_netmsg_playeritem_world1) ){
-         relink_remote_player_worlds( item->client );
-      }
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_chat ){
-      netmsg_chat *chat = msg->m_pData;
-      
-      struct network_player *player = &netplayers.list[ chat->client ];
-      network_msgstring( chat->msg, msg->m_cbSize, sizeof(netmsg_chat),
-                         player->chat, NETWORK_MAX_CHAT );
-      player->chat_time = vg.time_real;
-      vg_info( "[%d]: %s\n", chat->client, player->chat );
-   }
-   else if( tmp->inetmsg_id == k_inetmsg_region ){
-      netmsg_region *region = msg->m_pData;
-      struct network_player *player = &netplayers.list[ region->client ];
-
-      u32 l = network_msgstring( 
-               region->loc, msg->m_cbSize, sizeof(netmsg_region),
-               player->region, NETWORK_REGION_MAX );
-      player->region_flags = region->flags;
-
-      if( l )
-         player->region_flags |= k_ent_region_flag_hasname;
-
-      player->effect_data.spark.colour = region_spark_colour(region->flags);
-   }
-}
-
-/*
- * Write localplayer pose to network
- */
-void remote_player_send_playerframe(void)
-{
-   u8 sysid = localplayer.subsystem;
-   if( sysid >= k_player_subsystem_max ) return;
-
-   struct player_subsystem_interface *sys = player_subsystems[sysid];
-
-   if( sys->animator_size ){
-      u32 max_buf_size = sys->animator_size + sizeof(localplayer.sfx_buffer),
-          base_size = sizeof(struct netmsg_playerframe),
-          max_packet = base_size + max_buf_size;
-
-      netmsg_playerframe *frame = alloca( max_packet );
-      frame->inetmsg_id = k_inetmsg_playerframe;
-      frame->client = 0xff;
-      frame->subsystem = localplayer.subsystem;
-      frame->flags = world_static.active_instance;
-
-      bitpack_ctx ctx = {
-         .mode = k_bitpack_compress,
-         .buffer = frame->animdata,
-         .buffer_len = max_buf_size,
-         .bytes = 0
-      };
-
-      /* animation 
-       * -----------------------------------------------*/
-
-      frame->timestamp = vg.time_real;
-      frame->boundary_hash = localplayer.boundary_hash;
-      if( sys->network_animator_exchange )
-         sys->network_animator_exchange( &ctx, sys->animator_data );
-      else
-         bitpack_bytes( &ctx, sys->animator_size, sys->animator_data );
-
-      /* sfx
-       * ---------------------------------------------*/
-
-      frame->sound_effects = localplayer.sfx_buffer_count;
-      for( u32 i=0; i<localplayer.sfx_buffer_count; i ++ )
-         net_sfx_exchange( &ctx, &localplayer.sfx_buffer[i] );
-
-      /* glider
-       * -------------------------------------------------------------*/
-
-      if( localplayer.have_glider || 
-            (localplayer.subsystem == k_player_subsystem_glide) ) {
-         frame->flags |= NETMSG_PLAYERFRAME_HAVE_GLIDER;
-      }
-
-      if( localplayer.glider_orphan )
-         frame->flags |= NETMSG_PLAYERFRAME_GLIDER_ORPHAN;
-
-      if( frame->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
-                          NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
-         player_glide_remote_animator_exchange( &ctx,    
-                                                &player_glide.remote_animator );
-      }
-
-      /* ------- */
-
-      u32 wire_size = base_size + ctx.bytes;
-      netplayers.up_bytes += wire_size;
-
-      SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
-            hSteamNetworkingSockets, network_client.remote, 
-            frame, wire_size,
-            k_nSteamNetworkingSend_Unreliable, NULL );
-   }
-}
-
-/*
- * Updates network traffic stats
- */
-void remote_player_debug_update(void)
-{
-   if( (vg.time_real - netplayers.last_data_measurement) > 1.0 ){
-      netplayers.last_data_measurement = vg.time_real;
-      u32 total_down = 0;
-
-      for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
-         struct network_player *player = &netplayers.list[i];
-         if( player->active ){
-            total_down += player->down_bytes;
-            player->down_kbs = ((f32)player->down_bytes)/1024.0f;
-            player->down_bytes = 0;
-         }
-      }
-
-      netplayers.down_kbs = ((f32)total_down)/1024.0f;
-      netplayers.up_kbs = ((f32)netplayers.up_bytes)/1024.0f;
-      netplayers.up_bytes = 0;
-   }
-}
-
-/*
- * Debugging information
- */
-void remote_player_network_imgui( ui_context *ctx, m4x4f pv )
-{
-   if( network_client.user_intent == k_server_intent_online )
-   {
-      if( !(steam_ready &&
-         (network_client.state == k_ESteamNetworkingConnectionState_Connected)))
-      {
-         char buf[128];
-         vg_str str;
-         vg_strnull( &str, buf, sizeof(buf) );
-         u32 fg = 0;
-         network_status_string( &str, &fg );
-         ui_text( ctx, (ui_rect){ vg.window_x - 200, 0, 200, 48 }, buf, 1, 
-                  k_ui_align_middle_center, fg );
-      }
-   }
-
-   if( !network_client.network_info ) 
-      return;
-
-   ui_rect panel = { (vg.window_x / 2) - 200, 0, 400, 600 };
-   ui_fill( ctx, panel, (ui_colour(ctx, k_ui_bg)&0x00ffffff)|0x50000000 );
-
-   ctx->font = &vgf_default_title;
-   ui_info( ctx, panel, "Network" );
-   ctx->font = &vgf_default_large;
-   ui_info( ctx, panel, "Status" );
-   ctx->font = &vgf_default_small;
-
-   char buf[512];
-   const char *netstatus = "PROGRAMMING ERROR";
-
-   struct { enum ESteamNetworkingConnectionState state; const char *str; }
-   states[] = {
-          { k_ESteamNetworkingConnectionState_None, "None" },
-          { k_ESteamNetworkingConnectionState_Connecting, 
-         (const char *[]){"Connecting -",
-                          "Connecting /",
-                          "Connecting |",
-                          "Connecting \\",
-                          }[(u32)(vg.time_real/0.25) & 0x3 ] },
-          { k_ESteamNetworkingConnectionState_FindingRoute, "Finding Route" },
-          { k_ESteamNetworkingConnectionState_Connected, "Connected" },
-          { k_ESteamNetworkingConnectionState_ClosedByPeer, "Closed by peer" },
-          { k_ESteamNetworkingConnectionState_ProblemDetectedLocally, 
-         "Problem Detected Locally" },
-          { k_ESteamNetworkingConnectionState_FinWait, "Fin Wait" },
-          { k_ESteamNetworkingConnectionState_Linger, "Linger" },
-          { k_ESteamNetworkingConnectionState_Dead, "Dead" }
-   };
-   for( u32 i=0; i<VG_ARRAY_LEN(states); i ++ )
-   {
-      if( states[i].state == network_client.state )
-      {
-         netstatus = states[i].str;
-         break;
-      }
-   }
-   snprintf( buf, 512, "Network: %s", netstatus );
-   ui_info( ctx, panel, buf );
-   ui_info( ctx, panel, "---------------------" );
-
-   if( network_client.state == k_ESteamNetworkingConnectionState_Connected )
-   {
-      ui_info( ctx, panel, "#-1: localplayer" );
-      
-      snprintf( buf, 512, "U%.3f/D%.3fkbs", 
-                netplayers.up_kbs, netplayers.down_kbs );
-      ui_info( ctx, panel, buf );
-
-      for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ )
-      {
-         struct network_player *player = &netplayers.list[i];
-         if( player->active )
-         {
-            const char *sysname = "invalid";
-
-            if( player->subsystem < k_player_subsystem_max )
-            {
-               sysname = player_subsystems[ player->subsystem ]->name;
-            }
-            snprintf( buf, 512, "#%u: %s [%s] D%.1fkbs", 
-                      i, player->username, sysname, player->down_kbs );
-            ui_info( ctx, panel, buf );
-         }
-      }
-   }
-   else 
-   {
-      ui_info( ctx, panel, "offline" );
-   }
-}
-
-static void remote_player_effect( struct network_player *player, 
-                                  player_pose *final_pose ){
-   /* effects */
-}
-
-/*
- * write the remote players final_mtx 
- */
-static void pose_remote_player( u32 index, 
-                                struct interp_frame *f0,
-                                struct interp_frame *f1 ){
-
-   struct network_player *player = &netplayers.list[ index ];
-
-   struct interp_buffer *buf = &netplayers.interp_data[ index ];
-   struct skeleton *sk = &localplayer.skeleton;
-   m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*index ];
-   struct player_board_pose *board_pose = &netplayers.board_poses[index];
-
-   struct player_subsystem_interface *sys0 = player_subsystems[f0->subsystem],
-                                     *sys1 = NULL;
-
-   struct player_board *board = 
-      addon_cache_item_if_loaded( k_addon_type_board, player->board_view_slot );
-
-   player_pose pose0, pose1, posed;
-   sys0->pose( &f0->data, &pose0 );
-
-   u8 instance_id = 0;
-
-   f32 t = 0.0f;
-
-   if( f1 ){
-      t = (buf->t - f0->timestamp) / (f1->timestamp - f0->timestamp);
-      t = vg_clampf( t, 0.0f, 1.0f );
-
-      sys1 = player_subsystems[f1->subsystem];
-      sys1->pose( &f1->data, &pose1 );
-
-      u16 bounds = f0->boundary_hash^f1->boundary_hash;
-
-      if( bounds & NETMSG_BOUNDARY_BIT )
-         t = 1.0f;
-
-      if( bounds & NETMSG_GATE_BOUNDARY_BIT ){
-         /* TODO: Extra work retransforming the root_co, instance_id.. etc */
-         t = 1.0f;
-      }
-
-      instance_id = f1->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
-      lerp_player_pose( &pose0, &pose1, t, &posed );
-      effect_blink_apply( &player->effect_data.blink, &posed, vg.time_delta );
-
-      apply_full_skeleton_pose( sk, &posed, final_mtx );
-
-      if( t < 0.5f ){
-         if( sys0->effects ) 
-            sys0->effects( &f0->data, final_mtx, board, &player->effect_data );
-      }
-      else{
-         if( sys1->effects ) 
-            sys1->effects( &f1->data, final_mtx, board, &player->effect_data );
-      }
-
-      memcpy( board_pose, &posed.board, sizeof(*board_pose) );
-   }
-   else {
-      instance_id = f0->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
-      effect_blink_apply( &player->effect_data.blink, &pose0, vg.time_delta );
-      apply_full_skeleton_pose( sk, &pose0, final_mtx );
-      if( sys0->effects ) 
-         sys0->effects( &f0->data, final_mtx, board, &player->effect_data );
-      memcpy( board_pose, &pose0.board, sizeof(*board_pose) );
-   }
-
-   if( f0->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
-                    NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
-      player->render_glider = 1;
-
-      v3f co;
-      v4f q;
-      f32 s;
-
-      if( f1 ){
-         v3_lerp( f0->data_glider.root_co, f1->data_glider.root_co, t, co );
-         q_nlerp( f0->data_glider.root_q,  f1->data_glider.root_q,  t, q );
-         s = vg_lerpf( f0->data_glider.s, f1->data_glider.s, t );
-      }
-      else {
-         v3_copy( f0->data_glider.root_co, co );
-         v4_copy( f0->data_glider.root_q, q );
-         s = f0->data_glider.s;
-      }
-
-      v3f *mtx = netplayers.glider_mtx[ index ];
-      q_m3x3( q, mtx );
-      m3x3_scalef( mtx, s );
-      v3_copy( co, mtx[3] );
-   }
-   else
-      player->render_glider = 0;
-
-   if( player->world_match[ instance_id ] )
-      player->active_world = &world_static.instances[ instance_id ];
-}
-
-/* 
- * animate remote player and store in final_mtx
- */
-void animate_remote_player( u32 index )
-{
-   /*
-    * Trys to keep the cursor inside the buffer
-    */
-   f64 min_time = -999999999.9,
-       max_time =  999999999.9,
-       abs_max_time = -999999999.9;
-
-   struct interp_frame *minframe = NULL,
-                       *maxframe = NULL,
-                       *abs_max_frame = NULL;
-
-   struct interp_buffer *buf = &netplayers.interp_data[index];
-   for( u32 i=0; i<VG_ARRAY_LEN(buf->frames); i ++ ){
-      struct interp_frame *ifr = &buf->frames[i];
-
-      if( ifr->active ){
-         if( (ifr->timestamp > min_time) && (ifr->timestamp < buf->t) ){
-            min_time = ifr->timestamp;
-            minframe = ifr;
-         }
-
-         if( (ifr->timestamp < max_time) && (ifr->timestamp > buf->t) ){
-            max_time = ifr->timestamp;
-            maxframe = ifr;
-         }
-
-         if( ifr->timestamp > abs_max_time ){
-            abs_max_time = ifr->timestamp;
-            abs_max_frame = ifr;
-         }
-      }
-   }
-
-   struct network_player *player = &netplayers.list[ index ];
-   if( player->active != 2 )
-      player->active_world = NULL;
-   
-   if( minframe && maxframe ){
-      pose_remote_player( index, minframe, maxframe );
-      buf->t += vg.time_frame_delta;
-   }
-   else {
-      buf->t = abs_max_time - 0.25;
-
-      if( abs_max_frame )
-         pose_remote_player( index, abs_max_frame, NULL );
-      else 
-         return;
-   }
-}
-
-/*
- * Update full final_mtx for all remote players
- */
-void animate_remote_players(void)
-{
-   for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i ++ ){
-      struct network_player *player = &netplayers.list[i];
-      if( !player->active ) continue;
-
-      animate_remote_player( i );
-   }
-}
-
-/*
- * Draw remote players
- */
-void render_remote_players( world_instance *world, vg_camera *cam )
-{
-   u32 draw_list[ NETWORK_MAX_PLAYERS ],
-       draw_list_count = 0,
-       gliders = 0;
-
-   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
-      struct network_player *player = &netplayers.list[i];
-      if( !player->active || player->isblocked ) continue;
-      if( player->active_world != world ) continue;
-
-#if 0
-      if( !player->isfriend && 
-            (world-world_static.instances == k_world_purpose_hub)) continue;
-#endif
-
-      draw_list[draw_list_count ++] = i;
-
-      if( player->render_glider )
-         gliders ++;
-   }
-
-   struct skeleton *sk = &localplayer.skeleton;
-
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-
-   for( u32 j=0; j<draw_list_count; j ++ ){
-      u32 index = draw_list[j];
-
-      struct network_player *player = &netplayers.list[index];
-      m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*index ];
-
-      struct player_model *model = 
-         addon_cache_item_if_loaded( k_addon_type_player, 
-                                     player->playermodel_view_slot );
-
-      if( !model ) model = &localplayer.fallback_model;
-      render_playermodel( cam, world, 0, model, sk, final_mtx );
-
-      struct player_board *board = 
-         addon_cache_item_if_loaded( k_addon_type_board,
-                                     player->board_view_slot );
-      render_board( cam, world, board, final_mtx[localplayer.id_board],
-                     &netplayers.board_poses[ index ], k_board_shader_player );
-   }
-
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-
-   if( !gliders )
-      return;
-
-   /* TODO: we really, really only need to do all this once. at some point
-    *       PLEASE figure out a good place to do this once per frame!
-    */
-   
-   shader_model_entity_use();
-   shader_model_entity_uTexMain( 0 );
-   shader_model_entity_uCamera( cam->transform[3] );
-   shader_model_entity_uPv( cam->mtx.pv );
-   
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
-
-   for( u32 j=0; j<draw_list_count; j ++ ){
-      u32 index = draw_list[j];
-
-      struct network_player *player = &netplayers.list[index];
-      if( !player->render_glider ) continue;
-
-      if( player->render_glider ){
-         v3f *glider_mtx = netplayers.glider_mtx[ index ];
-         render_glider_model( cam, world, glider_mtx, k_board_shader_entity );
-      }
-   }
-}
-
-static int remote_players_randomize( int argc, const char *argv[] ){
-   for( int i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
-      struct network_player *player = &netplayers.list[i];
-
-      player->active = (vg_randu32(&vg.rand) & 0x1)? 2: 0;
-      player->isfriend = vg_randu32(&vg.rand) & vg_randu32(&vg.rand) & 0x1;
-      player->isblocked = vg_randu32(&vg.rand) & 
-                          vg_randu32(&vg.rand) & 
-                          vg_randu32(&vg.rand) & 0x1;
-      player->world_match[ 0 ] = vg_randu32(&vg.rand) & 0x1;
-      player->world_match[ 1 ] = 0;
-
-      if( player->world_match[0] )
-         player->active_world = &world_static.instances[0];
-      else
-         player->active_world = NULL;
-
-      for( int i=0; i<sizeof(player->username)-1; i ++ ){
-         player->username[i] = 'a' + (vg_randu32(&vg.rand) % 30);
-         player->username[i+1] = '\0';
-
-         if( (vg_randu32(&vg.rand) % 8) == 3 )
-            break;
-      }
-
-      for( int i=0; i<3; i ++ ){
-         player->medals[i] = vg_randu32(&vg.rand) % 3;
-      }
-
-      v3f pos;
-
-      vg_rand_sphere( &vg.rand, pos );
-      v3_muladds( localplayer.rb.co, pos, 100.0f,
-                  netplayers.final_mtx[ i*localplayer.skeleton.bone_count][3] );
-   }
-
-   return 0;
-}
-
-void remote_players_init(void)
-{
-   vg_console_reg_cmd( "add_test_players", remote_players_randomize, NULL );
-   vg_console_reg_var( "k_show_own_name", &k_show_own_name, 
-                       k_var_dtype_i32, 0 );
-   for( u32 i=0; i<NETWORK_SFX_QUEUE_LENGTH; i ++ ){
-      netplayers.sfx_queue[i].system = k_player_subsystem_invalid;
-   }
-}
-
-void remote_sfx_pre_update(void)
-{
-   for( u32 i=0; i<NETWORK_SFX_QUEUE_LENGTH; i ++ ){
-      struct net_sfx *si = &netplayers.sfx_queue[i];
-
-      if( si->system != k_player_subsystem_invalid ){
-         si->subframe -= vg.time_frame_delta;
-         if( si->subframe <= 0.0f ){
-            net_sfx_play( si );
-            si->system = k_player_subsystem_invalid;
-         }
-      }
-   }
-}
-
-/*
- * animator->root_co of remote player
- */
-static void remote_player_position( int id, v3f out_co ){
-   struct skeleton *sk = &localplayer.skeleton;
-   m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*id ];
-   v3_copy( final_mtx[0][3], out_co );
-}
-
-enum remote_player_gui_type {
-   k_remote_player_gui_type_stranger,
-   k_remote_player_gui_type_friend,
-   k_remote_player_gui_type_you,
-};
-
-/*
- * Given players' root_co, get the screen point where we should draw tag info.
- */
-static int player_tag_position( m4x4f pv, v3f root_co, ui_point out_point ){
-   v4f wpos;
-   v3_copy( root_co, wpos );
-   wpos[1] += 2.0f;
-   wpos[3] = 1.0f;
-
-   m4x4_mulv( pv, wpos, wpos );
-
-   if( wpos[3] > 0.0f ){
-      v2_muls( wpos, (1.0f/wpos[3]) * 0.5f, wpos );
-      v2_add( wpos, (v2f){ 0.5f, 0.5f }, wpos );
-
-      float k_max = 32000.0f;
-      
-      out_point[0] = vg_clampf(wpos[0] * vg.window_x, -k_max, k_max );
-      out_point[1] = vg_clampf((1.0f-wpos[1]) * vg.window_y, -k_max, k_max );
-      return 1;
-   }
-   else
-      return 0;
-}
-
-/*
- * Draw chat box relative to the root tag position on the screen
- */
-static void chat_box( ui_context *ctx,
-                      ui_point tag_root, f64 time, const char *message )
-{
-   if( (vg.time_real - time) > 15.0 ) 
-      return;
-
-   ui_rect wr;
-   wr[2] = ui_text_line_width( ctx, message ) + 8;
-   wr[3] = ctx->font->ch + 2;
-   wr[0] = tag_root[0]-(wr[2]/2);
-   wr[1] = tag_root[1] - wr[3] - 8;
-
-   ui_fill( ctx, wr, ui_opacity( ui_colour(ctx, k_ui_bg), 0.23f ) );
-   ui_text( ctx, wr, message, 1, k_ui_align_middle_center, 0 );
-}
-
-/*
- * Draw full imgui for remote player
- */
-static void remote_player_nametag( ui_context *ctx, ui_point tag_root, 
-                                   struct network_player *player )
-{
-   ctx->font = &vgf_default_large;
-
-   ui_rect wr;
-   wr[2] = VG_MAX( ui_text_line_width( ctx, player->username ), 140 ) + 8;
-   wr[3] = 32;
-   wr[0] = tag_root[0]-(wr[2]/2);
-   wr[1] = tag_root[1]-(wr[3]/2);
-
-   ui_fill( ctx, wr, ui_opacity( ui_colour(ctx, k_ui_bg), 0.23f ) );
-   ui_text( ctx, wr, player->username, 1, k_ui_align_middle_center, 0 );
-   ctx->font = &vgf_default_small;
-
-   /* medals */
-   int cols = 0;
-   for( int i=0; i<3; i ++ )
-      if( player->medals[i] ) 
-         cols ++;
-
-   char buf[32];
-   vg_str str;
-
-   if( cols )
-   {
-      f32 w = (f32)wr[2] / (f32)cols;
-      cols = 0;
-
-      for( int i=0; i<3; i ++ )
-      {
-         if( player->medals[i] )
-         {
-            ui_rect col = { wr[0] + (f32)cols*w, wr[1] + wr[3], 
-                            w, ctx->font->ch };
-
-            vg_strnull( &str, buf, 32 );
-#if 0
-            vg_strcatch( &str, (char)k_SRglyph_vg_circle );
-#endif
-            vg_strcati32( &str, player->medals[i] );
-
-            ui_text( ctx, col, buf, 1, k_ui_align_middle_center, 
-                     ui_colour( ctx, (enum ui_scheme_colour[]){ 
-                        k_ui_yellow, k_ui_gray, k_ui_orange }[i] ) );
-            
-            cols ++;
-         }
-      }
-   }
-}
-
-static void remote_player_world_gui( ui_context *ctx, m4x4f pv, v3f root_co, 
-                                     struct network_player *player  ){
-   ui_point tag_root;
-   if( !player_tag_position( pv, root_co, tag_root ) )
-      return;
-
-   if( player )
-   {
-      remote_player_nametag( ctx, tag_root, player );
-      chat_box( ctx, tag_root, player->chat_time, player->chat );
-   }
-   else 
-   {
-      if( netplayers.chatting )
-         chat_box( ctx, tag_root, vg.time_real, netplayers.chat_buffer );
-      else
-      {
-         chat_box( ctx, tag_root, 
-                   netplayers.chat_time, netplayers.chat_message );
-      }
-   }
-}
-
-static void remote_player_gui_info( ui_context *ctx, ui_rect box, 
-                                    const char *username,
-                                    const char *activity, 
-                                    enum remote_player_gui_type type,
-                                    int in_world ){
-
-   f32 opacity = in_world? 0.6f: 0.3f;
-
-   if( type == k_remote_player_gui_type_you )
-      ui_fill( ctx, box, ui_opacity( 0xff555555, opacity ) );
-   else
-      ui_fill( ctx, box, ui_opacity( 0xff000000, opacity ) );
-
-   if( type == k_remote_player_gui_type_friend )
-      ui_outline( ctx, box, -1, ui_opacity( 0xff00c4f0, opacity ), 0 );
-
-   ui_rect top, bottom;
-   ui_split_ratio( box, k_ui_axis_h, 0.6666f, 1, top, bottom );
-
-   u32 fg;
-   
-   if( type == k_remote_player_gui_type_friend )
-      fg = ui_colour( ctx, k_ui_yellow + (in_world? k_ui_brighter: 0) );
-   else
-      fg = ui_colour( ctx, in_world? k_ui_fg: k_ui_fg+4 );
-
-   ctx->font = &vgf_default_large;
-   ui_text( ctx, top, username, 1, k_ui_align_middle_center, fg );
-   ctx->font = &vgf_default_small;
-   ui_text( ctx, bottom, activity, 1, k_ui_align_middle_center, fg );
-}
-
-void remote_players_imgui_lobby( ui_context *ctx )
-{
-   if( network_client.user_intent == k_server_intent_online ){
-      if( !(steam_ready &&
-         (network_client.state == k_ESteamNetworkingConnectionState_Connected)))
-      {
-         return;
-      }
-   }
-
-   ui_px y = 50, width = 200, height = 42, gap = 2,
-         x = vg.window_x - width;
-
-   ctx->font = &vgf_default_large;
-   ui_text( ctx, (ui_rect){ x, 0, width, height }, 
-            "In World", 1, k_ui_align_middle_center, 0 );
-   ctx->font = &vgf_default_small;
-
-
-   ui_rect us = { x, y, width, height };
-   /* FIXME: your location */
-   remote_player_gui_info( ctx, us, steam_username_at_startup, "you",
-                           k_remote_player_gui_type_you, 1 );
-   y += height + gap;
-
-   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ )
-   {
-      struct network_player *player = &netplayers.list[i];
-      if( !player->active || player->isblocked ) continue;
-
-      int in_same_world = player->active_world == world_current_instance();
-      if( !player->isfriend && !in_same_world )
-         continue;
-
-      const char *location = in_same_world? "": "another world";
-      if( player->region_flags & k_ent_region_flag_hasname ){
-         location = player->region;
-      }
-      
-      ui_rect box = { x, y, width, height };
-      remote_player_gui_info( ctx, box, player->username, location,
-                              player->isfriend, in_same_world );
-      y += height + gap;
-   }
-}
-
-void remote_players_imgui_world( ui_context *ctx, 
-                                 world_instance *world, m4x4f pv,
-                                 f32 max_dist, int geo_cull )
-{
-   ui_flush( ctx, k_ui_shader_colour, NULL );
-
-   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i++ )
-   {
-      struct network_player *player = &netplayers.list[i];
-      if( player->active )
-      {
-         v3f co;
-         remote_player_position( i, co );
-
-         if( !player->active_world )
-            continue;
-         if( !player->isfriend && 
-               (world-world_static.instances == k_world_purpose_hub)) continue;
-
-         /* their in our active subworld */
-         if( player->active_world != world )
-         {
-            m4x3_mulv( global_miniworld.mmdl, co, co );
-            co[1] -= 2.0f; /* HACK lol */
-         }
-
-         f32 d2 = v3_dist2( co, localplayer.rb.co );
-
-         if( d2 > (max_dist*max_dist) )
-            continue;
-
-         f32 dist = sqrtf(d2);
-         f32 opacity = 0.95f * sqrtf(((max_dist-dist)/max_dist));
-
-         if( geo_cull ){
-            ray_hit hit;
-            hit.dist = dist;
-
-            v3f dir;
-            v3_sub( co, g_render.cam.pos, dir );
-            v3_normalize( dir );
-
-            if( ray_world( world, g_render.cam.pos, dir, &hit, 
-                           k_material_flag_ghosts ) ){
-               opacity *= 0.5f;
-            }
-         }
-
-         player->opacity = vg_lerpf( player->opacity, opacity,
-                                     vg.time_frame_delta * 2.0f );
-         
-         remote_player_world_gui( ctx, pv, co, player );
-
-         vg_ui.colour[3] = player->opacity;
-         ui_flush( ctx, k_ui_shader_colour, NULL );
-      }
-   }
-
-   vg_ui.colour[3] = 1.0f;
-   remote_player_world_gui( ctx, pv, localplayer.rb.co, NULL );
-   ui_flush( ctx, k_ui_shader_colour, NULL );
-}
-
-static void chat_escape( ui_context *ctx )
-{
-   netplayers.chatting = -1;
-}
-
-static void chat_enter( ui_context *ctx, char *buf, u32 len ){
-   vg_strncpy( buf, netplayers.chat_message, NETWORK_MAX_CHAT, 
-               k_strncpy_always_add_null );
-   netplayers.chatting = -1;
-   netplayers.chat_time = vg.time_real;
-   chat_send_message( buf );
-}
-
-void remote_players_chat_imgui( ui_context *ctx )
-{
-   if( netplayers.chatting == 1 )
-   {
-      ui_rect box = { 0, 0, 400, 40 },
-              window = { 0, 0, vg.window_x, vg.window_y };
-      ui_rect_center( window, box );
-
-      struct ui_textbox_callbacks callbacks = 
-      {
-         .enter = chat_enter,
-         .escape = chat_escape
-      };
-
-      ui_textbox( ctx, box, NULL, 
-                  netplayers.chat_buffer, NETWORK_MAX_CHAT, 1,
-                  UI_TEXTBOX_AUTOFOCUS, &callbacks );
-   }
-   else 
-   {
-      if( netplayers.chatting == -1 )
-      {
-         netplayers.chatting = 0;
-         srinput.state = k_input_state_resume;
-      }
-      else 
-      {
-         if( (skaterift.activity == k_skaterift_default) && 
-               button_down( k_srbind_chat ) ){
-            netplayers.chatting = 1;
-            netplayers.chat_buffer[0] = '\0';
-            srinput.state = k_input_state_pause;
-         }
-      }
-   }
-}
diff --git a/player_remote.h b/player_remote.h
deleted file mode 100644 (file)
index 3e4b67e..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-#pragma once
-#include "player.h"
-#include "network.h"
-#include "network_common.h"
-#include "player_render.h"
-#include "player_effects.h"
-#include "player_api.h"
-
-#include "player_skate.h"
-#include "player_walk.h"
-#include "player_dead.h"
-#include "player_basic_info.h"
-#include "player_glide.h"
-
-#define NETWORK_SFX_QUEUE_LENGTH 12
-
-struct global_netplayers
-{
-   struct network_player {
-      int active, isfriend, isblocked;
-      u64 steamid;
-      u16 board_view_slot, playermodel_view_slot;
-      enum player_subsystem subsystem;
-
-      /* this is set IF they exist in a world that we have loaded */
-      world_instance *active_world;
-      int world_match[ k_world_max ];
-      u32 location_pstr; /* TODO: valid if active_world set. */
-
-      char username[ NETWORK_USERNAME_MAX ];
-      char items[k_netmsg_playeritem_max][ADDON_UID_MAX];
-      char chat[ NETWORK_MAX_CHAT ];
-      char region[ NETWORK_REGION_MAX ];
-      u32 region_flags;
-      f64 chat_time;
-
-      /* ui */
-      u32 medals[3];
-      f32 opacity;
-
-      u32 down_bytes;
-      f32 down_kbs;
-      
-      struct player_effects_data effect_data;
-      bool render_glider;
-   }
-   list[ NETWORK_MAX_PLAYERS ];
-
-   struct interp_buffer {
-      /* collect the most recent 6 frames of animation data */
-      struct interp_frame {
-         int active;
-         f64 timestamp;
-         enum player_subsystem subsystem;
-
-         u8 flags;
-         u16 boundary_hash;
-
-         union interp_animdata {
-            /* these aren't accessed directly, just used to take the 
-             * max(sizeof) all systems */
-            struct player_skate_animator __skate;
-            struct player_walk_animator __walk;
-            struct player_dead_animator __dead;
-            struct player_basic_info_animator __basic;
-         } 
-         data;
-
-         struct remote_glider_animator data_glider;
-      }
-      frames[ NETWORK_BUFFERFRAMES ];
-
-      f64 t;
-   }
-   interp_data[ NETWORK_MAX_PLAYERS ];
-
-   struct net_sfx sfx_queue[ NETWORK_SFX_QUEUE_LENGTH ];
-
-   m4x3f *final_mtx,
-         *glider_mtx;
-   struct player_board_pose board_poses[ NETWORK_MAX_PLAYERS ];
-
-   u32 up_bytes;
-   f32 up_kbs, down_kbs;
-   f64 last_data_measurement;
-
-   int chatting;
-   char chat_buffer[ NETWORK_MAX_CHAT ], chat_message[ NETWORK_MAX_CHAT ];
-   f64 chat_time;
-}
-extern netplayers;
-
-void player_remote_rx_200_300( SteamNetworkingMessage_t *msg );
-void remote_player_debug_update(void);
-void remote_player_send_playerframe(void);
-void animate_remote_player( u32 index );
-void animate_remote_players(void);
-void render_remote_players( world_instance *world, vg_camera *cam );
-void relink_all_remote_player_worlds(void);
-void player_remote_update_friendflags( struct network_player *remote );
-void remote_players_init(void);
-void remote_sfx_pre_update(void);
-void remote_player_network_imgui( ui_context *ctx, m4x4f pv );
-void remote_players_imgui_world( ui_context *ctx, world_instance *world, 
-                                 m4x4f pv, f32 max_dist, int geo_cull );
-void remote_players_imgui_lobby( ui_context *ctx );
-void remote_players_chat_imgui( ui_context *ctx );
diff --git a/player_render.c b/player_render.c
deleted file mode 100644 (file)
index 15dec10..0000000
+++ /dev/null
@@ -1,629 +0,0 @@
-#include "player.h"
-#include "player_render.h"
-#include "vg/vg_camera.h"
-#include "player_model.h"
-#include "ent_skateshop.h"
-#include "audio.h"
-#include "input.h"
-
-#include "shaders/model_character_view.h"
-#include "shaders/model_board_view.h"
-#include "shaders/model_entity.h"
-#include "shaders/model_board_view.h"
-#include "depth_compare.h"
-
-#include "network.h"
-#include "player_remote.h"
-#include "player_glide.h"
-
-void player_load_animation_reference( const char *path )
-{
-   mdl_context *meta = &localplayer.skeleton_meta;
-   mdl_open( meta, path, vg_mem.rtmemory );
-   mdl_load_metadata_block( meta, vg_mem.rtmemory );
-   mdl_load_animation_block( meta, vg_mem.rtmemory );
-   mdl_close( meta );
-
-   struct skeleton *sk = &localplayer.skeleton;
-   skeleton_setup( sk, vg_mem.rtmemory, meta );
-
-   localplayer.id_world      = skeleton_bone_id( sk, "world" );
-   localplayer.id_hip        = skeleton_bone_id( sk, "hips" );
-   localplayer.id_chest      = skeleton_bone_id( sk, "chest" );
-   localplayer.id_ik_hand_l  = skeleton_bone_id( sk, "hand.IK.L" );
-   localplayer.id_ik_hand_r  = skeleton_bone_id( sk, "hand.IK.R" );
-   localplayer.id_ik_elbow_l = skeleton_bone_id( sk, "elbow.L" );
-   localplayer.id_ik_elbow_r = skeleton_bone_id( sk, "elbow.R" );
-   localplayer.id_head       = skeleton_bone_id( sk, "head" );
-   localplayer.id_foot_l  = skeleton_bone_id( sk, "foot.L" );
-   localplayer.id_foot_r  = skeleton_bone_id( sk, "foot.R" );
-   localplayer.id_ik_foot_l  = skeleton_bone_id( sk, "foot.IK.L" );
-   localplayer.id_ik_foot_r  = skeleton_bone_id( sk, "foot.IK.R" );
-   localplayer.id_board      = skeleton_bone_id( sk, "board" );
-   localplayer.id_wheel_l    = skeleton_bone_id( sk, "wheel.L" );
-   localplayer.id_wheel_r    = skeleton_bone_id( sk, "wheel.R" );
-   localplayer.id_ik_knee_l  = skeleton_bone_id( sk, "knee.L" );
-   localplayer.id_ik_knee_r  = skeleton_bone_id( sk, "knee.R" );
-   localplayer.id_eyes       = skeleton_bone_id( sk, "eyes" );
-
-   for( i32 i=0; i<sk->bone_count; i ++ ){
-      localplayer.skeleton_mirror[i] = 0;
-   }
-
-   for( i32 i=1; i<sk->bone_count-1; i ++ ){
-      struct skeleton_bone *si = &sk->bones[i];
-
-      char tmp[64];
-      vg_str str;
-      vg_strnull( &str, tmp, 64 );
-      vg_strcat( &str, si->name );
-
-      char *L = vg_strch( &str, 'L' );
-      if( !L ) continue;
-      u32 len = L-tmp;
-
-      for( i32 j=i+1; j<sk->bone_count; j ++ ){
-         struct skeleton_bone *sj = &sk->bones[j];
-
-         if( !strncmp( si->name, sj->name, len ) ){
-            if( sj->name[len] == 'R' ){
-               localplayer.skeleton_mirror[i] = j;
-               localplayer.skeleton_mirror[j] = i;
-               break;
-            }
-         }
-      }
-   }
-
-   setup_ragdoll_from_skeleton( sk, &localplayer.ragdoll );
-
-   /* allocate matrix buffers for localplayer and remote players */
-   u32 mtx_size = sizeof(m4x3f)*sk->bone_count;
-   localplayer.final_mtx = vg_linear_alloc( vg_mem.rtmemory, mtx_size );
-   netplayers.final_mtx = vg_linear_alloc( vg_mem.rtmemory, 
-                                           mtx_size*NETWORK_MAX_PLAYERS );
-   netplayers.glider_mtx = vg_linear_alloc( vg_mem.rtmemory,
-                                            sizeof(m4x3f)*NETWORK_MAX_PLAYERS );
-}
-
-/* TODO: Standard model load */
-
-void dynamic_model_load( mdl_context *ctx,
-                         struct dynamic_model_1texture *mdl, 
-                         const char *path, u32 *fixup_table )
-{
-   if( !mdl_arrcount( &ctx->textures ) )
-      vg_fatal_error( "No texture in model" );
-
-   mdl_texture *tex0 = mdl_arritm( &ctx->textures, 0 );
-   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
-   mdl_fread_pack_file( ctx, &tex0->file, data );
-
-   vg_tex2d_load_qoi_async( data, tex0->file.pack_size,
-                            VG_TEX2D_NEAREST|VG_TEX2D_CLAMP,
-                            &mdl->texture );
-
-   mdl_async_load_glmesh( ctx, &mdl->mesh, fixup_table );
-}
-
-void dynamic_model_unload( struct dynamic_model_1texture *mdl )
-{
-   mesh_free( &mdl->mesh );
-   glDeleteTextures( 1, &mdl->texture );
-}
-
-/* TODO: allow error handling */
-void player_board_load( struct player_board *board, const char *path )
-{
-   vg_linear_clear( vg_mem.scratch );
-
-   mdl_context ctx;
-   mdl_open( &ctx, path, vg_mem.scratch );
-   mdl_load_metadata_block( &ctx, vg_mem.scratch );
-
-   dynamic_model_load( &ctx, &board->mdl, path, NULL );
-
-   mdl_array_ptr markers;
-   MDL_LOAD_ARRAY( &ctx, &markers, ent_marker, vg_mem.scratch );
-
-   /* TODO: you get put into a new section, the above is standard mdl loads. */
-   for( int i=0; i<4; i++ )
-      board->wheels[i].indice_count = 0;
-   for( int i=0; i<2; i++ )
-      board->trucks[i].indice_count = 0;
-   board->board.indice_count = 0;
-
-   for( u32 i=0; i<mdl_arrcount(&ctx.meshs); i++ ){
-      mdl_mesh *mesh = mdl_arritm( &ctx.meshs, i );
-
-      if( mdl_entity_id_type( mesh->entity_id ) != k_ent_marker )
-         continue;
-
-      u32 index = mdl_entity_id_id( mesh->entity_id );
-      ent_marker *marker = mdl_arritm( &markers, index );
-
-      mdl_submesh *sm0 = mdl_arritm( &ctx.submeshs, mesh->submesh_start );
-      
-      const char *alias = mdl_pstr( &ctx, marker->pstr_alias );
-      u32 lr = marker->transform.co[0] > 0.0f? 1: 0,
-          fb = marker->transform.co[2] > 0.0f? 0: 1;
-
-      if( !strcmp( alias, "wheel" ) ){
-         u32 id = fb<<1 | lr;
-         board->wheels[ id ] = *sm0;
-         v3_copy( marker->transform.co, board->wheel_positions[ id ] );
-      }
-      else if( !strcmp( alias, "board" ) ){
-         board->board = *sm0;
-         v3_copy( marker->transform.co, board->board_position );
-      }
-      else if( !strcmp( alias, "truck" ) ){
-         board->trucks[ fb ] = *sm0;
-         v3_copy( marker->transform.co, board->truck_positions[ fb ] );
-      }
-   }
-
-   mdl_close( &ctx );
-}
-
-void player_board_unload( struct player_board *board )
-{
-   dynamic_model_unload( &board->mdl );
-}
-
-void player_model_load( struct player_model *board, const char *path)
-{
-   vg_linear_clear( vg_mem.scratch );
-
-   mdl_context ctx;
-   mdl_open( &ctx, path, vg_mem.scratch );
-   mdl_load_metadata_block( &ctx, vg_mem.scratch );
-
-   if( !ctx.armatures.count )
-      vg_fatal_error( "No armature in playermodel\n" );
-
-   mdl_armature *armature = mdl_arritm( &ctx.armatures, 0 );
-
-   u32 fixup_table[ armature->bone_count+1 ];
-   for( u32 i=0; i<armature->bone_count+1; i ++ )
-      fixup_table[i] = 0;
-
-   for( u32 i=1; i<localplayer.skeleton.bone_count; i ++ ){
-      struct skeleton_bone *sb = &localplayer.skeleton.bones[i];
-      u32 hash = vg_strdjb2( sb->name );
-
-      for( u32 j=1; j<armature->bone_count; j ++ ){
-         mdl_bone *bone = mdl_arritm( &ctx.bones, armature->bone_start+j );
-
-         if( mdl_pstreq( &ctx, bone->pstr_name, sb->name, hash ) ){
-            fixup_table[j+1] = i;
-            break;
-         }
-      }
-   }
-
-   dynamic_model_load( &ctx, &board->mdl, path, fixup_table );
-   mdl_close( &ctx );
-}
-
-void player_model_unload( struct player_model *board )
-{
-   dynamic_model_unload( &board->mdl );
-}
-
-void apply_full_skeleton_pose( struct skeleton *sk, player_pose *pose,
-                               m4x3f *final_mtx ){
-   m4x3f transform;
-   q_m3x3( pose->root_q, transform );
-   v3_copy( pose->root_co, transform[3] );
-   
-   if( pose->type == k_player_pose_type_ik ){
-      skeleton_apply_pose( sk, pose->keyframes, 
-                           k_anim_apply_defer_ik, final_mtx );
-      skeleton_apply_ik_pass( sk, final_mtx );
-      skeleton_apply_pose( sk, pose->keyframes, 
-                           k_anim_apply_deffered_only, final_mtx );
-      skeleton_apply_inverses( sk, final_mtx );
-      skeleton_apply_transform( sk, transform, final_mtx );
-   }
-   else if( pose->type == k_player_pose_type_fk_2 ){
-      skeleton_apply_pose( sk, pose->keyframes, 
-                           k_anim_apply_always, final_mtx );
-      skeleton_apply_inverses( sk, final_mtx );
-      skeleton_apply_transform( sk, transform, final_mtx );
-   }
-}
-
-void player__animate(void)
-{
-   struct player_subsystem_interface *sys = 
-      player_subsystems[localplayer.subsystem];
-
-   struct player_board *board = 
-      addon_cache_item_if_loaded( k_addon_type_board, 
-                                  localplayer.board_view_slot );
-
-   sys->animate();
-
-   player_pose *pose = &localplayer.pose;
-   sys->pose( sys->animator_data, pose );
-
-   struct skeleton *sk = &localplayer.skeleton;
-
-   if( localplayer.holdout_time > 0.0f ){
-      skeleton_lerp_pose( sk, 
-                          pose->keyframes,localplayer.holdout_pose.keyframes, 
-                          localplayer.holdout_time, pose->keyframes );
-
-      v3_muladds( pose->root_co, localplayer.holdout_pose.root_co, 
-                  localplayer.holdout_time, pose->root_co );
-      q_nlerp( pose->root_q, localplayer.holdout_pose.root_q, 
-               localplayer.holdout_time, pose->root_q );
-
-      localplayer.holdout_time -= vg.time_frame_delta / 0.25f;
-   }
-
-   effect_blink_apply( &localplayer.effect_data.blink,
-                       &localplayer.pose, vg.time_delta );
-   apply_full_skeleton_pose( sk, &localplayer.pose, localplayer.final_mtx );
-   
-   if( sys->effects ){
-      sys->effects( sys->animator_data, localplayer.final_mtx, board,
-                    &localplayer.effect_data );
-   }
-
-   skeleton_debug( sk, localplayer.final_mtx );
-
-   if( sys->post_animate )
-      sys->post_animate();
-
-   player__observe_system( localplayer.subsystem );
-   if( sys->sfx_comp )
-      sys->sfx_comp( sys->animator_data );
-
-   player__cam_iterate();
-}
-
-static void player_copy_frame_animator( replay_frame *frame ){
-   struct player_subsystem_interface *sys = 
-      player_subsystems[localplayer.subsystem];
-
-   if( sys->animator_size ){
-      void *src = replay_frame_data( frame, k_replay_framedata_animator );
-      memcpy( sys->animator_data, src, sys->animator_size );
-   }
-}
-
-void lerp_player_pose( player_pose *pose0, player_pose *pose1, f32 t,
-                       player_pose *posed ){
-   struct skeleton *sk = &localplayer.skeleton;
-   
-   v3_lerp( pose0->root_co, pose1->root_co, t, posed->root_co );
-   q_nlerp( pose0->root_q,  pose1->root_q,  t, posed->root_q );
-   posed->type = pose0->type;
-   posed->board.lean = vg_lerpf( pose0->board.lean, pose1->board.lean, t );
-
-   if( pose0->type != pose1->type ){
-      /* it would be nice to apply IK pass in-keyframes. TOO BAD! */
-      skeleton_copy_pose( sk, pose0->keyframes, posed->keyframes );
-   }
-   else {
-      skeleton_lerp_pose( sk, pose0->keyframes, pose1->keyframes, t, 
-                          posed->keyframes );
-   }
-}
-
-void player__observe_system( enum player_subsystem id )
-{
-   if( id != localplayer.observing_system ){
-      struct player_subsystem_interface *sysm1 = 
-         player_subsystems[ localplayer.observing_system ];
-
-      if( sysm1->sfx_kill ) sysm1->sfx_kill();
-      localplayer.observing_system = id;
-   }
-}
-
-void player__animate_from_replay( replay_buffer *replay )
-{
-   replay_frame *frame = replay->cursor_frame,
-                *next = NULL;
-   if( frame ){
-      next = frame->r;
-
-      struct player_subsystem_interface 
-         *sys0 = player_subsystems[frame->system];
-      void *a0 = replay_frame_data( frame, k_replay_framedata_animator );
-
-      struct replay_glider_data 
-         *g0 = replay_frame_data( frame, k_replay_framedata_glider ),
-         *g1;
-
-      f32 t = 0.0f;
-
-      if( next ){
-         t = replay_subframe_time( replay );
-
-         player_pose pose0, pose1;
-
-         struct player_subsystem_interface 
-            *sys1 = player_subsystems[next->system];
-         void *a1 = replay_frame_data( next, k_replay_framedata_animator );
-
-         sys0->pose( a0, &pose0 );
-         sys1->pose( a1, &pose1 );
-
-         lerp_player_pose( &pose0, &pose1, t, &localplayer.pose );
-         g1 = replay_frame_data( next,  k_replay_framedata_glider );
-      }
-      else{
-         sys0->pose( a0, &localplayer.pose );
-         g1 = NULL;
-      }
-
-      player__observe_system( frame->system );
-      if( sys0->sfx_comp ) 
-         sys0->sfx_comp( a0 );
-
-      if( g0 ){
-         if( g0->glider_orphan ){
-            if( g1 ){
-               v3_lerp( g0->co, g1->co, t, player_glide.rb.co );
-               q_nlerp( g0->q,  g1->q,  t, player_glide.rb.q );
-            }
-            else {
-               v3_copy( g0->co, player_glide.rb.co );
-               v4_copy( g0->q,  player_glide.rb.q );
-            }
-
-            rb_update_matrices( &player_glide.rb );
-         }
-
-         if( g1 )
-            player_glide.t = vg_lerpf( g0->t, g1->t, t );
-         else
-            player_glide.t = g0->t;
-
-         localplayer.have_glider   = g0->have_glider;
-         localplayer.glider_orphan = g0->glider_orphan;
-      }
-      else /* no glider data in g1, or edge case we dont care about */ {
-         localplayer.have_glider = 0;
-         localplayer.glider_orphan = 0;
-         player_glide.t = 0.0f;
-      }
-   }
-   else return;
-
-   apply_full_skeleton_pose( &localplayer.skeleton, &localplayer.pose,
-                             localplayer.final_mtx );
-}
-
-void player__pre_render(void)
-{
-   /* shadowing/ao info */
-   struct player_board *board = 
-      addon_cache_item_if_loaded( k_addon_type_board,
-                                  localplayer.board_view_slot );
-   v3f vp0, vp1;
-   if( board ){
-      v3_copy((v3f){0.0f,0.1f, board->truck_positions[0][2]}, vp0 );
-      v3_copy((v3f){0.0f,0.1f, board->truck_positions[1][2]}, vp1 );
-   }
-   else{
-      v3_zero( vp0 );
-      v3_zero( vp1 );
-   }
-
-   struct ub_world_lighting *ubo = &world_current_instance()->ub_lighting;
-   v3f *board_mtx = localplayer.final_mtx[ localplayer.id_board ];
-   m4x3_mulv( board_mtx, vp0, ubo->g_board_0 );
-   m4x3_mulv( board_mtx, vp1, ubo->g_board_1 );
-}
-
-void render_board( vg_camera *cam, world_instance *world,
-                   struct player_board *board, m4x3f root,
-                   struct player_board_pose *pose,
-                   enum board_shader shader )
-{
-   if( !board ) 
-      board = &localplayer.fallback_board;
-
-   /* TODO: 
-    *  adding depth compare to this shader
-    */
-
-   v3f inverse;
-
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, board->mdl.texture );
-
-   if( shader == k_board_shader_player )
-   {
-      shader_model_board_view_use();
-      shader_model_board_view_uTexMain( 0 );
-      shader_model_board_view_uCamera( cam->transform[3] );
-      shader_model_board_view_uPv( cam->mtx.pv );
-
-      shader_model_board_view_uDepthMode(1);
-      depth_compare_bind(
-         shader_model_board_view_uTexSceneDepth,
-         shader_model_board_view_uInverseRatioDepth,
-         shader_model_board_view_uInverseRatioMain,
-         cam );
-
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_board_view );
-   }
-   else if( shader == k_board_shader_entity )
-   {
-      shader_model_entity_use();
-      shader_model_entity_uTexMain( 0 );
-      shader_model_entity_uCamera( cam->transform[3] );
-      shader_model_entity_uPv( cam->mtx.pv );
-      
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
-   }
-
-   mesh_bind( &board->mdl.mesh );
-
-   m4x4f m4mdl;
-
-   if( board->board.indice_count ){
-      m4x3f mlocal;
-      m3x3_identity( mlocal );
-
-      mdl_keyframe kf;
-      v3_zero( kf.co );
-      q_identity( kf.q );
-      v3_zero( kf.s );
-
-      v4f qroll;
-      q_axis_angle( qroll, (v3f){0.0f,0.0f,1.0f}, pose->lean * 0.6f );
-      keyframe_rotate_around( &kf, (v3f){0.0f,0.11f,0.0f}, 
-                              (v3f){0.0f,0.0f,0.0f}, qroll );
-
-      v3_add( board->board_position, kf.co, mlocal[3] );
-      q_m3x3( kf.q, mlocal );
-
-      m4x3_mul( root, mlocal, mlocal );
-
-      if( shader == k_board_shader_entity ){
-         /* TODO: provide a way to supply previous mdl mtx? */
-         m4x3_expand( mlocal, m4mdl );
-         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
-         shader_model_entity_uPvmPrev( m4mdl );
-         shader_model_entity_uMdl( mlocal );
-      }
-      else
-         shader_model_board_view_uMdl( mlocal );
-
-      mdl_draw_submesh( &board->board );
-   }
-
-   for( int i=0; i<2; i++ ){
-      if( !board->trucks[i].indice_count )
-         continue;
-
-      m4x3f mlocal;
-      m3x3_identity( mlocal );
-      v3_copy( board->truck_positions[i], mlocal[3] );
-      m4x3_mul( root, mlocal, mlocal );
-
-      if( shader == k_board_shader_entity ){
-         m4x3_expand( mlocal, m4mdl );
-         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
-         shader_model_entity_uPvmPrev( m4mdl );
-         shader_model_entity_uMdl( mlocal );
-      }
-      else
-         shader_model_board_view_uMdl( mlocal );
-
-      mdl_draw_submesh( &board->trucks[i] );
-   }
-
-   for( int i=0; i<4; i++ ){
-      if( !board->wheels[i].indice_count )
-         continue;
-
-      m4x3f mlocal;
-      m3x3_identity( mlocal );
-      v3_copy( board->wheel_positions[i], mlocal[3] );
-      m4x3_mul( root, mlocal, mlocal );
-
-      if( shader == k_board_shader_entity ){
-         m4x3_expand( mlocal, m4mdl );
-         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
-         shader_model_entity_uPvmPrev( m4mdl );
-         shader_model_entity_uMdl( mlocal );
-      }
-      else
-         shader_model_board_view_uMdl( mlocal );
-
-      mdl_draw_submesh( &board->wheels[i] );
-   }
-}
-
-void render_playermodel( vg_camera *cam, world_instance *world,
-                         int depth_compare,
-                         struct player_model *model,
-                         struct skeleton *skeleton,
-                         m4x3f *final_mtx )
-{
-   if( !model ) return;
-   
-   shader_model_character_view_use();
-
-       glActiveTexture( GL_TEXTURE0 );
-       glBindTexture( GL_TEXTURE_2D, model->mdl.texture );
-   shader_model_character_view_uTexMain( 0 );
-   shader_model_character_view_uCamera( cam->transform[3] );
-   shader_model_character_view_uPv( cam->mtx.pv );
-   shader_model_character_view_uDepthMode( depth_compare );
-   if( depth_compare )
-   {
-      depth_compare_bind(
-         shader_model_character_view_uTexSceneDepth,
-         shader_model_character_view_uInverseRatioDepth,
-         shader_model_character_view_uInverseRatioMain,
-         cam );
-   }
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_character_view );
-
-   glUniformMatrix4x3fv( _uniform_model_character_view_uTransforms,
-                         skeleton->bone_count,
-                         0,
-                         (const GLfloat *)final_mtx );
-   
-   mesh_bind( &model->mdl.mesh );
-   mesh_draw( &model->mdl.mesh );
-}
-
-void player__render( vg_camera *cam )
-{
-   world_instance *world = world_current_instance();
-   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
-
-   struct player_model *model = 
-      addon_cache_item_if_loaded( k_addon_type_player, 
-                                  localplayer.playermodel_view_slot );
-
-   if( !model ) model = &localplayer.fallback_model;
-   render_playermodel( cam, world, 1, model, &localplayer.skeleton,
-                       localplayer.final_mtx );
-
-   struct player_board *board = 
-      addon_cache_item_if_loaded( k_addon_type_board,
-                                  localplayer.board_view_slot );
-
-   render_board( cam, world, board, localplayer.final_mtx[localplayer.id_board],
-                  &localplayer.pose.board, k_board_shader_player );
-
-   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
-
-   glEnable( GL_CULL_FACE );
-   player_glide_render( cam, world, &localplayer.pose );
-   glDisable( GL_CULL_FACE );
-}
-
-void player_mirror_pose( mdl_keyframe pose[32], mdl_keyframe mirrored[32] )
-{
-   mdl_keyframe temp[32];
-
-   struct skeleton *sk = &localplayer.skeleton;
-   for( u32 i=1; i<sk->bone_count; i ++ ){
-      mdl_keyframe *dest = &temp[i-1];
-      u8 mapping = localplayer.skeleton_mirror[i];
-
-      if( mapping ) *dest = pose[mapping-1]; /* R */
-      else          *dest = pose[i-1];       /* L */
-
-      dest->co[2] *= -1.0f;
-      dest->q[0] *= -1.0f;
-      dest->q[1] *= -1.0f;
-   }
-
-   for( u32 i=0; i<sk->bone_count-1; i ++ ){
-      mirrored[i] = temp[i];
-   }
-}
diff --git a/player_render.h b/player_render.h
deleted file mode 100644 (file)
index cfc48e7..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-#pragma once
-#include "model.h"
-#include "skeleton.h"
-#include "vg/vg_camera.h"
-#include "world.h"
-#include "player_render.h"
-#include "player_api.h"
-#include "player_replay.h"
-
-enum eboard_truck{
-   k_board_truck_back = 0,
-   k_board_truck_front = 1
-};
-
-enum eboard_wheel{
-   k_board_wheel_fl = 0,
-   k_board_wheel_fr = 1,
-   k_board_wheel_bl = 2,
-   k_board_wheel_br = 3,
-};
-
-/* TODO: Fully featured dynamic models
- * This is FAR from the final system we want at all, but it will do for now */
-struct dynamic_model_1texture{
-   glmesh mesh;
-   GLuint texture;
-};
-
-struct player_board{
-   struct dynamic_model_1texture mdl;
-
-   v4f wheel_positions[4],
-       truck_positions[2],
-       board_position;
-
-   mdl_submesh wheels[4],
-               trucks[2],
-               board;
-};
-
-struct player_model{
-   struct dynamic_model_1texture mdl;
-};
-
-enum board_shader{
-   k_board_shader_player,
-   k_board_shader_entity
-};
-
-void dynamic_model_load( mdl_context *ctx,
-                            struct dynamic_model_1texture *mdl, 
-                            const char *path, u32 *fixup_table );
-void dynamic_model_unload( struct dynamic_model_1texture *mdl );
-
-void player_board_load( struct player_board *mdl, const char *path );
-void player_board_unload( struct player_board *mdl );
-
-void player_model_load( struct player_model *board, const char *path);
-void player_model_unload( struct player_model *board );
-
-void render_board( vg_camera *cam, world_instance *world,
-                      struct player_board *board, m4x3f root,
-                      struct player_board_pose *pose,
-                      enum board_shader shader );
-
-void render_playermodel( vg_camera *cam, world_instance *world,
-                            int depth_compare,
-                            struct player_model *model,
-                            struct skeleton *skeleton,
-                            m4x3f *final_mtx );
-void apply_full_skeleton_pose( struct skeleton *sk, player_pose *pose,
-                               m4x3f *final_mtx );
-void lerp_player_pose( player_pose *pose0, player_pose *pose1, f32 t,
-                       player_pose *posed );
-void player_mirror_pose( mdl_keyframe pose[32], 
-                         mdl_keyframe mirrored[32] );
-void player__observe_system( enum player_subsystem id );
-void player_load_animation_reference( const char *path );
-void player__render( vg_camera *cam );
-void player__animate_from_replay( replay_buffer *replay );
-void player__animate(void);
-void player__pre_render(void);
diff --git a/player_replay.c b/player_replay.c
deleted file mode 100644 (file)
index 950ad78..0000000
+++ /dev/null
@@ -1,1207 +0,0 @@
-#include "skaterift.h"
-#include "player.h"
-#include "player_replay.h"
-#include "input.h"
-#include "gui.h"
-#include "freecam.h"
-
-#include "player_walk.h"
-#include "player_skate.h"
-#include "player_dead.h"
-#include "player_glide.h"
-
-struct replay_globals player_replay = 
-{
-   .active_keyframe = -1,
-   .show_ui = 1,
-   .editor_mode = 0
-};
-
-void replay_clear( replay_buffer *replay )
-{
-   replay->head = NULL;
-   replay->tail = NULL;
-   replay->cursor_frame = NULL;
-   replay->statehead = NULL;
-   replay->cursor = -99999.9;
-}
-
-void *replay_frame_data( replay_frame *frame, enum replay_framedata type )
-{
-   if( frame->data_table[type][1] == 0 )
-      return NULL;
-
-   void *baseptr = frame;
-   return baseptr + frame->data_table[type][0];
-}
-
-static u16 replay_frame_calculate_data_offsets( 
-      u16 data_table[k_replay_framedata_rows][2] ){
-
-   u32 total = vg_align8( sizeof(replay_frame) );
-   for( u32 i=0; i<k_replay_framedata_rows; i++ ){
-      data_table[i][0] = total;
-      total += vg_align8(data_table[i][1]);
-
-      if( total > 0xffff )
-         vg_fatal_error( "Exceeded frame storage capacity\n" );
-   }
-   return total;
-}
-
-static void replay_tailpop( replay_buffer *replay ){
-   if( replay->cursor_frame == replay->tail )
-      replay->cursor_frame = NULL;
-   if( replay->statehead == replay->tail )
-      replay->statehead = NULL;
-
-   replay->tail = replay->tail->r;
-
-   if( replay->tail )
-      replay->tail->l = NULL;
-   else
-      replay->head = NULL;
-}
-
-static replay_frame *replay_newframe( replay_buffer *replay, 
-                                         u16 animator_size,
-                                         u16 gamestate_size,
-                                         u16 sfx_count,
-                                         bool save_glider ){
-   u16 data_table[ k_replay_framedata_rows ][2];
-   data_table[ k_replay_framedata_animator ][1]  = animator_size;
-   data_table[ k_replay_framedata_gamestate ][1] = gamestate_size;
-   data_table[ k_replay_framedata_sfx ][1] = sfx_count*sizeof(struct net_sfx);
-   data_table[ k_replay_framedata_internal_gamestate ][1] = 0;
-   if( gamestate_size )
-   {
-      data_table[ k_replay_framedata_internal_gamestate ][1] = 
-         sizeof( replay_gamestate );
-   }
-
-   data_table[ k_replay_framedata_glider ][1] = 0;
-   if( save_glider )
-   {
-      data_table[ k_replay_framedata_glider ][1] = 
-         sizeof(struct replay_glider_data);
-   }
-
-   u32 nextsize = replay_frame_calculate_data_offsets( data_table );
-
-   replay_frame *frame = NULL;
-   if( replay->head )
-   {
-      u32 headsize = replay->head->total_size,
-          nextpos  = ((void *)replay->head - replay->data) + headsize;
-
-      if( nextpos + nextsize > replay->size )
-      {
-         nextpos = 0;
-         
-         /* maintain contiguity */
-         while( replay->tail )
-         {
-            if( (void *)replay->tail - replay->data )
-               replay_tailpop( replay );
-            else break;
-         }
-      }
-
-check_again:;
-      u32 tailpos = (void *)replay->tail - replay->data;
-
-      if( tailpos >= nextpos )
-      {
-         if( nextpos + nextsize > tailpos )
-         {
-            replay_tailpop( replay );
-
-            if( replay->tail )
-               goto check_again;
-         }
-      }
-
-      frame = replay->data + nextpos;
-
-      if( replay->head )
-         replay->head->r = frame;
-   }
-   else
-      frame = replay->data;
-
-   for( u32 i=0; i<k_replay_framedata_rows; i++ )
-   {
-      frame->data_table[i][0] = data_table[i][0];
-      frame->data_table[i][1] = data_table[i][1];
-   }
-
-   frame->total_size = nextsize;
-   frame->l = replay->head;
-   frame->r = NULL;
-   replay->head = frame;
-   if( !replay->tail ) replay->tail = frame;
-   if( gamestate_size ) replay->statehead = frame;
-
-   return frame;
-}
-
-static void replay_emit_frame_sounds( replay_frame *frame ){
-   void *baseptr = frame;
-   u16 *inf = frame->data_table[k_replay_framedata_sfx];
-   struct net_sfx *buffer = baseptr + inf[0];
-   u32 count = inf[1] / sizeof(struct net_sfx);
-
-   for( u32 i=0; i<count; i ++ ){
-      net_sfx_play( buffer + i );
-   }
-}
-
-int replay_seek( replay_buffer *replay, f64 t )
-{
-   if( !replay->head ) return 0;
-
-   if( t < replay->tail->time ) t = replay->tail->time;
-   if( t > replay->head->time ) t = replay->head->time;
-
-   if( !replay->cursor_frame ) {
-      replay->cursor = replay->head->time;
-      replay->cursor_frame = replay->head;
-
-      if( fabs(replay->head->time-t) > fabs(replay->tail->time-t) ){
-         replay->cursor = replay->tail->time;
-         replay->cursor_frame = replay->tail;
-      }
-   }
-
-   f64 dir = t - replay->cursor;
-   if( dir == 0.0 ) return 0;
-   dir = vg_signf( dir );
-
-   
-   u32 i=4096;
-   while( i --> 0 ){
-      if( dir < 0.0 ){
-         if( t > replay->cursor_frame->time ) {
-            replay->cursor = t;
-            return 1;
-         }
-      }
-
-      replay_frame *next;
-      if( dir > 0.0 ) next = replay->cursor_frame->r;
-      else            next = replay->cursor_frame->l;
-
-      if( !next ) break;
-
-      if( dir > 0.0 ){
-         if( t < next->time ){
-            replay->cursor = t;
-            return 1;
-         }
-      }
-
-      replay_emit_frame_sounds( next );
-
-      replay->cursor_frame = next;
-      replay->cursor = next->time;
-
-      if( !i ) return 1;
-   }
-
-   replay->cursor = t;
-   return 0;
-}
-
-replay_frame *replay_find_recent_stateframe( replay_buffer *replay )
-{
-   replay_frame *frame = replay->cursor_frame;
-   u32 i=4096;
-   while( i --> 0 ){
-      if( !frame ) return frame;
-      if( frame->data_table[ k_replay_framedata_gamestate ][1] ) return frame;
-      frame = frame->l;
-   }
-
-   return NULL;
-}
-
-f32 replay_subframe_time( replay_buffer *replay )
-{
-   replay_frame *frame = replay->cursor_frame;
-   if( !frame ) return 0.0f;
-   replay_frame *next = frame->r;
-   if( next )
-   {
-      f64 l = next->time - frame->time,
-          t = (l <= (1.0/128.0))? 0.0: (replay->cursor - frame->time) / l;
-      return vg_clampf( t, 0.0f, 1.0f );
-   }
-   else 
-      return 0.0f;
-}
-
-void replay_get_frame_camera( replay_frame *frame, vg_camera *cam )
-{
-   cam->fov = frame->cam.fov;
-   v3_copy( frame->cam.pos, cam->pos );
-   v3_copy( frame->cam.angles, cam->angles );
-}
-
-void replay_get_camera( replay_buffer *replay, vg_camera *cam )
-{
-   cam->nearz = 0.1f;
-   cam->farz = 100.0f;
-   if( replay->cursor_frame )
-   {
-      replay_frame *next = replay->cursor_frame->r;
-
-      if( next )
-      {
-         vg_camera temp;
-         
-         replay_get_frame_camera( replay->cursor_frame, cam );
-         replay_get_frame_camera( next, &temp );
-         vg_camera_lerp( cam, &temp, replay_subframe_time( replay ), cam );
-      }
-      else 
-      {
-         replay_get_frame_camera( replay->cursor_frame, cam );
-      }
-   }
-   else 
-   {
-      v3_zero( cam->pos );
-      v3_zero( cam->angles );
-      cam->fov = 90.0f;
-   }
-}
-
-void skaterift_get_replay_cam( vg_camera *cam )
-{
-   replay_buffer *replay = &player_replay.local;
-
-   if( player_replay.active_keyframe != -1 )
-   {
-      replay_keyframe *kf = 
-         &player_replay.keyframes[player_replay.active_keyframe];
-
-      v3_copy( kf->cam.pos, cam->pos );
-      v3_copy( kf->cam.angles, cam->angles );
-      cam->fov = kf->cam.fov;
-      return;
-   }
-
-   if( player_replay.keyframe_count >= 2 )
-   {
-      for( u32 i=0; i<player_replay.keyframe_count-1; i ++ )
-      {
-         replay_keyframe *kf = &player_replay.keyframes[i];
-
-         if( (kf[0].time<=replay->cursor) && (kf[1].time>replay->cursor) )
-         {
-            f64 l = kf[1].time - kf[0].time,
-                t = (l <= (1.0/128.0))? 0.0: (replay->cursor-kf[0].time) / l;
-
-            if( player_replay.keyframe_count >= 3 )
-            {
-               f32 m_start = 0.5f, m_end = 0.5f;
-
-               if( i > 0 )
-               {
-                  if( (t < 0.5f) || (i==player_replay.keyframe_count-2) )
-                  {
-                     kf --;
-                  }
-               }
-
-               u32 last = player_replay.keyframe_count-1;
-               if( kf+0 == player_replay.keyframes ) m_start = 1.0f;
-               if( kf+2 == player_replay.keyframes+last ) m_end = 1.0f;
-
-               f32 ts = vg_lerpf( kf[0].time, kf[1].time, 1.0f-m_start ),
-                   te = vg_lerpf( kf[1].time, kf[2].time, m_end );
-
-               l = te-ts;
-               t = (replay->cursor-ts)/l;
-
-               /* 
-                * Adjust t, so that its derivative matches at the endpoints.
-                * Since t needs to go from 0 to 1, it will naturally change at 
-                * different rates between keyframes. So this smooths it out.
-                *
-                * Newton method, going through standard direct quadratic eq has 
-                * precision / other problems. Also we only care about 0>t>1. 
-                */
-               f32 b = (kf[1].time-ts)/l,
-                  x0 = 1.0-t;
-               for( u32 i=0; i<4; i ++ )
-               {
-                  f32 ix0 = 1.0f-x0,
-                      fx_x0 = 2.0f*b*x0*ix0 + ix0*ix0 - t,
-                      fxd_x0 = 2.0f*(-2.0f*b*x0 + b + x0 - 1.0f);
-                   x0 = x0 - (fx_x0/fxd_x0);
-               }
-               t = 1.0-x0;
-
-               f32 t0 = t*m_start+(1.0f-m_start),
-                   t1 = t*m_end;
-
-               v3f ps, pe, as, ae;
-               f32 fs, fe;
-
-               /* first order */
-               v3_lerp( kf[0].cam.pos, kf[1].cam.pos, t0, ps );
-               vg_camera_lerp_angles( kf[0].cam.angles, kf[1].cam.angles, 
-                                      t0, as );
-               fs = vg_lerpf( kf[0].cam.fov, kf[1].cam.fov, t0 );
-
-               v3_lerp( kf[1].cam.pos, kf[2].cam.pos, t1, pe );
-               vg_camera_lerp_angles( kf[1].cam.angles, kf[2].cam.angles, 
-                                      t1, ae );
-               fe = vg_lerpf( kf[1].cam.fov, kf[2].cam.fov, t1 );
-
-               /* second order */
-               v3_lerp( ps, pe, t, cam->pos );
-               vg_camera_lerp_angles( as, ae, t, cam->angles );
-               cam->fov = vg_lerpf( fs, fe, t );
-            }
-            else 
-            {
-               v3_lerp( kf[0].cam.pos,  kf[1].cam.pos, t, cam->pos );
-               vg_camera_lerp_angles( kf[0].cam.angles, kf[1].cam.angles, 
-                                      t, cam->angles );
-               cam->fov = vg_lerpf( kf[0].cam.fov, kf[1].cam.fov, t );
-            }
-            return;
-         }
-      }
-   }
-
-   replay_get_camera( replay, cam );
-}
-
-struct replay_rb
-{
-   v3f co, v, w;
-   v4f q;
-};
-
-void skaterift_record_frame( replay_buffer *replay, int force_gamestate )
-{
-   f64 delta      = 9999999.9,
-       statedelta = 9999999.9;
-
-   if( replay->head )
-      delta = vg.time - replay->head->time;
-
-   if( replay->statehead )
-      statedelta = vg.time - replay->statehead->time;
-
-   const f64 k_replay_rate = 1.0/30.0,
-             k_gamestate_rate = 0.5;
-
-   int save_frame = 0,
-       save_state = 0,
-       save_glider = 0;
-
-   if( force_gamestate ) save_state = 1;
-   if( statedelta > k_gamestate_rate ) save_state = 1;
-   if( delta > k_replay_rate ) save_frame = 1;
-   if( save_state ) save_frame = 1;
-
-   if( localplayer.have_glider || localplayer.glider_orphan ||
-       localplayer.subsystem == k_player_subsystem_glide ){
-      save_glider = 1;
-   }
-
-   if( !save_frame ) return;
-
-   u16 gamestate_size = 0;
-   if( save_state ){
-      /* TODO: have as part of system struct */
-      gamestate_size = (u32 []){ 
-         [k_player_subsystem_walk ] = sizeof(struct player_walk_state),
-         [k_player_subsystem_drive] = 0,
-         [k_player_subsystem_skate] = sizeof(struct player_skate_state),
-         [k_player_subsystem_dead ] = localplayer.ragdoll.part_count * 
-                                       sizeof(struct replay_rb),
-         [k_player_subsystem_glide] = sizeof(struct replay_rb),
-      }[ localplayer.subsystem ];
-   }
-
-   u16 animator_size = player_subsystems[localplayer.subsystem]->animator_size;
-   
-   replay_frame *frame = replay_newframe( replay,
-                                          animator_size, gamestate_size, 
-                                          localplayer.local_sfx_buffer_count,
-                                          save_glider );
-   frame->system = localplayer.subsystem;
-
-   if( save_state ){
-      replay_gamestate *gs = 
-         replay_frame_data( frame, k_replay_framedata_internal_gamestate );
-
-      gs->current_run_version = world_static.current_run_version;
-      gs->drowned = localplayer.drowned;
-
-      /* permanent block */
-      memcpy( &gs->rb, &localplayer.rb, sizeof(rigidbody) );
-      memcpy( &gs->glider_rb, &player_glide.rb, sizeof(rigidbody) );
-      memcpy( &gs->cam_control, &localplayer.cam_control, 
-               sizeof(struct player_cam_controller) );
-      v3_copy( localplayer.angles, gs->angles );
-
-      void *dst = replay_frame_data( frame, k_replay_framedata_gamestate );
-
-      /* subsytem/dynamic block */
-      if( localplayer.subsystem == k_player_subsystem_walk )
-         memcpy( dst, &player_walk.state, gamestate_size );
-      else if( localplayer.subsystem == k_player_subsystem_skate )
-         memcpy( dst, &player_skate.state, gamestate_size );
-      else if( localplayer.subsystem == k_player_subsystem_dead ){
-         struct replay_rb *arr = dst;
-         for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
-            rigidbody *rb = &localplayer.ragdoll.parts[i].rb;
-            v3_copy( rb->co, arr[i].co );
-            v3_copy( rb->w, arr[i].w );
-            v3_copy( rb->v, arr[i].v );
-            v4_copy( rb->q, arr[i].q );
-         }
-      }
-      else if( localplayer.subsystem == k_player_subsystem_glide ){
-         struct replay_rb *arr = dst;
-         rigidbody *rb = &player_glide.rb;
-         v3_copy( rb->co, arr[0].co );
-         v3_copy( rb->w, arr[0].w );
-         v3_copy( rb->v, arr[0].v );
-         v4_copy( rb->q, arr[0].q );
-      }
-   }
-
-   if( save_glider ){
-      struct replay_glider_data *inf = 
-         replay_frame_data( frame, k_replay_framedata_glider );
-
-      inf->have_glider   = localplayer.have_glider;
-      inf->glider_orphan = localplayer.glider_orphan;
-      inf->t             = player_glide.t;
-      v3_copy( player_glide.rb.co, inf->co );
-      v4_copy( player_glide.rb.q,  inf->q );
-   }
-
-   replay->cursor = vg.time;
-   replay->cursor_frame = frame;
-   frame->time = vg.time;
-
-   /* camera */
-   v3_copy( localplayer.cam.pos, frame->cam.pos );
-   if( localplayer.gate_waiting ){
-      m4x3_mulv( localplayer.gate_waiting->transport, 
-                  frame->cam.pos, frame->cam.pos );
-
-      v3f v0;
-      v3_angles_vector( localplayer.cam.angles, v0 );
-      m3x3_mulv( localplayer.gate_waiting->transport, v0, v0 );
-      v3_angles( v0, frame->cam.angles );
-   }
-   else 
-      v3_copy( localplayer.cam.angles, frame->cam.angles );
-
-   frame->cam.fov = localplayer.cam.fov;
-
-   /* animator */
-   void *dst = replay_frame_data( frame, k_replay_framedata_animator ),
-        *src = player_subsystems[localplayer.subsystem]->animator_data;
-   memcpy( dst, src, animator_size );
-
-   /* sound effects */
-   memcpy( replay_frame_data( frame, k_replay_framedata_sfx ),
-           localplayer.local_sfx_buffer,
-           sizeof(struct net_sfx)*localplayer.local_sfx_buffer_count );
-
-   localplayer.local_sfx_buffer_count = 0;
-}
-
-static void skaterift_restore_frame( replay_frame *frame )
-{
-   replay_gamestate *gs = 
-      replay_frame_data( frame, k_replay_framedata_internal_gamestate );
-   void *src = replay_frame_data( frame, k_replay_framedata_gamestate );
-   u16 src_size = frame->data_table[ k_replay_framedata_gamestate ][1];
-   world_static.current_run_version = gs->current_run_version;
-   localplayer.drowned = gs->drowned;
-
-   if(frame->system == k_player_subsystem_walk ){
-      memcpy( &player_walk.state, src, src_size );
-   }
-   else if( frame->system == k_player_subsystem_skate ){
-      memcpy( &player_skate.state, src, src_size );
-   }
-   else if( frame->system == k_player_subsystem_dead ){
-      player__dead_transition(0);
-      struct replay_rb *arr = src;
-
-      for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
-         struct ragdoll_part *part = &localplayer.ragdoll.parts[i];
-         rigidbody *rb = &part->rb;
-
-         v3_copy( arr[i].co, rb->co );
-         v3_copy( arr[i].w, rb->w );
-         v3_copy( arr[i].v, rb->v );
-         v4_copy( arr[i].q, rb->q );
-
-         v3_copy( arr[i].co, part->prev_co );
-         v4_copy( arr[i].q, part->prev_q );
-         rb_update_matrices( rb );
-      }
-   }
-   else if( frame->system == k_player_subsystem_glide ){
-      struct replay_rb *arr = src;
-      rigidbody *rb = &player_glide.rb;
-      v3_copy( arr[0].co, rb->co );
-      v3_copy( arr[0].w, rb->w );
-      v3_copy( arr[0].v, rb->v );
-      v4_copy( arr[0].q, rb->q );
-      rb_update_matrices( rb );
-   }
-
-   localplayer.subsystem = frame->system;
-
-   /* restore the seperated glider data if we have it */
-   if( frame->data_table[ k_replay_framedata_glider ][1] ){
-      struct replay_glider_data *inf = 
-         replay_frame_data( frame, k_replay_framedata_glider );
-
-      localplayer.have_glider   = inf->have_glider;
-      localplayer.glider_orphan = inf->glider_orphan;
-      player_glide.t            = inf->t;
-   }
-   else {
-      localplayer.have_glider = 0;
-      localplayer.glider_orphan = 0;
-      player_glide.t = 0.0f;
-   }
-
-   memcpy( &localplayer.rb, &gs->rb, sizeof(rigidbody) );
-   memcpy( &player_glide.rb, &gs->glider_rb, sizeof(rigidbody) );
-   v3_copy( gs->angles, localplayer.angles );
-
-   v3_copy( frame->cam.pos, localplayer.cam.pos );
-   v3_copy( frame->cam.angles, localplayer.cam.angles );
-   localplayer.cam.fov = frame->cam.fov;
-
-   memcpy( &localplayer.cam_control, &gs->cam_control, 
-            sizeof(struct player_cam_controller) );
-
-   /* chop end off replay */
-   frame->r = NULL;
-   player_replay.local.statehead = frame;
-   player_replay.local.head = frame;
-   player_replay.local.cursor_frame = frame;
-   player_replay.local.cursor = frame->time;
-   player_replay.replay_control = k_replay_control_scrub;
-   skaterift.activity = k_skaterift_default;
-   vg.time = frame->time;
-}
-
-static void skaterift_replay_resume(void){
-   replay_frame *prev = replay_find_recent_stateframe(&player_replay.local);
-
-   if( prev ){
-      player_replay.replay_control = k_replay_control_resume;
-      player_replay.resume_target = prev;
-      player_replay.resume_begin = player_replay.local.cursor;
-      player_replay.resume_transition = 0.0f;
-   }
-
-   gui_helper_clear();
-}
-
-static void skaterift_replay_update_helpers(void);
-
-void skaterift_replay_pre_update(void)
-{
-   if( skaterift.activity != k_skaterift_replay ) return;
-
-   bool input = player_replay.editor_mode^0x1;
-
-   if( player_replay.replay_control == k_replay_control_resume )
-   {
-      if( player_replay.local.cursor_frame == player_replay.resume_target ||
-          player_replay.local.cursor_frame == NULL )
-      {
-         skaterift_restore_frame( player_replay.resume_target );
-      }
-      else 
-      {
-         vg_slewf( &player_replay.resume_transition, 1.0f, 
-                   vg.time_frame_delta * (1.0f/1.0f) );
-
-         if( player_replay.resume_transition >= 1.0f )
-            skaterift_restore_frame( player_replay.resume_target );
-         else {
-            f64 target = vg_lerp( player_replay.resume_begin, 
-                           player_replay.resume_target->time, 
-                           vg_smoothstepf( player_replay.resume_transition ) );
-            if( replay_seek( &player_replay.local, target ) )
-               player_replay.track_velocity = 1.0f;
-            else
-               player_replay.track_velocity = 0.0f;
-         }
-      }
-   }
-   else 
-   {
-      if( input && button_down( k_srbind_replay_play ) )
-         player_replay.replay_control = k_replay_control_play;
-      if( input && button_down( k_srbind_replay_freecam ) )
-      {
-         player_replay.use_freecam ^= 0x1;
-
-         if( player_replay.use_freecam )
-         {
-            replay_get_camera( &player_replay.local, 
-                               &player_replay.replay_freecam );
-         }
-         skaterift_replay_update_helpers();
-      }
-
-      f32 target_speed = 0.0f;
-      if( input )
-         target_speed = axis_state( k_sraxis_replay_h ) * 5.0;
-
-      if( input && button_press( k_srbind_reset ) )
-         target_speed += -2.0;
-
-      if( fabsf(target_speed) > 0.01f )
-         player_replay.replay_control = k_replay_control_scrub;
-
-      if( player_replay.replay_control == k_replay_control_play )
-         target_speed = 1.0;
-
-      vg_slewf( &player_replay.track_velocity, target_speed, 
-                18.0f*vg.time_frame_delta );
-
-      if( fabsf( player_replay.track_velocity ) > 0.0001f )
-      {
-         f64 target = player_replay.local.cursor;
-         target += player_replay.track_velocity * vg.time_frame_delta;
-
-         if( !replay_seek( &player_replay.local, target ) )
-            player_replay.track_velocity = 0.0f;
-      }
-
-      if( input && button_down( k_srbind_mback ) )
-      {
-         if( player_replay.local.statehead )
-            skaterift_restore_frame( player_replay.local.statehead );
-         else
-            skaterift.activity = k_skaterift_default;
-         srinput.state = k_input_state_resume;
-         gui_helper_clear();
-      }
-
-      if( input )
-      {
-         if( player_replay.use_freecam )
-         {
-            freecam_preupdate();
-         }
-         else 
-         {
-            if( button_down( k_srbind_replay_resume ) )
-            {
-               skaterift_replay_resume();
-            }
-         }
-      }
-   }
-}
-
-static void skaterift_replay_update_helpers(void)
-{
-   player_replay.helper_resume->greyed = player_replay.use_freecam;
-
-   vg_str freecam_text;
-   vg_strnull( &freecam_text, player_replay.helper_freecam->text, 
-               GUI_HELPER_TEXT_LENGTH );
-   vg_strcat( &freecam_text, 
-               player_replay.use_freecam? "Exit freecam": "Freecam" );
-}
-
-static void replay_show_helpers(void)
-{
-   gui_helper_clear();
-   vg_str text;
-
-   if( gui_new_helper( input_axis_list[k_sraxis_replay_h], &text ) )
-      vg_strcat( &text, "Scrub" );
-
-   if( (player_replay.helper_resume = gui_new_helper( 
-               input_button_list[k_srbind_replay_resume], &text )) )
-      vg_strcat( &text, "Resume" );
-   
-   if( gui_new_helper( input_button_list[k_srbind_replay_play], &text ))
-      vg_strcat( &text, "Playback" );
-
-   player_replay.helper_freecam = gui_new_helper(
-         input_button_list[k_srbind_replay_freecam], &text );
-
-   skaterift_replay_update_helpers();
-}
-
-void skaterift_replay_post_render(void)
-{
-#ifndef SR_ALLOW_REWIND_HUB
-   if( world_static.active_instance != k_world_purpose_client )
-      return;
-#endif
-
-   /* capture the current resume frame at the very last point */
-   if( button_down( k_srbind_reset ) )
-   {
-      if( skaterift.activity == k_skaterift_default )
-      {
-         localplayer.rewinded_since_last_gate = 1;
-         skaterift.activity = k_skaterift_replay;
-         skaterift_record_frame( &player_replay.local, 1 );
-         if( player_replay.local.head )
-         {
-            player_replay.local.cursor = player_replay.local.head->time;
-            player_replay.local.cursor_frame = player_replay.local.head;
-         }
-         player_replay.replay_control = k_replay_control_scrub;
-         replay_show_helpers();
-      }
-   }
-}
-
-void skaterift_replay_init(void)
-{
-   u32 bytes = 1024*1024*10;
-   player_replay.local.data = vg_linear_alloc( vg_mem.rtmemory, bytes );
-   player_replay.local.size = bytes;
-   replay_clear( &player_replay.local );
-}
-
-void skaterift_replay_debug_info( ui_context *ctx )
-{
-   player__debugtext( ctx, 2, "replay info" );
-   replay_buffer *replay = &player_replay.local;
-
-   u32 head = 0,
-       tail = 0;
-   if( replay->tail ) tail = (void *)replay->tail - replay->data;
-   if( replay->head ) head = (void *)replay->head - replay->data;
-
-   player__debugtext( ctx, 1, "head @%u | tail @%u\n", head, tail );
-
-   if( replay->statehead )
-   {
-      for( u32 i=0; i<k_replay_framedata_rows; i++ )
-      {
-         player__debugtext( ctx, 1, "[%u]: [%hu, %hu]\n", i,
-              replay->statehead->data_table[i][0],
-              replay->statehead->data_table[i][1] );
-      }
-      u32 state = (void *)replay->statehead - replay->data;
-      player__debugtext( ctx, 1, "gs @%u\n", state );
-      player__debugtext( ctx, 1, "gamestate_size: %hu\n", 
-           replay->statehead->data_table[k_replay_framedata_gamestate][1] );
-   }
-   else
-      player__debugtext( ctx, 1, "gs @NULL\n" );
-
-   f64 start = replay->cursor,
-       end   = replay->cursor;
-   if( replay->tail ) start = replay->tail->time;
-   if( replay->head ) end = replay->head->time;
-
-   f64 cur = replay->cursor - start,
-       len = end - start;
-
-   player__debugtext( ctx, 1, "cursor: %.2fs / %.2fs\n", cur, len );
-}
-
-static int _keyframe_cmp( const void *p1, const void *p2 )
-{
-   const replay_keyframe *kf1 = p1, *kf2 = p2;
-   return kf1->time > kf2->time;
-}
-
-static void replay_keyframe_sort(void)
-{
-   qsort( player_replay.keyframes, player_replay.keyframe_count,
-          sizeof(replay_keyframe), _keyframe_cmp );
-}
-
-static void replay_fly_edit_keyframe( ui_context *ctx, replay_keyframe *kf )
-{
-   if( ui_click_down( ctx, UI_MOUSE_LEFT ) )
-   {
-      /* init freecam */
-      v3_copy( kf->cam.pos, player_replay.replay_freecam.pos );
-      v3_copy( kf->cam.angles, player_replay.replay_freecam.angles );
-      v3_zero( player_replay.freecam_v );
-      v3_zero( player_replay.freecam_w );
-      player_replay.replay_freecam.fov = kf->cam.fov;
-   }
-
-   /* move freecam */
-   ui_capture_mouse( ctx, 0 );
-   freecam_preupdate();
-
-   if( vg_getkey(SDLK_q) )
-      player_replay.freecam_v[1] -= vg.time_frame_delta*6.0f*20.0f;
-   if( vg_getkey(SDLK_e) )
-      player_replay.freecam_v[1] += vg.time_frame_delta*6.0f*20.0f;
-
-   v3_copy( player_replay.replay_freecam.pos, g_render.cam.pos );
-   v3_copy( player_replay.replay_freecam.angles, g_render.cam.angles);
-   g_render.cam.fov = player_replay.replay_freecam.fov;
-
-   v3_copy( g_render.cam.pos, kf->cam.pos );
-   v3_copy( g_render.cam.angles, kf->cam.angles );
-   kf->cam.fov = g_render.cam.fov;
-}
-
-void skaterift_replay_imgui( ui_context *ctx )
-{
-   if( skaterift.activity != k_skaterift_replay ) return;
-
-   /* extra keys for entering editor */
-   static u8 f1_key = 0;
-   u8 f1_now = vg_getkey(SDLK_F1);
-   if( f1_now && !f1_key && player_replay.show_ui )
-   {
-      player_replay.editor_mode ^= 0x1;
-
-      if( player_replay.editor_mode )
-         gui_helper_clear();
-      else
-         replay_show_helpers();
-   }
-   f1_key = f1_now;
-
-   static u8 f2_key = 0;
-   u8 f2_now = vg_getkey(SDLK_F2);
-   if( f2_now && !f2_key )
-   {
-      player_replay.show_ui ^= 0x1;
-   }
-   f2_key = f2_now;
-
-   if( player_replay.editor_mode )
-   {
-      static u8 space_key = 0;
-      u8 space_now = vg_getkey(SDLK_SPACE);
-      if( space_now & !space_key )
-      {
-         player_replay.replay_control ^= k_replay_control_play;
-      }
-      space_key = space_now;
-   }
-
-   if( !player_replay.show_ui ) return;
-
-   if( player_replay.editor_mode )
-   {
-      u32 colour = ui_opacity( ui_colour(ctx,k_ui_fg), 0.3333f );
-      ui_rect cx = { vg.window_x/2, 0, 1, vg.window_y },
-              cy = { 0, vg.window_y/2, vg.window_x, 1 };
-      ui_fill( ctx, cx, colour );
-      ui_fill( ctx, cy, colour );
-   }
-
-   replay_buffer *replay = &player_replay.local;
-   f64 start = replay->cursor,
-       end   = replay->cursor;
-   if( replay->tail ) start = replay->tail->time;
-   if( replay->head ) end = replay->head->time;
-   f64 len = end - start,
-       cur = (replay->cursor - start) / len;
-
-   char buffer[ 128 ];
-
-   /* mainbar */
-   ui_px height = 32,
-         cwidth = 2;
-   ui_rect timeline = { 0, 0, vg.window_x, height };
-   ui_fill( ctx, timeline, ui_colour( ctx, k_ui_bg ) );
-
-   /* cursor frame block */
-   if( replay->cursor_frame )
-   {
-      if( replay->cursor_frame->r )
-      {
-         f64 l = (replay->cursor_frame->r->time-replay->cursor_frame->time)/len,
-             s = (replay->cursor_frame->time - start) / len;
-         ui_rect box = { s*(f64)vg.window_x, 0, 
-                         VG_MAX(4,(ui_px)(l*vg.window_x)), timeline[3]+2 };
-         ui_fill( ctx, box, ui_colour( ctx, k_ui_bg+4 ) );
-      }
-   }
-
-   /* cursor */
-   ui_rect cusor = { cur * (f64)vg.window_x - (cwidth/2), 0, 
-                     cwidth, (player_replay.editor_mode? 0: 16) + timeline[3] };
-   ui_fill( ctx, cusor, ui_colour( ctx, k_ui_bg+7 ) );
-
-   /* latest state marker */
-   if( replay->statehead )
-   {
-      f64 t = (replay->statehead->time - start) / len;
-      ui_rect tag = { t*(f64)vg.window_x, 0, 2, timeline[3]+8 };
-      ui_fill( ctx, tag, ui_colour( ctx, k_ui_green+k_ui_brighter ) );
-   }
-
-   /* previous state marker */
-   replay_frame *prev = replay_find_recent_stateframe( replay );
-   if( prev )
-   {
-      f64 t = (prev->time - start) / len;
-      ui_rect tag = { t*(f64)vg.window_x, 0, 2, timeline[3]+8 };
-      ui_fill( ctx, tag, ui_colour( ctx, k_ui_yellow+k_ui_brighter ) );
-   }
-
-   snprintf( buffer, 128, "-%.2fs (F1: Edit replay)", (end-replay->cursor) );
-   ui_text( ctx, timeline, buffer, 1, k_ui_align_middle_left, 0 );
-   ui_text( ctx, timeline, "0s", 1, k_ui_align_middle_right, 0 );
-
-   if( !player_replay.editor_mode ) return;
-   ui_capture_mouse( ctx, 1 );
-
-   ui_rect panel = { 0, timeline[3] + 20, 200, 400 };
-   ui_fill( ctx, panel, ui_opacity( ui_colour( ctx, k_ui_bg ), 0.5f ) );
-   ui_rect_pad( panel, (ui_px[2]){4,4} );
-
-   if( ui_button( ctx, panel, 
-            (player_replay.replay_control == k_replay_control_play)? 
-               "Pause (space)": "Play (space)" ) == k_ui_button_click )
-   {
-      player_replay.replay_control ^= k_replay_control_play;
-   }
-
-   /* script bar */
-   ui_rect script = { 0, height + 2, vg.window_x, 16 };
-   ui_fill( ctx, script, ui_colour( ctx, k_ui_bg ) );
-   f64 mouse_t = start + ((f64)ctx->mouse[0] / (f64)vg.window_x)*len;
-
-   /* keyframe draw and select */
-   bool absorb_by_keyframe = 0;
-   ui_px lx = 0;
-   for( u32 i=0; i<player_replay.keyframe_count; i ++ )
-   {
-      replay_keyframe *kf = &player_replay.keyframes[i];
-      f64 t = (kf->time-start)/len;
-
-      ui_px x = t*(f64)vg.window_x-8;
-
-      /* draw connections between keyframes */
-      if( i )
-      {
-         ui_rect con = { lx, script[1]+7, x-lx, 1 };
-         ui_fill( ctx, con, ui_colour( ctx, k_ui_blue ) );
-         vg_line( kf->cam.pos, player_replay.keyframes[i-1].cam.pos, VG__BLUE );
-      }
-
-      /* keyframe selection */
-      ui_rect tag = { x, script[1], 16, 16 };
-
-      if( ui_inside_rect( tag, ctx->mouse ) )
-      {
-         absorb_by_keyframe = 1;
-
-         if( ui_click_down( ctx, UI_MOUSE_LEFT ) )
-         {
-            if( player_replay.active_keyframe != i )
-            {
-               player_replay.active_keyframe = i;
-               replay_seek( &player_replay.local, kf->time );
-            }
-         }
-         else
-         {
-            ui_outline( ctx, tag, 1, ui_colour(ctx, k_ui_fg), 0 );
-         }
-      }
-
-      /* edit controls */
-      u32 drag_colour = ui_opacity( ui_colour(ctx, k_ui_bg+2), 0.5f );
-      if( i == player_replay.active_keyframe )
-      {
-         ui_outline( ctx, tag, 2, ui_colour(ctx, k_ui_fg), 0 );
-
-         ui_rect tray = { tag[0]+8-32, tag[1]+16+2, 64, 16 };
-         ui_rect dragbar = { tray[0]+16, tray[1], 32, 16 };
-
-         bool pos_correct = 0;
-
-         if( ui_inside_rect( dragbar, ctx->mouse_click ) )
-         {
-            if( ui_clicking( ctx, UI_MOUSE_LEFT ) )
-            {
-               drag_colour = ui_opacity( ui_colour(ctx,k_ui_fg), 0.5f );
-               pos_correct = 1;
-               replay_seek( &player_replay.local, mouse_t );
-            }
-            else if( ui_click_up( ctx, UI_MOUSE_LEFT ) )
-            {
-               pos_correct = 1;
-               kf->time = mouse_t;
-               replay_keyframe_sort();
-
-               for( u32 j=0; j<player_replay.keyframe_count; j ++ )
-               {
-                  if( player_replay.keyframes[j].time == mouse_t )
-                  {
-                     player_replay.active_keyframe = j;
-                     break;
-                  }
-               }
-            }
-
-            if( pos_correct )
-            {
-               tag[0] = ctx->mouse[0]-8;
-               tray[0] = tag[0]+8-32;
-               dragbar[0] = tray[0]+16;
-            }
-         }
-
-         if( ui_inside_rect( dragbar, ctx->mouse ) )
-         {
-            ctx->cursor = k_ui_cursor_hand;
-         }
-
-         if( !pos_correct )
-         {
-            ui_fill( ctx, tray, 
-                     ui_opacity( ui_colour( ctx, k_ui_bg+2 ), 0.5f ) );
-         }
-
-         ui_fill( ctx, dragbar, drag_colour );
-         ui_text( ctx, dragbar, ":::", 1, k_ui_align_middle_center, 0 );
-
-         if( !pos_correct )
-         {
-            ui_rect btn = { tray[0], tray[1], 16, 16 };
-            if( ui_button_text( ctx, btn, "X", 1 ) == k_ui_button_click )
-            {
-               for( u32 j=i; j<player_replay.keyframe_count-1; j ++ )
-                  player_replay.keyframes[j] = player_replay.keyframes[j+1];
-
-               player_replay.keyframe_count --;
-               player_replay.active_keyframe = -1;
-            }
-
-            ui_rect btn1 = { tray[0]+48, tray[1], 16, 16 };
-
-            enum ui_button_state mask_using = 
-                  k_ui_button_holding_inside |
-                  k_ui_button_holding_outside |
-                  k_ui_button_click;
-
-            if( ui_button_text( ctx, btn1, "E", 1 ) & mask_using )
-            {
-               replay_fly_edit_keyframe( ctx, kf );
-               vg_ui_set_mouse_pos( btn1[0]+8, btn1[1]+8 );
-            }
-         }
-      }
-
-      ui_fill( ctx, tag, ui_colour( ctx, k_ui_blue ) );
-      lx = x;
-   }
-
-   /* adding keyframes */
-   if( ui_inside_rect( script, ctx->mouse ) )
-   {
-      ctx->cursor = k_ui_cursor_hand;
-
-      ui_rect cursor = { ctx->mouse[0], script[1], 4, 16 };
-      ui_fill( ctx, cursor, ui_colour( ctx, k_ui_fg ) );
-
-      if( !absorb_by_keyframe && ui_click_down( ctx, UI_MOUSE_LEFT ) )
-      {
-         u32 max = VG_ARRAY_LEN( player_replay.keyframes );
-         if( player_replay.keyframe_count == max )
-         {
-            ui_start_modal( ctx, "Maximum keyframes reached", UI_MODAL_BAD );
-         }
-         else 
-         {
-            replay_keyframe *kf = 
-               &player_replay.keyframes[player_replay.keyframe_count++];
-
-            kf->time = mouse_t;
-            v3_copy( g_render.cam.pos, kf->cam.pos );
-            v3_copy( g_render.cam.angles, kf->cam.angles );
-            kf->cam.fov = g_render.cam.fov;
-
-            replay_keyframe_sort();
-         }
-      }
-   }
-
-   /* timeline scrub */
-   bool start_in_timeline = 
-      ui_clicking(ctx, UI_MOUSE_LEFT) && 
-      ui_inside_rect(timeline, ctx->mouse_click);
-   if( (ui_inside_rect( timeline, ctx->mouse )) || start_in_timeline )
-   {
-      ui_rect cursor = { ctx->mouse[0], timeline[1], 4, timeline[3] };
-      ui_fill( ctx, cursor, ui_colour( ctx, k_ui_fg ) );
-      ctx->cursor = k_ui_cursor_ibeam;
-
-      if( ui_clicking( ctx, UI_MOUSE_LEFT ) && start_in_timeline )
-      {
-         replay_seek( &player_replay.local, mouse_t );
-         player_replay.active_keyframe = -1;
-      }
-   }
-
-   if( ui_button( ctx, panel, "Clear keyframes" ) == k_ui_button_click )
-   {
-      player_replay.keyframe_count = 0;
-   }
-
-   if( (ui_button( ctx, panel, "Hide UI (F2)" ) == k_ui_button_click) )
-   {
-      player_replay.show_ui ^= 0x1;
-   }
-
-   if( player_replay.active_keyframe != -1 )
-   {
-      replay_keyframe *kf = 
-         &player_replay.keyframes[ player_replay.active_keyframe ];
-
-      enum ui_button_state mask_using = 
-            k_ui_button_holding_inside |
-            k_ui_button_holding_outside |
-            k_ui_button_click;
-
-      if( ui_button( ctx, panel, "Edit cam" ) & mask_using )
-      {
-         replay_fly_edit_keyframe( ctx, kf );
-      }
-   }
-
-   ui_info( ctx, panel, "World settings" );
-   f32 new_time = world_current_instance()->time;
-   if( ui_slider( ctx, panel, "Time of day", 0, 1, &new_time ) )
-   {
-      world_current_instance()->time = new_time;
-   }
-
-   ui_info( ctx, panel, "" );
-   if( ui_button( ctx, panel, "Exit editor (F1)" ) == k_ui_button_click )
-   {
-      player_replay.editor_mode = 0;
-      replay_show_helpers();
-   }
-
-   /* TODO: Add Q/E scrub here too.
-    *       Add replay trimming 
-    */
-}
diff --git a/player_replay.h b/player_replay.h
deleted file mode 100644 (file)
index 313b4d4..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-#pragma once
-#include "player_render.h"
-#include "vg/vg_rigidbody.h"
-
-typedef struct replay_buffer replay_buffer;
-typedef struct replay_frame replay_frame;
-typedef struct replay_keyframe replay_keyframe;
-
-typedef struct replay_gamestate replay_gamestate;
-typedef struct replay_sfx replay_sfx;
-
-struct replay_buffer {
-   void *data;
-   u32 size; /* bytes */
-
-   replay_frame *head, *tail, *cursor_frame,
-                *statehead;
-   f64 cursor;
-};
-
-enum replay_framedata{
-   k_replay_framedata_animator,
-   k_replay_framedata_gamestate,
-   k_replay_framedata_internal_gamestate,
-   k_replay_framedata_sfx,
-   k_replay_framedata_glider,
-   k_replay_framedata_rows
-};
-
-struct replay_cam
-{
-   v3f pos, angles;
-   f32 fov;
-};
-
-struct replay_frame 
-{
-   struct replay_cam cam;
-   f64 time;
-
-   replay_frame *l, *r;
-
-   enum player_subsystem system;
-   u16 total_size;
-   u16 data_table[k_replay_framedata_rows][2];
-};
-
-/* player-defined replay frames */
-struct replay_keyframe
-{
-   struct replay_cam cam;
-   f64 time;
-};
-
-struct replay_gamestate 
-{
-   rigidbody rb, glider_rb; /* TODO: these don't need to be saved with their 
-                                     full matrices */
-   v3f angles;
-   struct player_cam_controller cam_control;
-   u32 current_run_version;
-   bool drowned;
-};
-
-/* we save this per-anim-frame. if there glider is existing in any state */
-struct replay_glider_data 
-{
-   bool have_glider, glider_orphan;
-   f32 t;
-   v3f co; 
-   v4f q;
-};
-
-struct replay_sfx {
-   u32 none;
-};
-
-struct replay_globals 
-{
-   replay_buffer local;
-   replay_frame *resume_target;
-   f64 resume_begin;
-   f32 resume_transition;
-
-   enum replay_control {
-      k_replay_control_scrub = 0x00,
-      k_replay_control_play  = 0x01,
-      k_replay_control_resume= 0x02
-   }
-   replay_control;
-   f32 track_velocity;
-   struct gui_helper *helper_resume, *helper_freecam;
-
-   vg_camera replay_freecam;
-
-   bool use_freecam;
-   bool show_ui;
-   v3f freecam_v, freecam_w;
-
-   i32 editor_mode;
-
-   replay_keyframe keyframes[32];
-   u32 keyframe_count;
-   i32 active_keyframe;
-}
-extern player_replay;
-
-int replay_seek( replay_buffer *replay, f64 t );
-
-replay_frame *replay_find_recent_stateframe( replay_buffer *replay );
-void replay_get_camera( replay_buffer *replay, vg_camera *cam );
-void replay_get_frame_camera( replay_frame *frame, vg_camera *cam );
-f32 replay_subframe_time( replay_buffer *replay );
-void replay_clear( replay_buffer *replay );
-void *
-replay_frame_data( replay_frame *frame, enum replay_framedata type );
-
-void skaterift_replay_pre_update(void);
-void skaterift_replay_imgui( ui_context *ctx );
-void skaterift_replay_debug_info( ui_context *ctx );
-void skaterift_record_frame( replay_buffer *replay, 
-                                    int force_gamestate );
-void skaterift_replay_post_render(void);
-void skaterift_replay_init(void);
-void skaterift_get_replay_cam( vg_camera *cam );
diff --git a/player_skate.c b/player_skate.c
deleted file mode 100644 (file)
index aea7027..0000000
+++ /dev/null
@@ -1,3664 +0,0 @@
-#include "player_skate.h"
-#include "player.h"
-#include "audio.h"
-#include "vg/vg_perlin.h"
-#include "vg/vg_lines.h"
-#include "menu.h"
-#include "ent_skateshop.h"
-#include "addon.h"
-#include "input.h"
-#include "ent_tornado.h"
-
-#include "vg/vg_rigidbody.h"
-#include "scene_rigidbody.h"
-#include "player_glide.h"
-#include "player_dead.h"
-#include "player_walk.h"
-#include <string.h>
-
-struct player_skate player_skate;
-struct player_subsystem_interface player_subsystem_skate = 
-{
-   .system_register = player__skate_register,
-   .bind = player__skate_bind,
-   .pre_update = player__skate_pre_update,
-   .update = player__skate_update,
-   .post_update = player__skate_post_update,
-   .im_gui = player__skate_im_gui,
-   .animate = player__skate_animate,
-   .pose = player__skate_pose,
-   .effects = player__skate_effects,
-   .post_animate = player__skate_post_animate,
-   .network_animator_exchange = player__skate_animator_exchange,
-   .sfx_oneshot = player__skate_sfx_oneshot,
-   .sfx_comp = player__skate_comp_audio,
-   .sfx_kill = player__skate_kill_audio,
-
-   .animator_data = &player_skate.animator,
-   .animator_size = sizeof(player_skate.animator),
-   .name = "Skate"
-};
-
-void player__skate_bind(void){
-   struct skeleton *sk = &localplayer.skeleton;
-   rb_update_matrices( &localplayer.rb );
-
-   struct { struct skeleton_anim **anim; const char *name; }
-   bindings[] = {
-      { &player_skate.anim_grind,        "pose_grind" },
-      { &player_skate.anim_grind_jump,   "pose_grind_jump" },
-      { &player_skate.anim_stand,        "pose_stand" },
-      { &player_skate.anim_highg,        "pose_highg" },
-      { &player_skate.anim_air,          "pose_air" },
-      { &player_skate.anim_slide,        "pose_slide" },
-      { &player_skate.anim_push,         "push" },
-      { &player_skate.anim_push_reverse, "push_reverse" },
-      { &player_skate.anim_ollie,        "ollie" },
-      { &player_skate.anim_ollie_reverse,"ollie_reverse" },
-      { &player_skate.anim_grabs,        "grabs" },
-      { &player_skate.anim_handplant,    "handplant" },
-   };
-
-   for( u32 i=0; i<VG_ARRAY_LEN(bindings); i++ )
-      *bindings[i].anim = skeleton_get_anim( sk, bindings[i].name );
-}
-
-void player__skate_kill_audio(void){
-   audio_lock();
-   if( player_skate.aud_main ){
-      player_skate.aud_main = 
-         audio_channel_fadeout( player_skate.aud_main, 0.1f );
-   }
-   if( player_skate.aud_air ){
-      player_skate.aud_air = 
-         audio_channel_fadeout( player_skate.aud_air, 0.1f );
-   }
-   if( player_skate.aud_slide ){
-      player_skate.aud_slide = 
-         audio_channel_fadeout( player_skate.aud_slide, 0.1f );
-   }
-   audio_unlock();
-}
-
-/* 
- * Collision detection routines
- *
- *
- */
-
-/*
- * Does collision detection on a sphere vs world, and applies some smoothing
- * filters to the manifold afterwards
- */
-static int skate_collide_smooth( m4x3f mtx, f32 r, rb_ct *man ){
-   world_instance *world = world_current_instance();
-
-   int len = 0;
-   len = rb_sphere__scene( mtx, r, NULL, world->geo_bh, man,
-                           k_material_flag_walking );
-
-   for( int i=0; i<len; i++ ){
-      man[i].rba = &localplayer.rb;
-      man[i].rbb = NULL;
-   }
-
-   rb_manifold_filter_coplanar( man, len, 0.03f );
-
-   if( len > 1 ){
-      rb_manifold_filter_backface( man, len );
-      rb_manifold_filter_joint_edges( man, len, 0.03f );
-      rb_manifold_filter_pairs( man, len, 0.03f );
-   }
-   int new_len = rb_manifold_apply_filtered( man, len );
-   if( len && !new_len )
-      len = 1;
-   else
-      len = new_len;
-
-   return len;
-}
-
-struct grind_info
-{
-   v3f co, dir, n;
-};
-
-static int skate_grind_scansq( v3f pos, v3f dir, float r,
-                                  struct grind_info *inf ){
-   world_instance *world = world_current_instance();
-
-   v4f plane;
-   v3_copy( dir, plane );
-   v3_normalize( plane );
-   plane[3] = v3_dot( plane, pos );
-
-   boxf box;
-   v3_add( pos, (v3f){ r, r, r }, box[1] );
-   v3_sub( pos, (v3f){ r, r, r }, box[0] );
-
-   struct grind_sample{
-      v2f co;
-      v2f normal;
-      v3f normal3,
-          centroid;
-   }
-   samples[48];
-   int sample_count = 0;
-
-   v2f support_min,
-       support_max;
-
-   v3f support_axis;
-   v3_cross( plane, (v3f){0,1,0}, support_axis );
-   v3_normalize( support_axis );
-   
-   bh_iter it;
-   bh_iter_init_box( 0, &it, box );
-   i32 idx;
-   
-   while( bh_next( world->geo_bh, &it, &idx ) ){
-      u32 *ptri = &world->scene_geo.arrindices[ idx*3 ];
-      v3f tri[3];
-
-      struct world_surface *surf = world_tri_index_surface(world,ptri[0]);
-      if( !(surf->info.flags & k_material_flag_grindable) )
-         continue;
-
-      for( int j=0; j<3; j++ )
-         v3_copy( world->scene_geo.arrvertices[ptri[j]].co, tri[j] );
-
-      for( int j=0; j<3; j++ ){
-         int i0 = j,
-             i1 = (j+1) % 3;
-         
-         struct grind_sample *sample = &samples[ sample_count ];
-         v3f co;
-
-         if( plane_segment( plane, tri[i0], tri[i1], co ) ){
-            v3f d;
-            v3_sub( co, pos, d );
-            if( v3_length2( d ) > r*r )
-               continue;
-
-            v3f va, vb, normal;
-            v3_sub( tri[1], tri[0], va );
-            v3_sub( tri[2], tri[0], vb );
-            v3_cross( va, vb, normal );
-
-            sample->normal[0] = v3_dot( support_axis, normal );
-            sample->normal[1] = normal[1];
-            sample->co[0]     = v3_dot( support_axis, d );
-            sample->co[1]     = d[1];
-
-            v3_copy( normal, sample->normal3 ); /* normalize later
-                                                   if we want to us it */
-
-            v3_muls(                      tri[0], 1.0f/3.0f, sample->centroid );
-            v3_muladds( sample->centroid, tri[1], 1.0f/3.0f, sample->centroid );
-            v3_muladds( sample->centroid, tri[2], 1.0f/3.0f, sample->centroid );
-
-            v2_normalize( sample->normal );
-            sample_count ++;
-
-            if( sample_count == VG_ARRAY_LEN( samples ) )
-               goto too_many_samples;
-         }
-      }
-   }
-
-too_many_samples:
-
-   if( sample_count < 2 )
-      return 0;
-
-   v3f average_direction,
-       average_normal;
-
-   v2f min_co, max_co;
-   v2_fill( min_co,  INFINITY );
-   v2_fill( max_co, -INFINITY );
-
-   v3_zero( average_direction );
-   v3_zero( average_normal );
-
-   int passed_samples = 0;
-   
-   for( int i=0; i<sample_count-1; i++ ){
-      struct grind_sample *si, *sj;
-
-      si = &samples[i];
-
-      for( int j=i+1; j<sample_count; j++ ){
-         if( i == j )
-            continue;
-
-         sj = &samples[j];
-
-         /* non overlapping */
-         if( v2_dist2( si->co, sj->co ) >= (0.01f*0.01f) )
-            continue;
-
-         /* not sharp angle */
-         if( v2_dot( si->normal, sj->normal ) >= 0.7f )
-            continue;
-
-         /* not convex */
-         v3f v0;
-         v3_sub( sj->centroid, si->centroid, v0 );
-         if( v3_dot( v0, si->normal3 ) >= 0.0f ||
-             v3_dot( v0, sj->normal3 ) <= 0.0f )
-            continue;
-
-         v2_minv( sj->co, min_co, min_co );
-         v2_maxv( sj->co, max_co, max_co );
-         
-         v3f n0, n1, dir;
-         v3_copy( si->normal3, n0 );
-         v3_copy( sj->normal3, n1 );
-         v3_cross( n0, n1, dir );
-
-         if( v3_length2( dir ) <= 0.000001f )
-            continue;
-
-         v3_normalize( dir );
-
-         /* make sure the directions all face a common hemisphere */
-         v3_muls( dir, vg_signf(v3_dot(dir,plane)), dir );
-         v3_add( average_direction, dir, average_direction );
-
-         float yi = si->normal3[1],
-               yj = sj->normal3[1];
-
-         if( yi > yj ) v3_add( si->normal3, average_normal, average_normal );
-         else          v3_add( sj->normal3, average_normal, average_normal );
-
-         passed_samples ++;
-      }
-   }
-
-   if( !passed_samples )
-      return 0;
-
-   if( (v3_length2( average_direction ) <= 0.001f) ||
-       (v3_length2( average_normal ) <= 0.001f ) )
-      return 0;
-
-   float div = 1.0f/(float)passed_samples;
-   v3_normalize( average_direction );
-   v3_normalize( average_normal );
-
-   v2f average_coord;
-   v2_add( min_co, max_co, average_coord );
-   v2_muls( average_coord, 0.5f, average_coord );
-
-   v3_muls( support_axis, average_coord[0], inf->co );
-   inf->co[1] += average_coord[1];
-   v3_add( pos, inf->co, inf->co );
-   v3_copy( average_normal, inf->n );
-   v3_copy( average_direction, inf->dir );
-
-   vg_line_point( inf->co, 0.02f, VG__GREEN );
-   vg_line_arrow( inf->co, average_direction, 0.3f, VG__GREEN );
-   vg_line_arrow( inf->co, inf->n, 0.2f, VG__CYAN );
-
-   return passed_samples;
-}
-
-static void reset_jump_info( jump_info *inf ){
-   inf->log_length = 0;
-   inf->land_dist = 0.0f;
-   inf->score = 0.0f;
-   inf->type = k_prediction_unset;
-   v3_zero( inf->apex );
-}
-
-static int create_jumps_to_hit_target( jump_info *jumps,
-                                          v3f target, float max_angle_delta,
-                                          float gravity ){
-   /* calculate the exact 2 solutions to jump onto that grind spot */
-
-   v3f v0;
-   v3_sub( target, localplayer.rb.co, v0 );
-
-   v3f ax;
-   v3_copy( v0, ax );
-   ax[1] = 0.0f;
-   v3_normalize( ax );
-
-   v2f d = { v3_dot( ax, v0 ),               v0[1] },
-       v = { v3_dot( ax, localplayer.rb.v ), localplayer.rb.v[1] };
-
-   float a = atan2f( v[1], v[0] ),
-         m = v2_length( v ),
-         root = m*m*m*m - gravity*(gravity*d[0]*d[0] + 2.0f*d[1]*m*m);
-
-   int valid_count = 0;
-
-   if( root > 0.0f ){
-      root = sqrtf( root );
-      float a0 = atanf( (m*m + root) / (gravity * d[0]) ),
-            a1 = atanf( (m*m - root) / (gravity * d[0]) );
-
-      if( fabsf(a0-a) < max_angle_delta ){
-         jump_info *inf = &jumps[ valid_count ++ ];
-         reset_jump_info( inf );
-
-         v3_muls( ax, cosf( a0 ) * m, inf->v );
-         inf->v[1] += sinf( a0 ) * m;
-         inf->land_dist = d[0] / (cosf(a0)*m);
-         inf->gravity = gravity;
-
-         v3_copy( target, inf->log[inf->log_length ++] );
-      }
-
-      if( fabsf(a1-a) < max_angle_delta ){
-         jump_info *inf = &jumps[ valid_count ++ ];
-         reset_jump_info( inf );
-
-         v3_muls( ax, cosf( a1 ) * m, inf->v );
-         inf->v[1] += sinf( a1 ) * m;
-         inf->land_dist = d[0] / (cosf(a1)*m);
-         inf->gravity = gravity;
-
-         v3_copy( target, inf->log[inf->log_length ++] );
-      }
-   }
-
-   return valid_count;
-}
-
-void player__approximate_best_trajectory(void)
-{
-   world_instance *world0 = world_current_instance();
-
-   float k_trace_delta = vg.time_fixed_delta * 10.0f;
-   struct player_skate_state *state = &player_skate.state;
-
-   state->air_start = vg.time;
-   v3_copy( localplayer.rb.v, state->air_init_v );
-   v3_copy( localplayer.rb.co, state->air_init_co );
-
-   player_skate.possible_jump_count = 0;
-
-   v3f axis;
-   v3_cross( localplayer.rb.v, localplayer.rb.to_world[1], axis );
-   v3_normalize( axis );
-
-   /* at high slopes, Y component is low */
-   float upness      = localplayer.rb.to_world[1][1],
-         angle_begin = -(1.0f-fabsf( upness )),
-         angle_end   =   1.0f;
-
-   struct grind_info grind;
-   int grind_located = 0;
-   float grind_located_gravity = k_gravity;
-
-
-   v3f launch_v_bounds[2];
-
-   for( int i=0; i<2; i++ ){
-      v3_copy( localplayer.rb.v, launch_v_bounds[i] );
-      float ang = (float[]){ angle_begin, angle_end }[ i ];
-            ang *= 0.15f;
-
-      v4f qbias;
-      q_axis_angle( qbias, axis, ang );
-      q_mulv( qbias, launch_v_bounds[i], launch_v_bounds[i] );
-   }
-
-   for( int m=0;m<=30; m++ ){
-      jump_info *inf = 
-         &player_skate.possible_jumps[ player_skate.possible_jump_count ++ ];
-      reset_jump_info( inf );
-
-      v3f launch_co, launch_v, co0, co1;
-      v3_copy( localplayer.rb.co, launch_co );
-      v3_copy( localplayer.rb.v,  launch_v );
-      v3_copy( launch_co, co0 );
-      world_instance *trace_world = world0;
-
-      float vt  = (float)m * (1.0f/30.0f),
-            ang = vg_lerpf( angle_begin, angle_end, vt ) * 0.15f;
-
-      v4f qbias;
-      q_axis_angle( qbias, axis, ang );
-      q_mulv( qbias, launch_v, launch_v );
-
-      float yaw_sketch = 1.0f-fabsf(upness);
-
-      float yaw_bias = ((float)(m%3) - 1.0f) * 0.08f * yaw_sketch;
-      q_axis_angle( qbias, localplayer.rb.to_world[1], yaw_bias );
-      q_mulv( qbias, launch_v, launch_v );
-
-      float gravity_bias = vg_lerpf( 0.85f, 1.4f, vt ),
-            gravity      = k_gravity * gravity_bias;
-      inf->gravity = gravity;
-      v3_copy( launch_v, inf->v );
-
-      /* initial conditions */
-      v3f v;
-      v3_copy( launch_v, v );
-      v3_copy( launch_co, co1 );
-
-      for( int i=1; i<=50; i++ ){
-         f32 t = (f32)i * k_trace_delta;
-
-         /* integrate forces */
-         v3f a;
-         ent_tornado_forces( co1, v, a );
-         a[1] -= gravity;
-
-         /* position */
-         v3_muladds( co1, v, k_trace_delta, co1 );
-         v3_muladds( co1, a, 0.5f*k_trace_delta*k_trace_delta, co1 );
-
-         /* velocity */
-         v3_muladds( v, a, k_trace_delta, v );
-
-         int search_for_grind = 1;
-         if( grind_located ) search_for_grind = 0;
-         if( v[1] > 0.0f ) search_for_grind = 0;
-
-         /* REFACTOR */
-
-         v3f closest={0.0f,0.0f,0.0f};
-         if( search_for_grind ){
-            if( bh_closest_point(trace_world->geo_bh,co1,closest,1.0f) != -1 ){
-               float min_dist = 0.75f;
-                     min_dist *= min_dist;
-
-               if( v3_dist2( closest, launch_co ) < min_dist )
-                  search_for_grind = 0;
-
-               v3f bound[2];
-
-               for( int j=0; j<2; j++ ){
-                  v3_muls( launch_v_bounds[j], t, bound[j] );
-                  bound[j][1] += -0.5f*gravity*t*t;
-                  v3_add( launch_co, bound[j], bound[j] );
-               }
-
-               float limh = vg_minf( 2.0f, t ),
-                     minh = vg_minf( bound[0][1], bound[1][1] )-limh,
-                     maxh = vg_maxf( bound[0][1], bound[1][1] )+limh;
-
-               if( (closest[1] < minh) || (closest[1] > maxh) ){
-                  search_for_grind = 0;
-               }
-            }
-            else
-               search_for_grind = 0;
-         }
-
-         if( search_for_grind ){
-            if( skate_grind_scansq( closest, v, 0.5f, &grind ) ){
-               /* check alignment */
-               v2f v0 = { v[0], v[2] },
-                   v1 = { grind.dir[0], grind.dir[2] };
-
-               v2_normalize( v0 );
-               v2_normalize( v1 );
-
-               float a = v2_dot( v0, v1 );
-
-               float a_min = cosf( VG_PIf * 0.185f );
-               if( state->grind_cooldown )
-                  a_min = cosf( VG_PIf * 0.05f );
-
-               /* check speed */
-               if( (fabsf(v3_dot( v, grind.dir ))>=k_grind_axel_min_vel) &&
-                   (a >= a_min) && 
-                   (fabsf(grind.dir[1]) < 0.70710678118654752f))
-               {
-                  grind_located = 1;
-                  grind_located_gravity = inf->gravity;
-               }
-            }
-         }
-
-         if( trace_world->rendering_gate ){
-            ent_gate *gate = trace_world->rendering_gate;
-            if( gate_intersect( gate, co1, co0 ) ){
-               m4x3_mulv( gate->transport, co0, co0 );
-               m4x3_mulv( gate->transport, co1, co1 );
-               m3x3_mulv( gate->transport, launch_v, launch_v);
-               m4x3_mulv( gate->transport, launch_co, launch_co );
-
-               if( gate->flags & k_ent_gate_nonlocal )
-                  trace_world = &world_static.instances[ gate->target ];
-            }
-         }
-
-         float t1;
-         v3f n;
-
-         float scan_radius = k_board_radius;
-               scan_radius *= vg_clampf( t, 0.02f, 1.0f );
-
-         int idx = spherecast_world( trace_world, co0, co1, scan_radius, &t1, n,
-                                     k_material_flag_walking );
-         if( idx != -1 ){
-            v3f co;
-            v3_lerp( co0, co1, t1, co );
-            v3_copy( co, inf->log[ inf->log_length ++ ] ); 
-
-            v3_copy( n, inf->n );
-            u32 *tri = &trace_world->scene_geo.arrindices[ idx*3 ];
-            struct world_surface *surf = 
-               world_tri_index_surface( trace_world, tri[0] );
-
-            inf->type = k_prediction_land;
-            inf->score = -v3_dot( v, inf->n );
-            inf->land_dist = t + k_trace_delta * t1;
-
-            /* Bias prediction towords ramps */
-            if( !(surf->info.flags & k_material_flag_skate_target) )
-               inf->score *= 10.0f;
-
-            if( surf->info.flags & k_material_flag_boundary )
-               player_skate.possible_jump_count --;
-
-            break;
-         }
-         
-         if( i % 3 == 0 )
-            v3_copy( co1, inf->log[ inf->log_length ++ ] ); 
-         v3_copy( co1, co0 );
-      }
-
-      if( inf->type == k_prediction_unset )
-         player_skate.possible_jump_count --;
-   }
-
-   if( grind_located ){
-      jump_info grind_jumps[2];
-      
-      int valid_count = 
-         create_jumps_to_hit_target( grind_jumps, grind.co, 
-                                     0.175f*VG_PIf, grind_located_gravity );
-
-      /* knock out original landing points in the 1m area */
-      for( u32 j=0; j<player_skate.possible_jump_count; j++ ){
-         jump_info *jump = &player_skate.possible_jumps[ j ];
-         float dist = v3_dist2( jump->log[jump->log_length-1], grind.co );
-         float descale = 1.0f-vg_minf(1.0f,dist);
-         jump->score += descale*3.0f;
-      }
-
-      for( int i=0; i<valid_count; i++ ){
-         jump_info *jump = &grind_jumps[i];
-         jump->type = k_prediction_grind;
-
-         v3f launch_v, launch_co, co0, co1;
-
-         v3_copy( jump->v, launch_v );
-         v3_copy( localplayer.rb.co, launch_co );
-         
-         float t = 0.05f * jump->land_dist;
-         v3_muls( launch_v, t, co0 );
-         co0[1] += -0.5f * jump->gravity * t*t;
-         v3_add( launch_co, co0, co0 );
-
-         /* rough scan to make sure we dont collide with anything */
-         for( int j=1; j<=16; j++ ){
-            t  = (float)j*(1.0f/16.0f);
-            t *= 0.9f;
-            t += 0.05f;
-            t *= jump->land_dist;
-
-            v3_muls( launch_v, t, co1 );
-            co1[1] += -0.5f * jump->gravity * t*t;
-            v3_add( launch_co, co1, co1 );
-            
-            float t1;
-            v3f n;
-
-            int idx = spherecast_world( world0, co0,co1,
-                                        k_board_radius*0.1f, &t1, n, 
-                                        k_material_flag_walking );
-            if( idx != -1 ){
-               goto invalidated_grind;
-            }
-
-            v3_copy( co1, co0 );
-         }
-
-         v3_copy( grind.n, jump->n );
-
-         /* determine score */
-         v3f ve;
-         v3_copy( jump->v, ve );
-         ve[1] += -jump->gravity*jump->land_dist;
-         jump->score = -v3_dot( ve, grind.n ) * 0.9f;
-
-         player_skate.possible_jumps[ player_skate.possible_jump_count ++ ] = 
-            *jump;
-
-         continue;
-invalidated_grind:;
-      }
-   }
-
-
-   float score_min =  INFINITY,
-         score_max = -INFINITY;
-
-   jump_info *best = NULL;
-
-   for( int i=0; i<player_skate.possible_jump_count; i ++ ){
-      jump_info *jump = &player_skate.possible_jumps[i];
-
-      if( jump->score < score_min )
-         best = jump;
-
-      score_min = vg_minf( score_min, jump->score );
-      score_max = vg_maxf( score_max, jump->score );
-   }
-
-   for( int i=0; i<player_skate.possible_jump_count; i ++ ){
-      jump_info *jump = &player_skate.possible_jumps[i];
-      float s = jump->score;
-
-      s -= score_min;
-      s /= (score_max-score_min);
-      s  = 1.0f - s;
-
-      jump->score = s;
-      jump->colour = s * 255.0f;
-
-      if( jump == best )
-         jump->colour <<= 16;
-      else if( jump->type == k_prediction_land )
-         jump->colour <<= 8;
-      
-      jump->colour |= 0xff000000;
-   }
-
-   if( best ){
-      v3_copy( best->n, state->land_normal );
-      v3_copy( best->v, localplayer.rb.v );
-      state->land_dist = best->land_dist;
-      state->gravity_bias = best->gravity;
-
-      if( best->type == k_prediction_grind ){
-         state->activity = k_skate_activity_air_to_grind;
-      }
-
-      v2f steer;
-      joystick_state( k_srjoystick_steer, steer );
-      v2_normalize_clamp( steer );
-
-      if( (fabsf(steer[1]) > 0.5f) && (state->land_dist >= 1.5f) ){
-         state->flip_rate = (1.0f/state->land_dist) * vg_signf(steer[1]) *
-                                 state->reverse ;
-         state->flip_time = 0.0f;
-         v3_copy( localplayer.rb.to_world[0], state->flip_axis );
-      }
-      else{
-         state->flip_rate = 0.0f;
-         v3_zero( state->flip_axis );
-      }
-   }
-   else
-      v3_copy( (v3f){0,1,0}, state->land_normal );
-}
-
-/*
- *
- * Varius physics models
- * ------------------------------------------------
- */
-
-/*
- * Air control, no real physics
- */
-static void skate_apply_air_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( state->activity_prev > k_skate_activity_air_to_grind )
-      player__approximate_best_trajectory();
-
-   float angle = v3_dot( localplayer.rb.to_world[1], state->land_normal );
-   angle = vg_clampf( angle, -1.0f, 1.0f );
-   v3f axis; 
-   v3_cross( localplayer.rb.to_world[1], state->land_normal, axis );
-
-   v4f correction;
-   q_axis_angle( correction, axis, 
-                  acosf(angle)*2.0f*VG_TIMESTEP_FIXED );
-   q_mul( correction, localplayer.rb.q, localplayer.rb.q );
-}
-
-static enum trick_type player_skate_trick_input(void);
-static void skate_apply_trick_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   v3f Fd, Fs, F;
-   v3f strength = { 3.7f, 3.6f, 8.0f };
-
-   v3_muls( state->trick_residualv, -4.0f , Fd );
-   v3_muls( state->trick_residuald, -10.0f, Fs );
-   v3_add( Fd, Fs, F );
-   v3_mul( strength, F, F );
-
-   v3_muladds( state->trick_residualv, F, vg.time_fixed_delta, 
-               state->trick_residualv );
-   v3_muladds( state->trick_residuald, state->trick_residualv,
-               vg.time_fixed_delta, state->trick_residuald );
-
-   if( state->activity <= k_skate_activity_air_to_grind ){
-      if( v3_length2( state->trick_vel ) < 0.0001f )
-         return;
-
-      int carry_on = state->trick_type == player_skate_trick_input();
-
-      /* we assume velocities share a common divisor, in which case the 
-       * interval is the minimum value (if not zero) */
-
-      float min_rate = 99999.0f;
-
-      for( int i=0; i<3; i++ ){
-         float v = state->trick_vel[i];
-         if( (v > 0.0f) && (v < min_rate) )
-            min_rate = v;
-      }
-
-      float interval = 1.0f / min_rate,
-            current  = floorf( state->trick_time ),
-            next_end = current+1.0f;
-
-
-      /* integrate trick velocities */
-      v3_muladds( state->trick_euler, state->trick_vel, vg.time_fixed_delta,
-                  state->trick_euler );
-
-      if( !carry_on && (state->trick_time + vg.time_fixed_delta/interval >= next_end) ){
-         state->trick_time = 0.0f;
-         state->trick_euler[0] = roundf( state->trick_euler[0] );
-         state->trick_euler[1] = roundf( state->trick_euler[1] );
-         state->trick_euler[2] = roundf( state->trick_euler[2] );
-         v3_copy( state->trick_vel, state->trick_residualv );
-         v3_zero( state->trick_vel );
-
-         audio_lock();
-         audio_oneshot_3d( &audio_flips[vg_randu32(&vg.rand)%4], 
-               localplayer.rb.co, 40.0f, 1.0f );
-         audio_unlock();
-      }
-      else 
-         state->trick_time += vg.time_fixed_delta / interval;
-   }
-   else{
-      if( (v3_length2(state->trick_vel) >= 0.0001f ) &&
-          state->trick_time > 0.2f)
-      {
-         vg_info( "player fell off due to lack of skill\n" );
-         player__dead_transition( k_player_die_type_feet );
-      }
-
-      state->trick_euler[0] = roundf( state->trick_euler[0] );
-      state->trick_euler[1] = roundf( state->trick_euler[1] );
-      state->trick_euler[2] = roundf( state->trick_euler[2] );
-      state->trick_time = 0.0f;
-      v3_zero( state->trick_vel );
-   }
-}
-
-static void skate_apply_grab_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   float grabt = axis_state( k_sraxis_grab );
-
-   if( grabt > 0.5f ){
-      v2_muladds( state->grab_mouse_delta, vg.mouse_delta, 0.02f, 
-                  state->grab_mouse_delta );
-
-      v2_normalize_clamp( state->grab_mouse_delta );
-   }
-   else
-      v2_zero( state->grab_mouse_delta );
-
-   state->grabbing = vg_lerpf( state->grabbing, grabt, 8.4f*vg.time_fixed_delta );
-}
-
-static void skate_apply_steering_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   v2f jsteer;
-   joystick_state( k_srjoystick_steer, jsteer );
-
-   /* Steering */
-   float steer = jsteer[0],
-         grab  = axis_state( k_sraxis_grab );
-
-   steer = vg_signf( steer ) * steer*steer * k_steer_ground;
-
-   v3f steer_axis;
-   v3_muls( localplayer.rb.to_world[1], -vg_signf( steer ), steer_axis );
-
-   float rate = 26.0f,
-         top  = 1.0f;
-
-   f32 skid_target = 0.0f;
-
-   if( state->activity <= k_skate_activity_air_to_grind ){
-      rate = 6.0f * fabsf(steer);
-      top  = 1.5f;
-   }
-   else{
-      /* rotate slower when grabbing on ground */
-      steer *= (1.0f-(state->jump_charge+grab)*0.4f);
-
-      if( state->activity == k_skate_activity_grind_5050 ){
-         rate = 0.0f;
-         top  = 0.0f;
-      }
-
-      else if( state->activity >= k_skate_activity_grind_any ){
-         rate *= fabsf(steer);
-
-         float a = 0.8f * -steer * vg.time_fixed_delta;
-
-         v4f q;
-         q_axis_angle( q, localplayer.rb.to_world[1], a );
-         q_mulv( q, player_skate.grind_vec, player_skate.grind_vec );
-
-         v3_normalize( player_skate.grind_vec );
-      }
-
-      else if( state->manual_direction ){
-         rate = 35.0f;
-         top  = 1.5f;
-      }
-      else {
-         f32 skid = axis_state(k_sraxis_skid);
-
-         /* skids on keyboard lock to the first direction pressed */
-         if( vg_input.display_input_method == k_input_method_kbm ){
-            if( button_press(k_srbind_skid) && (fabsf(state->skid)<0.01f) &&
-                (fabsf(steer) > 0.4f) ){
-               state->skid = vg_signf( steer ) * 0.02f;
-            }
-
-            if( button_press(k_srbind_skid) && (fabsf(state->skid)>0.01f) ){
-               skid_target = vg_signf( state->skid );
-            }
-         }
-         else {
-            if( fabsf(skid) > 0.1f ){
-               skid_target = skid;
-            }
-         }
-      }
-
-      if( grab < 0.5f ){
-         top *= 1.0f+v3_length( state->throw_v )*k_mmthrow_steer;
-      }
-   }
-
-   vg_slewf( &state->skid, skid_target, vg.time_fixed_delta*(1.0f/0.1f) );
-   steer = vg_lerpf( steer, state->skid*k_steer_ground*0.5f, 
-                     fabsf(state->skid*0.8f) );
-
-   float current  = v3_dot( localplayer.rb.to_world[1], localplayer.rb.w ),
-         addspeed = (steer * -top) - current,
-         maxaccel = rate * vg.time_fixed_delta,
-         accel    = vg_clampf( addspeed, -maxaccel, maxaccel );
-
-   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[1], 
-               accel, localplayer.rb.w );
-}
-
-/*
- * Computes friction and surface interface model
- */
-static void skate_apply_friction_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   /*
-    * Computing localized friction forces for controlling the character
-    * Friction across X is significantly more than Z
-    */
-
-   v3f vel;
-   m3x3_mulv( localplayer.rb.to_local, localplayer.rb.v, vel );
-   float slip = 0.0f;
-   
-   if( fabsf(vel[2]) > 0.01f )
-      slip = fabsf(-vel[0] / vel[2]) * vg_signf(vel[0]);
-
-   if( fabsf( slip ) > 1.2f )
-      slip = vg_signf( slip ) * 1.2f;
-
-   state->slip = slip;
-   state->reverse = -vg_signf(vel[2]);
-
-   f32 lat = k_friction_lat;
-
-   if( fabsf(axis_state(k_sraxis_skid)) > 0.1f ){
-      if( (player_skate.surface == k_surface_prop_snow) ||
-          (player_skate.surface == k_surface_prop_sand) ){
-         lat *= 8.0f;
-      }
-      else
-         lat *= 1.5f;
-   }
-
-   if( player_skate.surface == k_surface_prop_snow )
-      lat *= 0.5f;
-   else if( player_skate.surface == k_surface_prop_sand )
-      lat *= 0.6f;
-
-   vel[0] += vg_cfrictf( vel[0], lat * vg.time_fixed_delta );
-   vel[2] += vg_cfrictf( vel[2], k_friction_resistance * vg.time_fixed_delta );
-
-   /* Pushing additive force */
-
-   if( !button_press( k_srbind_jump ) && (fabsf(state->skid)<0.1f) ){
-      if( button_press( k_srbind_push ) || (vg.time-state->start_push<0.75) ){
-         if( (vg.time - state->cur_push) > 0.25 )
-            state->start_push = vg.time;
-
-         state->cur_push = vg.time;
-
-         double push_time = vg.time - state->start_push;
-
-         float cycle_time = push_time*k_push_cycle_rate,
-               accel      = k_push_accel * (sinf(cycle_time)*0.5f+0.5f),
-               amt        = accel * VG_TIMESTEP_FIXED,
-               current    = v3_length( vel ),
-               new_vel    = vg_minf( current + amt, k_max_push_speed ),
-               delta      = new_vel - vg_minf( current, k_max_push_speed );
-
-         vel[2] += delta * -state->reverse;
-      }
-   }
-
-   /* Send back to velocity */
-   m3x3_mulv( localplayer.rb.to_world, vel, localplayer.rb.v );
-}
-
-static void skate_apply_jump_model(void){
-   struct player_skate_state *state = &player_skate.state;
-   int charging_jump_prev = state->charging_jump;
-   state->charging_jump = button_press( k_srbind_jump );
-
-   /* Cannot charge this in air */
-   if( state->activity <= k_skate_activity_air_to_grind ){
-      state->charging_jump = 0;
-      return;
-   }
-
-   if( state->charging_jump ){
-      state->jump_charge += vg.time_fixed_delta * k_jump_charge_speed;
-
-      if( !charging_jump_prev )
-         state->jump_dir = state->reverse>0.0f? 1: 0;
-   }
-   else{
-      state->jump_charge -= k_jump_charge_speed * vg.time_fixed_delta;
-   }
-
-   state->jump_charge = vg_clampf( state->jump_charge, 0.0f, 1.0f );
-
-   /* player let go after charging past 0.2: trigger jump */
-   if( (!state->charging_jump) && (state->jump_charge > 0.2f) ){
-      v3f jumpdir;
-      
-      /* Launch more up if alignment is up else improve velocity */
-      float aup = localplayer.rb.to_world[1][1],
-            mod = 0.5f,
-            dir = mod + fabsf(aup)*(1.0f-mod);
-
-      if( state->activity == k_skate_activity_ground ){
-         v3_copy( localplayer.rb.v, jumpdir );
-         v3_normalize( jumpdir );
-         v3_muls( jumpdir, 1.0f-dir, jumpdir );
-         v3_muladds( jumpdir, localplayer.rb.to_world[1], dir, jumpdir );
-         v3_normalize( jumpdir );
-      }else{
-         v3_copy( state->up_dir, jumpdir );
-         state->grind_cooldown = 30;
-         state->activity = k_skate_activity_ground;
-
-         v2f steer;
-         joystick_state( k_srjoystick_steer, steer );
-
-         float tilt  = steer[0] * 0.3f;
-               tilt *= vg_signf(v3_dot( localplayer.rb.v, 
-                                        player_skate.grind_dir ));
-
-         v4f qtilt;
-         q_axis_angle( qtilt, player_skate.grind_dir, tilt );
-         q_mulv( qtilt, jumpdir, jumpdir );
-      }
-      state->surface_cooldown = 10;
-      state->trick_input_collect = 0.0f;
-      
-      float force = k_jump_force*state->jump_charge;
-      v3_muladds( localplayer.rb.v, jumpdir, force, localplayer.rb.v );
-      state->jump_charge = 0.0f;
-      state->jump_time = vg.time;
-      player__networked_sfx( k_player_subsystem_skate, 32, 
-                             k_player_skate_soundeffect_jump,
-                             localplayer.rb.co, 1.0f );
-   }
-}
-
-static void skate_apply_handplant_model(void){
-   struct player_skate_state *state = &player_skate.state;
-   if( localplayer.rb.to_world[1][1] < -0.1f ) return;
-   if( localplayer.rb.to_world[1][1] > 0.6f ) return;
-   if( !( button_press(k_srbind_skid) || (fabsf(state->skid)>0.1f)) ) return;
-
-   v3f lco = { 0.0f, -0.2f, -state->reverse },
-       co, dir;
-   m4x3_mulv( localplayer.rb.to_world, lco, co );
-   v3_muls( localplayer.rb.to_world[2], state->reverse, dir );
-   vg_line_arrow( co, dir, 0.13f, 0xff000000 );
-
-   ray_hit hit = { .dist = 2.0f };
-   if( ray_world( world_current_instance(), co, dir, 
-                  &hit, k_material_flag_ghosts )) {
-      vg_line( co, hit.pos, 0xff000000 );
-      vg_line_point( hit.pos, 0.1f, 0xff000000 );
-
-      if( hit.normal[1] < 0.7f ) return;
-      if( hit.dist < 0.95f ) return;
-
-      state->activity = k_skate_activity_handplant;
-      state->handplant_t = 0.0f;
-      v3_copy( localplayer.rb.co, state->store_co );
-      v3_copy( localplayer.rb.v, state->air_init_v );
-      v4_copy( localplayer.rb.q, state->store_q );
-      v3_copy( state->cog, state->store_cog );
-      v3_copy( state->cog_v, state->store_cog_v );
-      v4_copy( state->smoothed_rotation, state->store_smoothed );
-   }
-}
-
-static void skate_apply_pump_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( state->activity != k_skate_activity_ground ){
-      v3_zero( state->throw_v );
-      return;
-   }
-
-   /* Throw / collect routine 
-    */
-   if( axis_state( k_sraxis_grab ) > 0.5f ){
-      if( state->activity == k_skate_activity_ground ){
-         /* Throw */
-         v3_muls( localplayer.rb.to_world[1], k_mmthrow_scale, state->throw_v );
-      }
-   }
-   else{
-      /* Collect */
-      f32 doty = v3_dot( localplayer.rb.to_world[1], state->throw_v );
-      
-      v3f Fl, Fv;
-      v3_muladds( state->throw_v, localplayer.rb.to_world[1], -doty, Fl);
-      player_skate.collect_feedback = v3_length(Fl) * 4.0f;
-
-      if( state->activity == k_skate_activity_ground ){
-         if( v3_length2(localplayer.rb.v)<(20.0f*20.0f) ){
-            v3_muladds( localplayer.rb.v, Fl, 
-                        k_mmcollect_lat, localplayer.rb.v );
-         }
-         v3_muladds( state->throw_v, Fl, -k_mmcollect_lat, state->throw_v );
-      }
-
-      v3_muls( localplayer.rb.to_world[1], -doty, Fv );
-      v3_muladds( localplayer.rb.v, Fv, k_mmcollect_vert, localplayer.rb.v );
-      v3_muladds( state->throw_v, Fv, k_mmcollect_vert, state->throw_v );
-   }
-
-   /* Decay */
-   if( v3_length2( state->throw_v ) > 0.0001f ){
-      v3f dir;
-      v3_copy( state->throw_v, dir );
-      v3_normalize( dir );
-
-      float max = v3_dot( dir, state->throw_v ),
-            amt = vg_minf( k_mmdecay * vg.time_fixed_delta, max );
-      v3_muladds( state->throw_v, dir, -amt, state->throw_v );
-   }
-}
-
-static void skate_apply_cog_model(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   v3f ideal_cog, ideal_diff, ideal_dir;
-   v3_copy( state->up_dir, ideal_dir );
-   v3_normalize( ideal_dir );
-
-   float grab = axis_state( k_sraxis_grab );
-   v3_muladds( localplayer.rb.co, ideal_dir, 1.0f-grab, ideal_cog );
-   v3_sub( ideal_cog, state->cog, ideal_diff );
-
-   /* Apply velocities */
-   v3f rv;
-   v3_sub( localplayer.rb.v, state->cog_v, rv );
-
-   v3f F;
-   v3_muls( ideal_diff, -k_cog_spring * 60.0f, F );
-   v3_muladds( F, rv,   -k_cog_damp * 60.0f, F );
-
-   float ra = k_cog_mass_ratio,
-         rb = 1.0f-k_cog_mass_ratio;
-
-   /* Apply forces & intergrate */
-   v3_muladds( state->cog_v, F, -rb, state->cog_v );
-   state->cog_v[1] += -9.8f * vg.time_fixed_delta;
-   v3_muladds( state->cog, state->cog_v, vg.time_fixed_delta, state->cog );
-}
-
-static void skate_integrate(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   float rate_x = 1.0f - (vg.time_fixed_delta * 3.0f),
-         rate_z = rate_x,
-         rate_y = 1.0f;
-
-   if( state->activity >= k_skate_activity_grind_any ){
-      rate_x = 1.0f-(16.0f*vg.time_fixed_delta);
-      rate_y = 1.0f-(10.0f*vg.time_fixed_delta);
-      rate_z = 1.0f-(40.0f*vg.time_fixed_delta);
-   }
-
-   float wx = v3_dot( localplayer.rb.w, localplayer.rb.to_world[0] ) * rate_x,
-         wy = v3_dot( localplayer.rb.w, localplayer.rb.to_world[1] ) * rate_y,
-         wz = v3_dot( localplayer.rb.w, localplayer.rb.to_world[2] ) * rate_z;
-
-   v3_muls(                  localplayer.rb.to_world[0], wx, localplayer.rb.w );
-   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[1], wy, 
-               localplayer.rb.w );
-   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[2], wz, 
-               localplayer.rb.w );
-
-   state->flip_time += state->flip_rate * vg.time_fixed_delta;
-   rb_update_matrices( &localplayer.rb );
-}
-
-static enum trick_type player_skate_trick_input(void){
-   return (button_press( k_srbind_trick0 )     ) |
-          (button_press( k_srbind_trick1 ) << 1) |
-          (button_press( k_srbind_trick2 ) << 1) |
-          (button_press( k_srbind_trick2 )     );
-}
-
-void player__skate_pre_update(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( state->activity == k_skate_activity_handplant ){
-      state->handplant_t += vg.time_delta;
-      mdl_keyframe hpose[32];
-
-      struct skeleton_anim *anim = player_skate.anim_handplant;
-
-      int end = !skeleton_sample_anim_clamped( 
-               &localplayer.skeleton, anim,
-               state->handplant_t, hpose );
-
-      if( state->reverse < 0.0f )
-         player_mirror_pose( hpose, hpose );
-
-      mdl_keyframe *kf_world = &hpose[ localplayer.id_world -1 ];
-      m4x3f world, mmdl, world_view;
-      q_m3x3( kf_world->q, world );
-      v3_copy( kf_world->co, world[3] );
-
-      /* original mtx */
-      q_m3x3( state->store_q, mmdl );
-      v3_copy( state->store_co, mmdl[3] );
-      m4x3_mul( mmdl, world, world_view );
-
-      vg_line_arrow( world_view[3], world_view[0], 1.0f, 0xff0000ff );
-      vg_line_arrow( world_view[3], world_view[1], 1.0f, 0xff00ff00 );
-      vg_line_arrow( world_view[3], world_view[2], 1.0f, 0xffff0000 );
-
-      m4x3f invworld;
-      m4x3_invert_affine( world, invworld );
-      m4x3_mul( mmdl, invworld, world_view );
-
-      v3_copy( world_view[3], localplayer.rb.co );
-      m3x3_q( world_view, localplayer.rb.q );
-
-      /* new * old^-1 = transfer function */
-      m4x3f transfer;
-      m4x3_invert_affine( mmdl, transfer );
-      m4x3_mul( world_view, transfer, transfer );
-
-      m3x3_mulv( transfer, state->air_init_v, localplayer.rb.v );
-      m3x3_mulv( transfer, state->store_cog_v, state->cog_v );
-
-      m4x3_mulv( transfer, state->store_cog, state->cog );
-      v3_muladds( state->cog, localplayer.rb.to_world[1], 
-                  -state->handplant_t*0.5f, state->cog );
-
-      v4f qtransfer;
-      m3x3_q( transfer, qtransfer );
-      q_mul( qtransfer, state->store_smoothed, state->smoothed_rotation );
-      q_normalize( state->smoothed_rotation );
-      rb_update_matrices( &localplayer.rb );
-
-      if( end ){
-         state->activity = k_skate_activity_air;
-      }
-      else return;
-   }
-
-   if( button_down(k_srbind_use) && (v3_length2(state->trick_vel) < 0.01f) ){
-      localplayer.subsystem = k_player_subsystem_walk;
-
-      if( (state->activity <= k_skate_activity_air_to_grind) &&
-           localplayer.have_glider ){
-         player_glide_transition();
-         return;
-      }
-
-      v3f angles;
-      v3_copy( localplayer.cam.angles, localplayer.angles );
-      localplayer.angles[2] = 0.0f;
-
-      v3f newpos, offset;
-      m4x3_mulv( localplayer.rb.to_world, (v3f){0.0f,1.0f,0.0f}, newpos );
-      v3_add( newpos, (v3f){0.0f,-1.0f,0.0f}, newpos );
-      v3_sub( localplayer.rb.co, newpos, offset );
-      v3_copy( newpos, localplayer.rb.co );
-      v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], -0.1f,
-                  localplayer.rb.co );
-
-      player__begin_holdout( offset );
-      player__walk_transition( state->activity <= k_skate_activity_air_to_grind?
-                               0: 1, state->trick_euler[0] );
-
-      return;
-   }
-
-   enum trick_type trick = player_skate_trick_input();
-   if( trick )
-      state->trick_input_collect += vg.time_frame_delta;
-   else 
-      state->trick_input_collect = 0.0f;
-
-   if( state->activity <= k_skate_activity_air_to_grind ){
-      if( trick && (state->trick_input_collect < 0.1f) ){
-         if( state->trick_time == 0.0f ){
-            audio_lock();
-            audio_oneshot_3d( &audio_flips[vg_randu32(&vg.rand)%4], 
-                  localplayer.rb.co, 40.0f, 1.0f );
-            audio_unlock();
-         }
-
-         if( state->trick_time < 0.1f ){
-            v3_zero( state->trick_vel );
-
-            if( trick == k_trick_type_kickflip ){
-               state->trick_vel[0] = 3.0f;
-            }
-            else if( trick == k_trick_type_shuvit ){
-               state->trick_vel[2] = 3.0f;
-            }
-            else if( trick == k_trick_type_treflip ){
-               state->trick_vel[0] = 2.0f;
-               state->trick_vel[2] = 2.0f;
-            }
-            state->trick_type = trick;
-         }
-      }
-   }
-   else
-      state->trick_type = k_trick_type_none;
-}
-
-void player__skate_comp_audio( void *_animator ){
-   struct player_skate_animator *animator = _animator;
-   audio_lock();
-
-   f32 air   = ((animator->activity <= k_skate_activity_air_to_grind) ||
-                (animator->activity == k_skate_activity_handplant))? 1.0f: 0.0f,
-       speed = v3_length( animator->root_v ),
-       attn  = vg_minf( 1.0f, speed*0.1f ),
-       slide = animator->slide;
-
-   if( animator->activity >= k_skate_activity_grind_any )
-      slide = 0.0f;
-
-   f32 gate = skaterift.time_rate;
-
-   if( skaterift.activity == k_skaterift_replay ){
-      gate = vg_minf( 1.0f, fabsf(player_replay.track_velocity) );
-   }
-
-   f32
-       vol_main    = sqrtf( (1.0f-air)*attn*(1.0f-slide) * 0.4f ) * gate,
-       vol_air     = sqrtf(       air *attn * 0.5f )              * gate,
-       vol_slide   = sqrtf( (1.0f-air)*attn*slide * 0.25f )       * gate;
-
-   const u32 flags = AUDIO_FLAG_SPACIAL_3D|AUDIO_FLAG_LOOP;
-
-   if( !player_skate.aud_air ){
-      player_skate.aud_air = audio_get_first_idle_channel();
-      if( player_skate.aud_air )
-         audio_channel_init( player_skate.aud_air, &audio_board[1], flags );
-   }
-
-   if( !player_skate.aud_slide ){
-      player_skate.aud_slide = audio_get_first_idle_channel();
-      if( player_skate.aud_slide ) 
-         audio_channel_init( player_skate.aud_slide, &audio_board[2], flags );
-   }
-
-
-   /* brrrrrrrrrrrt sound for tiles and stuff 
-    * --------------------------------------------------------*/
-   float sidechain_amt = 0.0f,
-         hz            = vg_maxf( speed * 2.0f, 2.0f );
-
-   if( (animator->surface == k_surface_prop_tiles) &&
-       (animator->activity < k_skate_activity_grind_any) )
-      sidechain_amt = 1.0f;
-   else
-      sidechain_amt = 0.0f;
-
-   audio_set_lfo_frequency( 0, hz );
-   audio_set_lfo_wave( 0, k_lfo_polynomial_bipolar, 
-                          vg_lerpf( 250.0f, 80.0f, attn ) );
-
-   if( player_skate.sample_change_cooldown > 0.0f ){
-      player_skate.sample_change_cooldown -= vg.time_frame_delta;
-   }
-   else{
-      int sample_type = k_skate_sample_concrete;
-
-      if( animator->activity == k_skate_activity_grind_5050 ){
-         if( animator->surface == k_surface_prop_metal )
-            sample_type = k_skate_sample_metal_scrape_generic;
-         else
-            sample_type = k_skate_sample_concrete_scrape_metal;
-      }
-      else if( (animator->activity == k_skate_activity_grind_back50) ||
-               (animator->activity == k_skate_activity_grind_front50) )
-      {
-         if( animator->surface == k_surface_prop_metal ){
-            sample_type = k_skate_sample_metal_scrape_generic;
-         }
-         else{
-#if 0
-            float a = v3_dot( localplayer.rb.to_world[2], 
-                              player_skate.grind_dir );
-            if( fabsf(a) > 0.70710678118654752f )
-               sample_type = k_skate_sample_concrete_scrape_wood;
-            else 
-               sample_type = k_skate_sample_concrete_scrape_metal;
-#endif
-
-            sample_type = k_skate_sample_concrete_scrape_wood;
-         }
-      }
-      else if( animator->activity == k_skate_activity_grind_boardslide ){
-         if( animator->surface == k_surface_prop_metal )
-            sample_type = k_skate_sample_metal_scrape_generic;
-         else
-            sample_type = k_skate_sample_concrete_scrape_wood;
-      }
-
-      audio_clip *relevant_samples[] = {
-         &audio_board[0],
-         &audio_board[0],
-         &audio_board[7],
-         &audio_board[6],
-         &audio_board[5]
-      };
-
-      if( (player_skate.main_sample_type != sample_type) || 
-          (!player_skate.aud_main) ){
-
-         player_skate.aud_main = 
-            audio_channel_crossfade( player_skate.aud_main, 
-                                     relevant_samples[sample_type],
-                                     0.06f, flags );
-         player_skate.sample_change_cooldown = 0.1f;
-         player_skate.main_sample_type = sample_type;
-      }
-   }
-
-   if( player_skate.aud_main ){
-      player_skate.aud_main->colour = 0x00103efe;
-      audio_channel_set_spacial( player_skate.aud_main, 
-                                 animator->root_co, 40.0f );
-      //audio_channel_slope_volume( player_skate.aud_main, 0.05f, vol_main );
-      audio_channel_edit_volume( player_skate.aud_main, vol_main, 1 );
-      audio_channel_sidechain_lfo( player_skate.aud_main, 0, sidechain_amt );
-
-      float rate = 1.0f + (attn-0.5f)*0.2f;
-      audio_channel_set_sampling_rate( player_skate.aud_main, rate );
-   }
-
-   if( player_skate.aud_slide ){
-      player_skate.aud_slide->colour = 0x00103efe;
-      audio_channel_set_spacial( player_skate.aud_slide, 
-                                 animator->root_co, 40.0f );
-      //audio_channel_slope_volume( player_skate.aud_slide, 0.05f, vol_slide );
-      audio_channel_edit_volume( player_skate.aud_slide, vol_slide, 1 );
-      audio_channel_sidechain_lfo( player_skate.aud_slide, 0, sidechain_amt );
-   }
-
-   if( player_skate.aud_air ){
-      player_skate.aud_air->colour = 0x00103efe;
-      audio_channel_set_spacial( player_skate.aud_air, 
-                                 animator->root_co, 40.0f );
-      //audio_channel_slope_volume( player_skate.aud_air, 0.05f, vol_air );
-      audio_channel_edit_volume( player_skate.aud_air, vol_air, 1 );
-   }
-
-   audio_unlock();
-}
-
-void player__skate_post_update(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   for( int i=0; i<player_skate.possible_jump_count; i++ ){
-      jump_info *jump = &player_skate.possible_jumps[i];
-
-      if( jump->log_length == 0 ){
-         vg_fatal_error( "assert: jump->log_length == 0\n" );
-      }
-      
-      for( int j=0; j<jump->log_length - 1; j ++ ){
-         float brightness = jump->score*jump->score*jump->score;
-         v3f p1;
-         v3_lerp( jump->log[j], jump->log[j+1], brightness, p1 );
-         vg_line( jump->log[j], p1, jump->colour );
-      }
-
-      vg_line_cross( jump->log[jump->log_length-1], jump->colour, 0.25f );
-
-      v3f p1;
-      v3_add( jump->log[jump->log_length-1], jump->n, p1 );
-      vg_line( jump->log[jump->log_length-1], p1, 0xffffffff );
-
-      vg_line_point( jump->apex, 0.02f, 0xffffffff );
-   }
-}
-
-/*
- * truck alignment model at ra(local)
- * returns 1 if valid surface:
- *             surface_normal will be filled out with an averaged normal vector
- *             axel_dir will be the direction from left to right wheels
- *
- * returns 0 if no good surface found
- */
-static 
-int skate_compute_surface_alignment( v3f ra, u32 colour,
-                                     v3f surface_normal, v3f axel_dir ){
-   world_instance *world = world_current_instance();
-
-   v3f truck, left, right;
-   m4x3_mulv( localplayer.rb.to_world, ra, truck );
-
-   v3_muladds( truck, localplayer.rb.to_world[0], -k_board_width, left  );
-   v3_muladds( truck, localplayer.rb.to_world[0],  k_board_width, right );
-   vg_line( left, right, colour );
-
-   float k_max_truck_flex = VG_PIf * 0.25f;
-   
-   ray_hit ray_l, ray_r;
-
-   v3f dir;
-   v3_muls( localplayer.rb.to_world[1], -1.0f, dir );
-
-   int res_l = 0, res_r = 0;
-
-   for( int i=0; i<8; i++ ){
-      float t = 1.0f - (float)i * (1.0f/8.0f);
-      v3_muladds( truck, localplayer.rb.to_world[0], -k_board_radius*t, left );
-      v3_muladds( left,  localplayer.rb.to_world[1],  k_board_radius,   left );
-      ray_l.dist = 2.1f * k_board_radius;
-
-      res_l = ray_world( world, left, dir, &ray_l, k_material_flag_walking );
-
-      if( res_l )
-         break;
-   }
-
-   for( int i=0; i<8; i++ ){
-      float t = 1.0f - (float)i * (1.0f/8.0f);
-      v3_muladds( truck, localplayer.rb.to_world[0],  k_board_radius*t, right );
-      v3_muladds( right, localplayer.rb.to_world[1],  k_board_radius,   right );
-      ray_r.dist = 2.1f * k_board_radius;
-
-      res_r = ray_world( world, right, dir, &ray_r, k_material_flag_walking );
-
-      if( res_r )
-         break;
-   }
-
-   v3f v0;
-   v3f midpoint;
-   v3f tangent_average;
-   v3_muladds( truck, localplayer.rb.to_world[1], -k_board_radius, midpoint );
-   v3_zero( tangent_average );
-
-   if( res_l || res_r ){
-      v3f p0, p1, t;
-      v3_copy( midpoint, p0 );
-      v3_copy( midpoint, p1 );
-
-      if( res_l ){
-         v3_copy( ray_l.pos, p0 );
-         v3_cross( ray_l.normal, localplayer.rb.to_world[0], t );
-         v3_add( t, tangent_average, tangent_average );
-      }
-      if( res_r ){
-         v3_copy( ray_r.pos, p1 );
-         v3_cross( ray_r.normal, localplayer.rb.to_world[0], t );
-         v3_add( t, tangent_average, tangent_average );
-      }
-
-      v3_sub( p1, p0, v0 );
-      v3_normalize( v0 );
-   }
-   else{
-      /* fallback: use the closes point to the trucks */
-      v3f closest;
-      int idx = bh_closest_point( world->geo_bh, midpoint, closest, 0.1f );
-
-      if( idx != -1 ){
-         u32 *tri = &world->scene_geo.arrindices[ idx * 3 ];
-         v3f verts[3];
-
-         for( int j=0; j<3; j++ )
-            v3_copy( world->scene_geo.arrvertices[ tri[j] ].co, verts[j] );
-
-         v3f vert0, vert1, n;
-         v3_sub( verts[1], verts[0], vert0 );
-         v3_sub( verts[2], verts[0], vert1 );
-         v3_cross( vert0, vert1, n );
-         v3_normalize( n );
-
-         if( v3_dot( n, localplayer.rb.to_world[1] ) < 0.3f )
-            return 0;
-
-         v3_cross( n, localplayer.rb.to_world[2], v0 );
-         v3_muladds( v0, localplayer.rb.to_world[2],
-                     -v3_dot( localplayer.rb.to_world[2], v0 ), v0 );
-         v3_normalize( v0 );
-
-         v3f t;
-         v3_cross( n, localplayer.rb.to_world[0], t );
-         v3_add( t, tangent_average, tangent_average );
-      }
-      else
-         return 0;
-   }
-
-   v3_muladds( truck, v0,  k_board_width, right );
-   v3_muladds( truck, v0, -k_board_width, left );
-
-   vg_line( left, right, VG__WHITE );
-
-   v3_normalize( tangent_average );
-   v3_cross( v0, tangent_average, surface_normal );
-   v3_copy( v0, axel_dir );
-
-   return 1;
-}
-
-static void skate_weight_distribute(void){
-   struct player_skate_state *state = &player_skate.state;
-   v3_zero( player_skate.weight_distribution );
-
-   int reverse_dir = v3_dot( localplayer.rb.to_world[2], 
-                             localplayer.rb.v ) < 0.0f?1:-1;
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   if( state->manual_direction == 0 ){
-      if( (steer[1] > 0.7f) && (state->activity == k_skate_activity_ground) &&
-          (state->jump_charge <= 0.01f) )
-         state->manual_direction = reverse_dir;
-   }
-   else{
-      if( steer[1] < 0.1f ){
-         state->manual_direction = 0;
-      }
-      else{
-         if( reverse_dir != state->manual_direction ){
-            return;
-         }
-      }
-   }
-
-   if( state->manual_direction ){
-      float amt = vg_minf( steer[1] * 8.0f, 1.0f );
-      player_skate.weight_distribution[2] = k_board_length * amt * 
-                                          (float)state->manual_direction;
-   }
-
-   if( state->manual_direction ){
-      v3f plane_z;
-
-      m3x3_mulv( localplayer.rb.to_world, player_skate.weight_distribution, 
-                 plane_z );
-      v3_negate( plane_z, plane_z );
-
-      v3_muladds( plane_z, player_skate.surface_picture,
-                  -v3_dot( plane_z, player_skate.surface_picture ), plane_z );
-      v3_normalize( plane_z );
-
-      v3_muladds( plane_z, player_skate.surface_picture, 0.3f, plane_z );
-      v3_normalize( plane_z );
-
-      v3f p1;
-      v3_muladds( localplayer.rb.co, plane_z, 1.5f, p1 );
-      vg_line( localplayer.rb.co, p1, VG__GREEN );
-
-      v3f refdir;
-      v3_muls( localplayer.rb.to_world[2], -(float)state->manual_direction,
-               refdir );
-
-      rb_effect_spring_target_vector( &localplayer.rb, refdir, plane_z,
-                                       k_manul_spring, k_manul_dampener,
-                                       player_skate.substep_delta );
-   }
-}
-
-static void skate_adjust_up_direction(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( state->activity == k_skate_activity_ground ){
-      v3f target;
-      v3_copy( player_skate.surface_picture, target );
-
-      target[1] += 2.0f * player_skate.surface_picture[1];
-      v3_normalize( target );
-
-      v3_lerp( state->up_dir, target,
-               8.0f * player_skate.substep_delta, state->up_dir );
-   }
-   else if( state->activity <= k_skate_activity_air_to_grind ){
-      v3_lerp( state->up_dir, localplayer.rb.to_world[1],
-               8.0f * player_skate.substep_delta, state->up_dir );
-   }
-   else{
-      v3f avg;
-      v3_add( localplayer.rb.to_world[1], (v3f){0,1,0}, avg );
-      v3_normalize( avg );
-
-      v3_lerp( state->up_dir, avg,
-               6.0f * player_skate.substep_delta, state->up_dir );
-   }
-}
-
-static int skate_point_visible( v3f origin, v3f target ){
-   v3f dir;
-   v3_sub( target, origin, dir );
-   
-   ray_hit ray;
-   ray.dist = v3_length( dir );
-   v3_muls( dir, 1.0f/ray.dist, dir );
-   ray.dist -= 0.025f;
-
-   if( ray_world( world_current_instance(), origin, dir, &ray, 
-                  k_material_flag_walking ) )
-      return 0;
-
-   return 1;
-}
-
-static void skate_grind_orient( struct grind_info *inf, m3x3f mtx ){
-   v3_copy( inf->dir, mtx[0] );
-   v3_copy( inf->n, mtx[1] );
-   v3_cross( mtx[0], mtx[1], mtx[2] );
-}
-
-static void skate_grind_friction( struct grind_info *inf, float strength ){
-   v3f v2;
-   v3_muladds( localplayer.rb.to_world[2], inf->n, 
-               -v3_dot( localplayer.rb.to_world[2], inf->n ), v2 );
-
-   float a        = 1.0f-fabsf( v3_dot( v2, inf->dir ) ),
-         dir      = vg_signf( v3_dot( localplayer.rb.v, inf->dir ) ),
-         F        = a * -dir * k_grind_max_friction;
-
-   v3_muladds( localplayer.rb.v, inf->dir, F*vg.time_fixed_delta*strength, 
-               localplayer.rb.v );
-}
-
-static void skate_grind_decay( struct grind_info *inf, float strength ){
-   m3x3f mtx, mtx_inv;
-   skate_grind_orient( inf, mtx );
-   m3x3_transpose( mtx, mtx_inv );
-
-   v3f v_grind;
-   m3x3_mulv( mtx_inv, localplayer.rb.v, v_grind );
-
-   float decay = 1.0f - ( vg.time_fixed_delta * k_grind_decayxy * strength );
-   v3_mul( v_grind, (v3f){ 1.0f, decay, decay }, v_grind );
-   m3x3_mulv( mtx, v_grind, localplayer.rb.v );
-}
-
-static void skate_grind_truck_apply( float sign, struct grind_info *inf,
-                                        float strength ){
-   struct player_skate_state *state = &player_skate.state;
-   /* REFACTOR */
-   v3f ra = { 0.0f, -k_board_radius, sign * k_board_length };
-   v3f raw, wsp;
-   m3x3_mulv( localplayer.rb.to_world, ra, raw );
-   v3_add( localplayer.rb.co, raw, wsp );
-
-   v3_copy( ra, player_skate.weight_distribution );
-
-   v3f delta;
-   v3_sub( inf->co, wsp, delta );
-
-   /* spring force */
-   v3_muladds( localplayer.rb.v, delta, k_spring_force*strength*vg.time_fixed_delta, 
-               localplayer.rb.v );
-
-   skate_grind_decay( inf, strength );
-   skate_grind_friction( inf, strength );
-
-   /* yeah yeah yeah yeah */
-   v3f raw_nplane, axis;
-   v3_muladds( raw, inf->n, -v3_dot( inf->n, raw ), raw_nplane );
-   v3_cross( raw_nplane, inf->n, axis );
-   v3_normalize( axis );
-
-   /* orientation */
-   m3x3f mtx;
-   skate_grind_orient( inf, mtx );
-   v3f target_fwd, fwd, up, target_up;
-   m3x3_mulv( mtx, player_skate.grind_vec, target_fwd );
-   v3_copy( raw_nplane, fwd );
-   v3_copy( localplayer.rb.to_world[1], up );
-   v3_copy( inf->n, target_up );
-
-   v3_muladds( target_fwd, inf->n, -v3_dot(inf->n,target_fwd), target_fwd );
-   v3_muladds( fwd, inf->n, -v3_dot(inf->n,fwd), fwd );
-
-   v3_normalize( target_fwd );
-   v3_normalize( fwd );
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   float way = steer[1] * vg_signf( v3_dot( raw_nplane, localplayer.rb.v ) );
-
-   v4f q;
-   q_axis_angle( q, axis, VG_PIf*0.125f * way );
-   q_mulv( q, target_up,  target_up );
-   q_mulv( q, target_fwd, target_fwd );
-
-   rb_effect_spring_target_vector( &localplayer.rb, up, target_up,
-                                    k_grind_spring, 
-                                    k_grind_dampener,
-                                    vg.time_fixed_delta );
-
-   rb_effect_spring_target_vector( &localplayer.rb, fwd, target_fwd,
-                                    k_grind_spring*strength, 
-                                    k_grind_dampener*strength,
-                                    vg.time_fixed_delta );
-
-   vg_line_arrow( localplayer.rb.co, target_up, 1.0f, VG__GREEN );
-   vg_line_arrow( localplayer.rb.co, fwd, 0.8f, VG__RED );
-   vg_line_arrow( localplayer.rb.co, target_fwd, 1.0f, VG__YELOW );
-
-   player_skate.grind_strength = strength;
-
-   /* Fake contact */
-   struct grind_limit *limit = 
-      &player_skate.limits[ player_skate.limit_count ++ ];
-   m4x3_mulv( localplayer.rb.to_local, wsp, limit->ra );
-   m3x3_mulv( localplayer.rb.to_local, inf->n, limit->n );
-   limit->p = 0.0f;
-
-   v3_copy( inf->dir, player_skate.grind_dir );
-}
-
-static void skate_5050_apply( struct grind_info *inf_front,
-                                 struct grind_info *inf_back ){
-   struct player_skate_state *state = &player_skate.state;
-   struct grind_info inf_avg;
-
-   v3_sub( inf_front->co, inf_back->co, inf_avg.dir );
-   v3_muladds( inf_back->co, inf_avg.dir, 0.5f, inf_avg.co );
-   v3_normalize( inf_avg.dir );
-
-   /* dont ask */
-   v3_muls( inf_avg.dir, vg_signf(v3_dot(inf_avg.dir,localplayer.rb.v)), 
-            inf_avg.dir );
-
-   v3f axis_front, axis_back, axis;
-   v3_cross( inf_front->dir, inf_front->n, axis_front );
-   v3_cross( inf_back->dir,  inf_back->n,  axis_back  );
-   v3_add( axis_front, axis_back, axis );
-   v3_normalize( axis );
-
-   v3_cross( axis, inf_avg.dir, inf_avg.n );
-   skate_grind_decay( &inf_avg, 1.0f );
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   float way = steer[1] * vg_signf( v3_dot( localplayer.rb.to_world[2], 
-                                            localplayer.rb.v ) );
-   v4f q;
-   v3f up, target_up;
-   v3_copy( localplayer.rb.to_world[1], up );
-   v3_copy( inf_avg.n, target_up );
-   q_axis_angle( q, localplayer.rb.to_world[0], VG_PIf*0.25f * -way );
-   q_mulv( q, target_up,  target_up );
-
-   v3_zero( player_skate.weight_distribution );
-   player_skate.weight_distribution[2] = k_board_length * -way;
-
-   rb_effect_spring_target_vector( &localplayer.rb, up, target_up,
-                                    k_grind_spring, 
-                                    k_grind_dampener,
-                                    vg.time_fixed_delta );
-   vg_line_arrow( localplayer.rb.co, up, 1.0f, VG__GREEN );
-   vg_line_arrow( localplayer.rb.co, target_up, 1.0f, VG__GREEN );
-
-   v3f fwd_nplane, dir_nplane;
-   v3_muladds( localplayer.rb.to_world[2], inf_avg.n,
-               -v3_dot( localplayer.rb.to_world[2], inf_avg.n ), fwd_nplane );
-
-   v3f dir;
-   v3_muls( inf_avg.dir, v3_dot( fwd_nplane, inf_avg.dir ), dir );
-   v3_muladds( dir, inf_avg.n, -v3_dot( dir, inf_avg.n ), dir_nplane );
-
-   v3_normalize( fwd_nplane );
-   v3_normalize( dir_nplane );
-
-   rb_effect_spring_target_vector( &localplayer.rb, fwd_nplane, dir_nplane,
-                                    1000.0f,
-                                    k_grind_dampener,
-                                    vg.time_fixed_delta );
-   vg_line_arrow( localplayer.rb.co, fwd_nplane, 0.8f, VG__RED );
-   vg_line_arrow( localplayer.rb.co, dir_nplane, 0.8f, VG__RED );
-
-   v3f pos_front = { 0.0f, -k_board_radius, -1.0f * k_board_length },
-       pos_back  = { 0.0f, -k_board_radius,  1.0f * k_board_length },
-       delta_front, delta_back, delta_total;
-
-   m4x3_mulv( localplayer.rb.to_world, pos_front, pos_front );
-   m4x3_mulv( localplayer.rb.to_world, pos_back,  pos_back  );
-
-   v3_sub( inf_front->co, pos_front, delta_front );
-   v3_sub( inf_back->co,  pos_back, delta_back );
-   v3_add( delta_front, delta_back, delta_total );
-
-   v3_muladds( localplayer.rb.v, delta_total, 50.0f * vg.time_fixed_delta, 
-               localplayer.rb.v );
-
-   /* Fake contact */
-   struct grind_limit *limit = 
-      &player_skate.limits[ player_skate.limit_count ++ ];
-   v3_zero( limit->ra );
-   m3x3_mulv( localplayer.rb.to_local, inf_avg.n, limit->n );
-   limit->p = 0.0f;
-
-   v3_copy( inf_avg.dir, player_skate.grind_dir );
-}
-
-static int skate_grind_truck_renew( f32 sign, struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   v3f wheel_co = { 0.0f, 0.0f,            sign * k_board_length },
-       grind_co = { 0.0f, -k_board_radius, sign * k_board_length };
-
-   m4x3_mulv( localplayer.rb.to_world, wheel_co, wheel_co );
-   m4x3_mulv( localplayer.rb.to_world, grind_co, grind_co );
-
-   /* Exit condition: lost grind tracking */
-   if( !skate_grind_scansq( grind_co, localplayer.rb.v, 0.3f, inf ) )
-      return 0;
-
-   /* Exit condition: cant see grind target directly */
-   if( !skate_point_visible( wheel_co, inf->co ) )
-      return 0;
-
-   /* Exit condition: minimum velocity not reached, but allow a bit of error */
-   float dv   = fabsf(v3_dot( localplayer.rb.v, inf->dir )),
-         minv = k_grind_axel_min_vel*0.8f;
-
-   if( dv < minv )
-      return 0;
-
-   if( fabsf(v3_dot( inf->dir, player_skate.grind_dir )) < k_grind_max_edge_angle )
-      return 0;
-
-   v3_copy( inf->dir, player_skate.grind_dir );
-   return 1;
-}
-
-static int skate_grind_truck_entry( f32 sign, struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   /* REFACTOR */
-   v3f ra = { 0.0f, -k_board_radius, sign * k_board_length };
-
-   v3f raw, wsp;
-   m3x3_mulv( localplayer.rb.to_world, ra, raw );
-   v3_add( localplayer.rb.co, raw, wsp );
-
-   if( skate_grind_scansq( wsp, localplayer.rb.v, 0.3, inf ) ){
-      if( fabsf(v3_dot( localplayer.rb.v, inf->dir )) < k_grind_axel_min_vel )
-         return 0;
-
-      /* velocity should be at least 60% aligned */
-      v3f pv, axis;
-      v3_cross( inf->n, inf->dir, axis );
-      v3_muladds( localplayer.rb.v, inf->n, 
-                  -v3_dot( localplayer.rb.v, inf->n ), pv );
-      
-      if( v3_length2( pv ) < 0.0001f )
-         return 0;
-      v3_normalize( pv );
-
-      if( fabsf(v3_dot( pv, inf->dir )) < k_grind_axel_max_angle )
-         return 0;
-
-      if( v3_dot( localplayer.rb.v, inf->n ) > 0.5f )
-         return 0;
-      
-      v3f local_co, local_dir, local_n;
-      m4x3_mulv( localplayer.rb.to_local, inf->co,  local_co );
-      m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
-      m3x3_mulv( localplayer.rb.to_local, inf->n,   local_n );
-
-      v2f delta = { local_co[0], local_co[2] - k_board_length*sign };
-
-      float truck_height = -(k_board_radius+0.03f);
-
-      v3f rv;
-      v3_cross( localplayer.rb.w, raw, rv );
-      v3_add( localplayer.rb.v, rv, rv );
-
-      if( (local_co[1] >= truck_height) &&
-          (v2_length2( delta ) <= k_board_radius*k_board_radius) )
-      {
-         return 1;
-      }
-   }
-
-   return 0;
-}
-
-static void skate_boardslide_apply( struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   v3f local_co, local_dir, local_n;
-   m4x3_mulv( localplayer.rb.to_local, inf->co, local_co );
-   m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
-   m3x3_mulv( localplayer.rb.to_local, inf->n, local_n );
-
-   v3f intersection;
-   v3_muladds( local_co, local_dir, local_co[0]/-local_dir[0], 
-               intersection );
-   v3_copy( intersection, player_skate.weight_distribution );
-
-   skate_grind_decay( inf, 0.0125f );
-   skate_grind_friction( inf, 0.25f );
-
-   /* direction alignment */
-   v3f dir, perp;
-   v3_cross( local_dir, local_n, perp );
-   v3_muls( local_dir, vg_signf(local_dir[0]), dir );
-   v3_muls( perp, vg_signf(perp[2]), perp );
-
-   m3x3_mulv( localplayer.rb.to_world, dir, dir );
-   m3x3_mulv( localplayer.rb.to_world, perp, perp );
-
-   v4f qbalance;
-   q_axis_angle( qbalance, dir, local_co[0]*k_grind_balance );
-   q_mulv( qbalance, perp, perp );
-
-   rb_effect_spring_target_vector( &localplayer.rb, localplayer.rb.to_world[0],
-                                    dir, 
-                                    k_grind_spring, k_grind_dampener,
-                                    vg.time_fixed_delta );
-
-   rb_effect_spring_target_vector( &localplayer.rb, localplayer.rb.to_world[2],
-                                    perp,
-                                    k_grind_spring, k_grind_dampener,
-                                    vg.time_fixed_delta );
-
-   vg_line_arrow( localplayer.rb.co, dir, 0.5f, VG__GREEN );
-   vg_line_arrow( localplayer.rb.co, perp, 0.5f, VG__BLUE );
-
-   v3_copy( inf->dir, player_skate.grind_dir );
-}
-
-static int skate_boardslide_entry( struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( skate_grind_scansq( localplayer.rb.co, 
-                           localplayer.rb.to_world[0], k_board_length,
-                           inf ) )
-   {
-      v3f local_co, local_dir;
-      m4x3_mulv( localplayer.rb.to_local, inf->co, local_co );
-      m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
-
-      if( (fabsf(local_co[2]) <= k_board_length) &&   /* within wood area */
-          (local_co[1] >= 0.0f) &&                    /* at deck level */
-          (fabsf(local_dir[0]) >= 0.25f) )            /* perpendicular to us */
-      {
-         if( fabsf(v3_dot( localplayer.rb.v, inf->dir )) < k_grind_axel_min_vel )
-            return 0;
-
-         return 1;
-      }
-   }
-
-   return 0;
-}
-
-static int skate_boardslide_renew( struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( !skate_grind_scansq( localplayer.rb.co, 
-                            localplayer.rb.to_world[0], k_board_length,
-                            inf ) )
-      return 0;
-
-   /* Exit condition: cant see grind target directly */
-   v3f vis;
-   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 0.2f, vis );
-   if( !skate_point_visible( vis, inf->co ) )
-      return 0;
-
-   /* Exit condition: minimum velocity not reached, but allow a bit of error */
-   float dv   = fabsf(v3_dot( localplayer.rb.v, inf->dir )),
-         minv = k_grind_axel_min_vel*0.8f;
-
-   if( dv < minv )
-      return 0;
-
-   if( fabsf(v3_dot( inf->dir, player_skate.grind_dir )) < k_grind_max_edge_angle )
-      return 0;
-
-   return 1;
-}
-
-static void skate_store_grind_vec( struct grind_info *inf ){
-   struct player_skate_state *state = &player_skate.state;
-
-   m3x3f mtx;
-   skate_grind_orient( inf, mtx );
-   m3x3_transpose( mtx, mtx );
-
-   v3f raw;
-   v3_sub( inf->co, localplayer.rb.co, raw );
-
-   m3x3_mulv( mtx, raw, player_skate.grind_vec );
-   v3_normalize( player_skate.grind_vec );
-   v3_copy( inf->dir, player_skate.grind_dir );
-}
-
-static enum skate_activity skate_availible_grind(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   if( state->grind_cooldown > 100 ){
-      vg_fatal_error( "wth!\n" );
-   }
-
-   /* debounces this state manager a little bit */
-   if( state->grind_cooldown ){
-      state->grind_cooldown --;
-      return k_skate_activity_undefined;
-   }
-
-   struct grind_info inf_back50,
-                     inf_front50,
-                     inf_slide;
-
-   int res_back50  = 0,
-       res_front50 = 0,
-       res_slide   = 0;
-
-   int allow_back  = 1,
-       allow_front = 1;
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   if( state->activity == k_skate_activity_grind_5050 || 
-       state->activity == k_skate_activity_grind_back50 ||
-       state->activity == k_skate_activity_grind_front50 )
-   {
-      float tilt = steer[1];
-
-      if( fabsf(tilt) >= 0.25f ){
-         v3f raw = {0.0f,0.0f,tilt};
-         m3x3_mulv( localplayer.rb.to_world, raw, raw );
-
-         float way = tilt * vg_signf( v3_dot( raw, localplayer.rb.v ) );
-
-         if( way < 0.0f ) allow_front = 0;
-         else allow_back = 0;
-      }
-   }
-
-   if( state->activity == k_skate_activity_grind_boardslide ){
-      res_slide = skate_boardslide_renew( &inf_slide );
-   }
-   else if( state->activity == k_skate_activity_grind_back50 ){
-      res_back50  = skate_grind_truck_renew( 1.0f, &inf_back50 );
-
-      if( allow_front )
-         res_front50 = skate_grind_truck_entry( -1.0f, &inf_front50 );
-   }
-   else if( state->activity == k_skate_activity_grind_front50 ){
-      res_front50 = skate_grind_truck_renew( -1.0f, &inf_front50 );
-
-      if( allow_back )
-         res_back50  = skate_grind_truck_entry(  1.0f, &inf_back50 );
-   }
-   else if( state->activity == k_skate_activity_grind_5050 ){
-      if( allow_front )
-         res_front50 = skate_grind_truck_renew( -1.0f, &inf_front50 );
-      if( allow_back )
-         res_back50  = skate_grind_truck_renew(  1.0f, &inf_back50 );
-   }
-   else{
-      res_slide   = skate_boardslide_entry( &inf_slide );
-
-      if( allow_back )
-         res_back50  = skate_grind_truck_entry(  1.0f, &inf_back50 );
-
-      if( allow_front )
-         res_front50 = skate_grind_truck_entry( -1.0f, &inf_front50 );
-
-      if( res_back50 != res_front50 ){
-         int wants_to_do_that = fabsf(steer[1]) >= 0.25f;
-
-         res_back50  &= wants_to_do_that;
-         res_front50 &= wants_to_do_that;
-      }
-   }
-
-   const enum skate_activity table[] =
-   {                                      /* slide | back | front */
-      k_skate_activity_undefined,         /* 0       0      0     */
-      k_skate_activity_grind_front50,     /* 0       0      1     */
-      k_skate_activity_grind_back50,      /* 0       1      0     */
-      k_skate_activity_grind_5050,        /* 0       1      1     */
-
-      /* slide has priority always */
-      k_skate_activity_grind_boardslide,  /* 1       0      0     */
-      k_skate_activity_grind_boardslide,  /* 1       0      1     */
-      k_skate_activity_grind_boardslide,  /* 1       1      0     */
-      k_skate_activity_grind_boardslide,  /* 1       1      1     */
-   }
-   , new_activity = table[ res_slide << 2 | res_back50 << 1 | res_front50 ];
-
-   if(      new_activity == k_skate_activity_undefined ){
-      if( state->activity >= k_skate_activity_grind_any ){
-         state->grind_cooldown = 15;
-         state->surface_cooldown = 10;
-      }
-   }
-   else if( new_activity == k_skate_activity_grind_boardslide ){
-      skate_boardslide_apply( &inf_slide );
-   }
-   else if( new_activity == k_skate_activity_grind_back50 ){
-      if( state->activity != k_skate_activity_grind_back50 )
-         skate_store_grind_vec( &inf_back50 );
-
-      skate_grind_truck_apply(  1.0f, &inf_back50, 1.0f );
-   }
-   else if( new_activity == k_skate_activity_grind_front50 ){
-      if( state->activity != k_skate_activity_grind_front50 )
-         skate_store_grind_vec( &inf_front50 );
-
-      skate_grind_truck_apply( -1.0f, &inf_front50, 1.0f );
-   }
-   else if( new_activity == k_skate_activity_grind_5050 )
-      skate_5050_apply( &inf_front50, &inf_back50 );
-
-   return new_activity;
-}
-
-void player__skate_update(void){
-   struct player_skate_state *state = &player_skate.state;
-   world_instance *world = world_current_instance();
-
-   if( state->activity == k_skate_activity_handplant )
-      return;
-
-   if( !world_water_player_safe( world, 0.25f ) ) return;
-
-   v3_copy( localplayer.rb.co, state->prev_pos );
-   state->activity_prev = state->activity;
-   v3f normal_total;
-   v3_zero( normal_total );
-
-   struct board_collider
-   {
-      v3f   pos;
-      float radius;
-
-      u32   colour;
-
-      enum  board_collider_state
-      {
-         k_collider_state_default,
-         k_collider_state_disabled,
-         k_collider_state_colliding
-      }
-      state;
-   }
-   wheels[] =
-   {
-      { 
-         { 0.0f, 0.0f,    -k_board_length }, 
-         .radius = k_board_radius,
-         .colour = VG__RED
-      },
-      { 
-         { 0.0f, 0.0f,     k_board_length }, 
-         .radius = k_board_radius,
-         .colour = VG__GREEN
-      }
-   };
-
-   float slap = 0.0f;
-
-   if( state->activity <= k_skate_activity_air_to_grind ){
-      float min_dist = 0.6f;
-      for( int i=0; i<2; i++ ){
-         v3f wpos, closest;
-         m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, wpos );
-
-         if( bh_closest_point( world->geo_bh, wpos, closest, min_dist ) != -1 ){
-            min_dist = vg_minf( min_dist, v3_dist( closest, wpos ) );
-         }
-      }
-      min_dist -= 0.2f;
-      float vy = vg_maxf( 0.0f, localplayer.rb.v[1] );
-      slap = vg_clampf( (min_dist/0.5f) + vy, 0.0f, 1.0f )*0.3f;
-   }
-   state->slap = vg_lerpf( state->slap, slap, 10.0f*vg.time_fixed_delta );
-
-   wheels[0].pos[1] = state->slap;
-   wheels[1].pos[1] = state->slap;
-
-
-   const int k_wheel_count = 2;
-
-   player_skate.substep = vg.time_fixed_delta;
-   player_skate.substep_delta = player_skate.substep;
-   player_skate.limit_count = 0;
-
-   int substep_count = 0;
-
-   v3_zero( player_skate.surface_picture );
-
-   int prev_contacts[2];
-
-   for( int i=0; i<k_wheel_count; i++ ){
-      wheels[i].state = k_collider_state_default;
-      prev_contacts[i] = player_skate.wheel_contacts[i];
-   }
-
-   /* check if we can enter or continue grind */
-   enum skate_activity grindable_activity = skate_availible_grind();
-   if( grindable_activity != k_skate_activity_undefined ){
-      state->activity = grindable_activity;
-      goto grinding;
-   }
-
-   int contact_count = 0;
-   for( int i=0; i<2; i++ ){
-      v3f normal, axel;
-      v3_copy( localplayer.rb.to_world[0], axel );
-
-      if( skate_compute_surface_alignment( wheels[i].pos, 
-                                           wheels[i].colour, normal, axel ) )
-      {
-         rb_effect_spring_target_vector( &localplayer.rb, 
-                                          localplayer.rb.to_world[0],
-                                          axel,
-                                          k_surface_spring, k_surface_dampener,
-                                          player_skate.substep_delta );
-
-         v3_add( normal, player_skate.surface_picture, 
-                 player_skate.surface_picture );
-         contact_count ++;
-         player_skate.wheel_contacts[i] = 1;
-      }
-      else{
-         player_skate.wheel_contacts[i] = 0;
-      }
-
-      m3x3_mulv( localplayer.rb.to_local, axel, player_skate.truckv0[i] );
-   }
-
-   if( state->surface_cooldown ){
-      state->surface_cooldown --;
-      contact_count = 0;
-   }
-
-   if( (prev_contacts[0]+prev_contacts[1] == 1) && (contact_count == 2) ){
-      for( int i=0; i<2; i++ ){
-         if( !prev_contacts[i] ){
-            v3f co;
-            m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, co );
-            player__networked_sfx( k_player_subsystem_skate, 32, 
-                                   k_player_skate_soundeffect_tap,
-                                   localplayer.rb.co, 0.75f );
-         }
-      }
-   }
-
-   if( contact_count ){
-      state->activity = k_skate_activity_ground;
-      state->gravity_bias = k_gravity;
-      v3_normalize( player_skate.surface_picture );
-
-      skate_apply_friction_model();
-      skate_weight_distribute();
-   }
-   else{
-      if( state->activity > k_skate_activity_air_to_grind )
-         state->activity = k_skate_activity_air;
-
-      v3_zero( player_skate.weight_distribution );
-      skate_apply_air_model();
-   }
-
-grinding:;
-
-   if( state->activity == k_skate_activity_grind_back50 )
-      wheels[1].state = k_collider_state_disabled;
-   if( state->activity == k_skate_activity_grind_front50 )
-      wheels[0].state = k_collider_state_disabled;
-   if( state->activity == k_skate_activity_grind_5050 ){
-      wheels[0].state = k_collider_state_disabled;
-      wheels[1].state = k_collider_state_disabled;
-   }
-
-   /* all activities */
-   skate_apply_steering_model();
-   skate_adjust_up_direction();
-   skate_apply_cog_model();
-   skate_apply_jump_model();
-   skate_apply_handplant_model();
-   skate_apply_grab_model();
-   skate_apply_trick_model();
-   skate_apply_pump_model();
-
-   ent_tornado_debug();
-   v3f a;
-   ent_tornado_forces( localplayer.rb.co, localplayer.rb.v, a );
-   v3_muladds( localplayer.rb.v, a, vg.time_fixed_delta, localplayer.rb.v );
-
-begin_collision:;
-
-   /*
-    * Phase 0: Continous collision detection
-    * --------------------------------------------------------------------------
-    */
-
-   v3f head_wp0, head_wp1, start_co;
-   m4x3_mulv( localplayer.rb.to_world, state->head_position, head_wp0 );
-   v3_copy( localplayer.rb.co, start_co );
-
-   /* calculate transform one step into future */
-   v3f future_co;
-   v4f future_q;
-   v3_muladds( localplayer.rb.co, localplayer.rb.v, player_skate.substep, 
-               future_co );
-
-   if( v3_length2( localplayer.rb.w ) > 0.0f ){
-      v4f rotation;
-      v3f axis;
-      v3_copy( localplayer.rb.w, axis );
-      
-      float mag = v3_length( axis );
-      v3_divs( axis, mag, axis );
-      q_axis_angle( rotation, axis, mag*player_skate.substep );
-      q_mul( rotation, localplayer.rb.q, future_q );
-      q_normalize( future_q );
-   }
-   else
-      v4_copy( localplayer.rb.q, future_q );
-
-   v3f future_cg, current_cg, cg_offset;
-   q_mulv( localplayer.rb.q, player_skate.weight_distribution, current_cg );
-   q_mulv( future_q, player_skate.weight_distribution, future_cg );
-   v3_sub( future_cg, current_cg, cg_offset );
-
-   /* calculate the minimum time we can move */
-   float max_time = player_skate.substep;
-
-   for( int i=0; i<k_wheel_count; i++ ){
-      if( wheels[i].state == k_collider_state_disabled )
-         continue;
-
-      v3f current, future, r_cg;
-      
-      q_mulv( future_q, wheels[i].pos, future );
-      v3_add( future, future_co, future );
-      v3_add( cg_offset, future, future );
-
-      q_mulv( localplayer.rb.q, wheels[i].pos, current );
-      v3_add( current, localplayer.rb.co, current );
-      
-      float t;
-      v3f n;
-
-      float cast_radius = wheels[i].radius - k_penetration_slop * 2.0f;
-      if( spherecast_world( world, current, future, cast_radius, &t, n,
-                            k_material_flag_walking ) != -1)
-         max_time = vg_minf( max_time, t * player_skate.substep );
-   }
-
-   /* clamp to a fraction of delta, to prevent locking */
-   float rate_lock = substep_count;
-   rate_lock *= vg.time_fixed_delta * 0.1f;
-   rate_lock *= rate_lock;
-
-   max_time = vg_maxf( max_time, rate_lock );
-   player_skate.substep_delta = max_time;
-
-   /* integrate */
-   v3_muladds( localplayer.rb.co, localplayer.rb.v, 
-               player_skate.substep_delta, localplayer.rb.co );
-   if( v3_length2( localplayer.rb.w ) > 0.0f ){
-      v4f rotation;
-      v3f axis;
-      v3_copy( localplayer.rb.w, axis );
-      
-      float mag = v3_length( axis );
-      v3_divs( axis, mag, axis );
-      q_axis_angle( rotation, axis, mag*player_skate.substep_delta );
-      q_mul( rotation, localplayer.rb.q, localplayer.rb.q );
-      q_normalize( localplayer.rb.q );
-
-      q_mulv( localplayer.rb.q, player_skate.weight_distribution, future_cg );
-      v3_sub( current_cg, future_cg, cg_offset );
-      v3_add( localplayer.rb.co, cg_offset, localplayer.rb.co );
-   }
-
-   rb_update_matrices( &localplayer.rb );
-   localplayer.rb.v[1] += -state->gravity_bias * player_skate.substep_delta;
-
-   player_skate.substep -= player_skate.substep_delta;
-
-   rb_ct manifold[128];
-   int manifold_len = 0;
-   /*
-    * Phase -1: head detection
-    * --------------------------------------------------------------------------
-    */
-   m4x3_mulv( localplayer.rb.to_world, state->head_position, head_wp1 );
-
-   float t;
-   v3f n;
-   if( (v3_dist2( head_wp0, head_wp1 ) > 0.001f) &&
-       (spherecast_world( world, head_wp0, head_wp1, 0.2f, &t, n,
-                          k_material_flag_walking ) != -1) )
-   {
-      v3_lerp( start_co, localplayer.rb.co, t, localplayer.rb.co );
-      rb_update_matrices( &localplayer.rb );
-
-      vg_info( "player fell of due to hitting head\n" );
-      player__dead_transition( k_player_die_type_head );
-      return;
-   }
-
-   /*
-    * Phase 1: Regular collision detection
-    * --------------------------------------------------------------------------
-    */
-
-   for( int i=0; i<k_wheel_count; i++ ){
-      if( wheels[i].state == k_collider_state_disabled )
-         continue;
-
-      m4x3f mtx;
-      m3x3_identity( mtx );
-      m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, mtx[3] );
-
-      rb_ct *man = &manifold[ manifold_len ];
-
-      int l = skate_collide_smooth( mtx, wheels[i].radius, man );
-      if( l )
-         wheels[i].state = k_collider_state_colliding;
-
-      manifold_len += l;
-   }
-
-   float grind_radius = k_board_radius * 0.75f;
-   rb_capsule capsule = { .h = (k_board_length+0.2f)*2.0f, 
-                          .r = grind_radius };
-   m4x3f mtx;
-   v3_muls( localplayer.rb.to_world[0],  1.0f, mtx[0] );
-   v3_muls( localplayer.rb.to_world[2], -1.0f, mtx[1] );
-   v3_muls( localplayer.rb.to_world[1],  1.0f, mtx[2] );
-   v3_muladds( localplayer.rb.to_world[3], localplayer.rb.to_world[1], 
-               grind_radius + k_board_radius*0.25f+state->slap, mtx[3] );
-
-   rb_ct *cman = &manifold[manifold_len];
-
-   int l = rb_capsule__scene( mtx, &capsule, NULL, world->geo_bh,
-                              cman, k_material_flag_walking );
-
-   /* weld joints */
-   for( int i=0; i<l; i ++ )
-      cman[l].type = k_contact_type_edge;
-   rb_manifold_filter_joint_edges( cman, l, 0.03f );
-   l = rb_manifold_apply_filtered( cman, l );
-
-   manifold_len += l;
-   vg_line_capsule( mtx, capsule.r, capsule.h, VG__WHITE );
-
-   /* add limits */
-   if( state->activity >= k_skate_activity_grind_any ){
-      for( int i=0; i<player_skate.limit_count; i++ ){
-         struct grind_limit *limit = &player_skate.limits[i];
-         rb_ct *ct = &manifold[ manifold_len ++ ];
-         m4x3_mulv( localplayer.rb.to_world, limit->ra, ct->co );
-         m3x3_mulv( localplayer.rb.to_world, limit->n, ct->n );
-         ct->p = limit->p;
-         ct->type = k_contact_type_default;
-      }
-   }
-
-   /* 
-    * Phase 3: Dynamics
-    * --------------------------------------------------------------------------
-    */
-
-
-   v3f world_cog;
-   m4x3_mulv( localplayer.rb.to_world, 
-              player_skate.weight_distribution, world_cog );
-   vg_line_point( world_cog, 0.02f, VG__BLACK );
-
-   for( int i=0; i<manifold_len; i ++ ){
-      rb_prepare_contact( &manifold[i], player_skate.substep_delta );
-      rb_debug_contact( &manifold[i] );
-   }
-
-   /* yes, we are currently rebuilding mass matrices every frame. too bad! */
-   v3f extent = { k_board_width*10.0f, 0.1f, k_board_length };
-   float ex2 = k_board_interia*extent[0]*extent[0],
-         ey2 = k_board_interia*extent[1]*extent[1],
-         ez2 = k_board_interia*extent[2]*extent[2];
-
-   float mass = 2.0f * (extent[0]*extent[1]*extent[2]);
-   float inv_mass = 1.0f/mass;
-
-   v3f I;
-   I[0] = ((1.0f/12.0f) * mass * (ey2+ez2));
-   I[1] = ((1.0f/12.0f) * mass * (ex2+ez2));
-   I[2] = ((1.0f/12.0f) * mass * (ex2+ey2));
-
-   m3x3f iI;
-   m3x3_identity( iI );
-   iI[0][0] = I[0];
-   iI[1][1] = I[1];
-   iI[2][2] = I[2];
-   m3x3_inv( iI, iI );
-
-   m3x3f iIw;
-   m3x3_mul( iI, localplayer.rb.to_local, iIw );
-   m3x3_mul( localplayer.rb.to_world, iIw, iIw );
-
-   for( int j=0; j<10; j++ ){
-      for( int i=0; i<manifold_len; i++ ){
-         /* 
-          * regular dance; calculate velocity & total mass, apply impulse.
-          */
-
-         rb_ct *ct = &manifold[i];
-         
-         v3f rv, delta;
-         v3_sub( ct->co, world_cog, delta ); 
-         v3_cross( localplayer.rb.w, delta, rv );
-         v3_add( localplayer.rb.v, rv, rv );
-
-         v3f raCn;
-         v3_cross( delta, ct->n, raCn );
-
-         v3f raCnI, rbCnI;
-         m3x3_mulv( iIw, raCn, raCnI );
-
-         float normal_mass = 1.0f / (inv_mass + v3_dot(raCn,raCnI)),
-               vn = v3_dot( rv, ct->n ),
-               lambda = normal_mass * ( -vn );
-
-         float temp = ct->norm_impulse;
-         ct->norm_impulse = vg_maxf( temp + lambda, 0.0f );
-         lambda = ct->norm_impulse - temp;
-
-         v3f impulse;
-         v3_muls( ct->n, lambda, impulse );
-
-         v3_muladds( normal_total, impulse, inv_mass, normal_total );
-         v3_muladds( localplayer.rb.v, impulse, inv_mass, localplayer.rb.v );
-         v3_cross( delta, impulse, impulse );
-         m3x3_mulv( iIw, impulse, impulse );
-         v3_add( impulse, localplayer.rb.w, localplayer.rb.w );
-
-         v3_cross( localplayer.rb.w, delta, rv );
-         v3_add( localplayer.rb.v, rv, rv );
-         vn = v3_dot( rv, ct->n );
-      }
-   }
-
-   v3f dt;
-   rb_depenetrate( manifold, manifold_len, dt );
-   v3_add( dt, localplayer.rb.co, localplayer.rb.co );
-   rb_update_matrices( &localplayer.rb );
-
-   substep_count ++;
-
-   if( player_skate.substep >= 0.0001f )
-      goto begin_collision;      /* again! */
-
-   /* 
-    * End of collision and dynamics routine
-    * --------------------------------------------------------------------------
-    */
-
-   f32 nforce = v3_length(normal_total);
-   if( nforce > 4.0f ){
-      if( nforce > 17.6f ){
-         vg_info( "player fell off due to hitting ground too hard\n" );
-         v3_muladds( localplayer.rb.v, normal_total, -1.0f, localplayer.rb.v );
-         player__dead_transition( k_player_die_type_feet );
-         return;
-      }
-
-      f32 amt = k_cam_punch;
-      if( localplayer.cam_control.camera_mode == k_cam_firstperson ){
-         amt *= 0.25f;
-      }
-
-      v3_muladds( localplayer.cam_land_punch_v, normal_total, amt,
-                  localplayer.cam_land_punch_v );
-   }
-
-   player_skate.surface = k_surface_prop_concrete;
-
-   for( int i=0; i<manifold_len; i++ ){
-      rb_ct *ct = &manifold[i];
-      struct world_surface *surf = world_contact_surface( world, ct );
-
-      if( surf->info.surface_prop > player_skate.surface )
-         player_skate.surface = surf->info.surface_prop;
-   }
-
-   for( int i=0; i<k_wheel_count; i++ ){
-      m4x3f mtx;
-      m3x3_copy( localplayer.rb.to_world, mtx );
-      m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, mtx[3] );
-      vg_line_sphere( mtx, wheels[i].radius,
-                      (u32[]){ VG__WHITE, VG__BLACK, 
-                            wheels[i].colour }[ wheels[i].state ]);
-   }
-
-   skate_integrate();
-   vg_line_point( state->cog, 0.02f, VG__WHITE );
-
-   u32 id = world_intersect_gates( world, localplayer.rb.co, state->prev_pos );
-
-   if( id ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
-
-      m4x3_mulv( gate->transport, localplayer.rb.co, localplayer.rb.co );
-      m3x3_mulv( gate->transport, localplayer.rb.v,  localplayer.rb.v );
-      m4x3_mulv( gate->transport, state->cog,   state->cog );
-      m3x3_mulv( gate->transport, state->cog_v, state->cog_v );
-      m3x3_mulv( gate->transport, state->throw_v, state->throw_v );
-      m3x3_mulv( gate->transport, state->head_position,
-                                  state->head_position );
-      m3x3_mulv( gate->transport, state->up_dir, state->up_dir );
-
-      v4f transport_rotation;
-      m3x3_q( gate->transport, transport_rotation );
-      q_mul( transport_rotation, localplayer.rb.q, localplayer.rb.q );
-      q_mul( transport_rotation, state->smoothed_rotation,
-                                 state->smoothed_rotation );
-      q_normalize( localplayer.rb.q );
-      q_normalize( state->smoothed_rotation );
-      rb_update_matrices( &localplayer.rb );
-      player__pass_gate( id );
-   }
-
-   /* FIXME: Rate limit */
-   static int stick_frames = 0;
-
-   if( state->activity >= k_skate_activity_ground )
-      stick_frames ++;
-   else
-      stick_frames = 0;
-
-   if( stick_frames > 5 ) stick_frames =  5;
-
-   if( stick_frames == 4 ){
-      if( state->activity == k_skate_activity_ground ){
-         if( (fabsf(state->slip) > 0.75f) ){
-            player__networked_sfx( k_player_subsystem_skate, 128, 
-                                   k_player_skate_soundeffect_land_bad,
-                                   localplayer.rb.co, 0.6f );
-         }
-         else{
-            player__networked_sfx( k_player_subsystem_skate, 128, 
-                                   k_player_skate_soundeffect_land_good,
-                                   localplayer.rb.co, 1.0f );
-         }
-      }
-      else if( player_skate.surface == k_surface_prop_metal ){
-         player__networked_sfx( k_player_subsystem_skate, 128, 
-                                k_player_skate_soundeffect_grind_metal,
-                                localplayer.rb.co, 1.0f );
-      }
-      else{
-         player__networked_sfx( k_player_subsystem_skate, 128, 
-                                k_player_skate_soundeffect_grind_wood,
-                                localplayer.rb.co, 1.0f );
-      }
-   } else if( stick_frames == 0 ){
-      /* TODO: EXIT SOUNDS */
-   }
-
-   if( (state->activity_prev < k_skate_activity_grind_any) && 
-       (state->activity >= k_skate_activity_grind_any) ){
-      state->velocity_limit = v3_length( localplayer.rb.v )*1.0f;
-      state->grind_y_start = localplayer.rb.co[1];
-   }
-
-   if( state->activity >= k_skate_activity_grind_any ){
-      f32 dy = localplayer.rb.co[1] - state->grind_y_start;
-      if( dy < 0.0f ){
-         state->velocity_limit += -dy*0.2f;
-      }
-      state->grind_y_start = localplayer.rb.co[1];
-
-
-      f32 speed_end = v3_length( localplayer.rb.v );
-      if( speed_end > state->velocity_limit ){
-         v3_muls( localplayer.rb.v, state->velocity_limit/speed_end, 
-                  localplayer.rb.v );
-      }
-   }
-}
-
-void player__skate_im_gui( ui_context *ctx )
-{
-   struct player_skate_state *state = &player_skate.state;
-   player__debugtext( ctx, 1, "V:  %5.2f %5.2f %5.2f",localplayer.rb.v[0],
-                                                localplayer.rb.v[1],
-                                                localplayer.rb.v[2] );
-   player__debugtext( ctx, 1, "CO: %5.2f %5.2f %5.2f",localplayer.rb.co[0],
-                                                localplayer.rb.co[1],
-                                                localplayer.rb.co[2] );
-   player__debugtext( ctx, 1, "W:  %5.2f %5.2f %5.2f",localplayer.rb.w[0],
-                                                localplayer.rb.w[1],
-                                                localplayer.rb.w[2] );
-
-   const char *activity_txt[] = {
-      "air",
-      "air_to_grind",
-      "ground",
-      "handplant",
-      "undefined (INVALID)",
-      "grind_any (INVALID)",
-      "grind_boardslide",
-      "grind_metallic (INVALID)",
-      "grind_back50",
-      "grind_front50",
-      "grind_5050"
-   };
-
-   player__debugtext( ctx, 1, "activity: %s", activity_txt[state->activity] );
-   player__debugtext( ctx, 1, "flip: %.4f %.4f", state->flip_rate, 
-                                             state->flip_time );
-   player__debugtext( ctx, 1, "trickv: %.2f %.2f %.2f", 
-                           state->trick_vel[0],
-                           state->trick_vel[1],
-                           state->trick_vel[2] );
-   player__debugtext( ctx, 1, "tricke: %.2fs %.2f %.2f %.2f", 
-                           state->trick_time,
-                           state->trick_euler[0],
-                           state->trick_euler[1],
-                           state->trick_euler[2] );
-}
-
-void player__skate_animate(void){
-   struct player_skate_state *state = &player_skate.state;
-   struct player_skate_animator *animator = &player_skate.animator;
-
-   /* Head */
-   float kheight = 2.0f,
-         kleg = 0.6f;
-
-   v3_zero( animator->offset );
-
-   v3f cog_local, cog_ideal;
-   m4x3_mulv( localplayer.rb.to_local, state->cog, cog_local );
-
-   v3_copy( state->up_dir, cog_ideal );
-   v3_normalize( cog_ideal );
-   m3x3_mulv( localplayer.rb.to_local, cog_ideal, cog_ideal );
-
-   v3_sub( cog_ideal, cog_local, animator->offset );
-
-   v3_muls( animator->offset, 4.0f, animator->offset );
-   animator->offset[1] *= -1.0f;
-
-   float curspeed  = v3_length( localplayer.rb.v ),
-         kickspeed = vg_clampf( curspeed*(1.0f/40.0f), 0.0f, 1.0f ),
-         kicks     = (vg_randf64(&vg.rand)-0.5f)*2.0f*kickspeed,
-         sign      = vg_signf( kicks );
-
-   animator->wobble[0] = vg_lerpf( animator->wobble[0], kicks*kicks*sign, 
-                                   6.0f*vg.time_delta);
-   animator->wobble[1] = vg_lerpf( animator->wobble[1], animator->wobble[0], 
-                                   2.4f*vg.time_delta);
-
-   animator->offset[0] *= 0.26f;
-   animator->offset[0] += animator->wobble[1]*3.0f;
-
-   animator->offset[1] *= -0.3f;
-   animator->offset[2] *= 0.01f;
-
-   animator->offset[0]=vg_clampf(animator->offset[0],-0.8f,0.8f)*
-                                 (1.0f-fabsf(animator->slide)*0.9f);
-   animator->offset[1]=vg_clampf(animator->offset[1],-0.5f,0.0f);
-
-   v3f cam_offset;
-   v3_mul( animator->offset, (v3f){1.0f,0.3f,1.0f}, cam_offset );
-
-   /* localized vectors */
-   m4x3_mulv( localplayer.rb.to_local, state->cog, animator->local_cog );
-
-   /* 
-    * Animation blending
-    * ===========================================
-    */
-   
-   /* sliding */
-   {
-      float desired = 0.0f;
-      if( state->activity == k_skate_activity_ground )
-         desired = vg_clampf( vg_maxf(fabsf( state->slip ),
-                                      fabsf( state->skid ) ), 0.0f, 1.0f );
-
-      animator->slide = vg_lerpf( animator->slide, desired, 2.4f*vg.time_delta);
-
-      f32 dirx = 0.0f;
-      if( fabsf(state->slip) > fabsf(dirx) ) dirx = state->slip;
-      if( fabsf(state->skid) > fabsf(dirx) ) dirx = state->skid;
-      if( fabsf( dirx ) > 0.025f ) dirx = vg_signf( dirx );
-         dirx = vg_signf( state->slip );
-      vg_slewf( &animator->x, dirx, 2.6f*vg.time_delta );
-   }
-
-   cam_offset[0] += animator->slide * -animator->x;
-   v3_copy( cam_offset, localplayer.cam_control.tpv_offset_extra );
-   
-   /* movement information */
-   int iair = state->activity <= k_skate_activity_air_to_grind;
-
-   float dirz = state->reverse > 0.0f? 0.0f: 1.0f,
-         fly  = iair?                  1.0f: 0.0f,
-         wdist= player_skate.weight_distribution[2] / k_board_length;
-
-   if( state->activity >= k_skate_activity_grind_any )
-      wdist = 0.0f;
-
-   animator->z      = vg_lerpf( animator->z,      dirz,  2.4f*vg.time_delta );
-   animator->skid = state->skid;
-   animator->fly    = vg_lerpf( animator->fly,    fly,   3.4f*vg.time_delta );
-   animator->weight = vg_lerpf( animator->weight, wdist, 9.0f*vg.time_delta );
-
-   float stand = 1.0f - vg_clampf( curspeed * 0.03f, 0.0f, 1.0f );
-   animator->stand  = vg_lerpf( animator->stand, stand, 6.0f*vg.time_delta );
-   animator->reverse = state->reverse;
-
-   if( fabsf(state->slip) > 0.3f ){
-      f32 slide_x = v3_dot(localplayer.rb.v, localplayer.rb.to_world[0]);
-      state->delayed_slip_dir = vg_signf(slide_x);
-   }
-
-   /* grinding */
-   f32 grind=state->activity >= k_skate_activity_grind_any? 1.0f: 0.0f;
-   animator->grind = vg_lerpf( animator->grind,  grind, 5.0f*vg.time_delta );
-
-   f32 grind_frame = 0.5f;
-
-   if( state->activity == k_skate_activity_grind_front50 )
-      grind_frame = 0.0f;
-   else if( state->activity == k_skate_activity_grind_back50 )
-      grind_frame = 1.0f;
-
-   animator->grind_balance = vg_lerpf( animator->grind_balance, grind_frame, 
-                                5.0f*vg.time_delta );
-   animator->activity = state->activity;
-   animator->surface = player_skate.surface;
-
-   /* pushing */
-   animator->push_time = vg.time - state->start_push;
-   animator->push = vg_lerpf( animator->push, 
-                              (vg.time - state->cur_push) < 0.125,
-                              6.0f*vg.time_delta );
-
-   /* jumping */
-   animator->jump_charge = state->jump_charge;
-   animator->jump = vg_lerpf( animator->jump, animator->jump_charge, 
-                              8.4f*vg.time_delta );
-
-   /* trick setup */
-   animator->jump_dir = state->jump_dir;
-   f32 jump_start_frame = 14.0f/30.0f;
-   animator->jump_time = animator->jump_charge * jump_start_frame;
-   f32 jump_frame = (vg.time - state->jump_time) + jump_start_frame;
-   if( jump_frame >= jump_start_frame && jump_frame <= (40.0f/30.0f) )
-      animator->jump_time = jump_frame;
-
-   /* trick */
-   float jump_t = vg.time-state->jump_time;
-   float k=17.0f;
-   float h = k*jump_t;
-   float extra = h*exp(1.0-h) * (state->jump_dir?1.0f:-1.0f);
-         extra *= state->slap * 4.0f;
-
-   v3_add( state->trick_euler, state->trick_residuald, 
-            animator->board_euler );
-   v3_muls( animator->board_euler, VG_TAUf, animator->board_euler );
-
-   animator->board_euler[0] *= 0.5f;
-   animator->board_euler[1] += extra;
-   animator->trick_type = state->trick_type;
-
-   /* board lean */
-   f32 lean1, lean2 = animator->steer[0] * animator->reverse * -0.36f,
-       lean;
-
-   lean1 = animator->slide * animator->delayed_slip_dir;
-   if( fabsf(lean1)>fabsf(lean2) ) lean = lean1;
-   else                            lean = lean2;
-
-   if( ((int)roundf(animator->board_euler[0]/VG_PIf)) % 2 ) lean = -lean;
-   lean = vg_clampf( lean, -1.0f, 1.0f );
-   animator->board_lean = 
-      vg_lerpf(animator->board_lean, lean, vg.time_delta*18.0f);
-
-   /* feet placement */
-   struct player_board *board = 
-      addon_cache_item_if_loaded( k_addon_type_board,
-                                  localplayer.board_view_slot );
-   if( board ){
-      if( animator->weight > 0.0f ){
-         animator->foot_offset[0] = 
-            board->truck_positions[k_board_truck_back][2]+0.3f;
-      }
-      else{
-         animator->foot_offset[1] = 
-            board->truck_positions[k_board_truck_front][2]-0.3f;
-      }
-   }
-
-   f32 slapm = vg_maxf( 1.0f-v3_length2( state->trick_vel ), 0.0f );
-   animator->slap = state->slap;
-   animator->subslap = vg_lerpf( animator->subslap, slapm, 
-                                 vg.time_delta*10.0f );
-
-#if 0
-   f32 l = ((state->activity < k_skate_activity_ground) &&
-             v3_length2(state->trick_vel) > 0.1f )? 1: 0;
-   animator->trick_foot = vg_lerpf( animator->trick_foot, l, 
-                                    8.4f*vg.time_delta );
-#endif
-
-   animator->trick_foot = vg_exp_impulse( state->trick_time, 5.0f );
-
-   /* grab */
-   v2f grab_input;
-   joystick_state( k_srjoystick_grab, grab_input );
-   v2_add( state->grab_mouse_delta, grab_input, grab_input );
-
-   if( v2_length2( grab_input ) <= 0.001f ) grab_input[0] = -1.0f;
-   else v2_normalize_clamp( grab_input );
-   v2_lerp( animator->grab, grab_input, 2.4f*vg.time_delta, animator->grab );
-   animator->grabbing = state->grabbing;
-
-   /* steer */
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-   animator->airdir = vg_lerpf( animator->airdir, 
-                                -steer[0], 2.4f*vg.time_delta );
-
-   animator->steer[0] = steer[0];
-   animator->steer[1] = vg_lerpf( animator->steer[1], 
-                                  steer[0], 4.0f*vg.time_delta );
-      
-
-   /* flip angle */
-   if( (state->activity <= k_skate_activity_air_to_grind) &&
-       (fabsf(state->flip_rate) > 0.01f) ){
-      float substep = vg.time_fixed_extrapolate;
-      float t  = state->flip_time+state->flip_rate*substep*vg.time_fixed_delta;
-            sign  = vg_signf( t );
-
-      t  = 1.0f - vg_minf( 1.0f, fabsf( t * 1.1f ) );
-      t  = sign * (1.0f-t*t);
-
-      f32 angle = vg_clampf( t, -1.0f, 1.0f ) * VG_TAUf,
-          distm = state->land_dist * fabsf(state->flip_rate) * 3.0f,
-          blend = vg_clampf( 1.0f-distm, 0.0f, 1.0f );
-      angle = vg_lerpf( angle, vg_signf(state->flip_rate)*VG_TAUf, blend );
-      q_axis_angle( animator->qflip, state->flip_axis, angle );
-   }
-   else 
-      q_identity( animator->qflip );
-
-   /* counter-rotation */
-   if( v3_length2( state->up_dir ) > 0.001f ){
-      v4_lerp( state->smoothed_rotation, localplayer.rb.q, 
-               2.0f*vg.time_frame_delta,
-               state->smoothed_rotation );
-      q_normalize( state->smoothed_rotation );
-
-      v3f yaw_smooth = {1.0f,0.0f,0.0f};
-      q_mulv( state->smoothed_rotation, yaw_smooth, yaw_smooth );
-      m3x3_mulv( localplayer.rb.to_local, yaw_smooth, yaw_smooth );
-      yaw_smooth[1] = 0.0f;
-      v3_normalize( yaw_smooth );
-
-      f32 yaw_counter_rotate  = yaw_smooth[0];
-          yaw_counter_rotate  = vg_maxf( 0.7f, yaw_counter_rotate );
-          yaw_counter_rotate  = acosf( yaw_counter_rotate );
-          yaw_counter_rotate *= 1.0f-animator->fly;
-
-      v3f ndir;
-      m3x3_mulv( localplayer.rb.to_local, state->up_dir, ndir );
-      v3_normalize( ndir );
-
-      v3f up = { 0.0f, 1.0f, 0.0f };
-      float a = v3_dot( ndir, up );
-      a = acosf( vg_clampf( a, -1.0f, 1.0f ) );
-
-      v3f axis;
-      v4f qcounteryaw, qfixup;
-      
-      v3_cross( up, ndir, axis );
-      q_axis_angle( qfixup, axis, a*2.0f );
-
-      v3_cross( (v3f){1.0f,0.0f,0.0f}, yaw_smooth, axis );
-      q_axis_angle( qcounteryaw, axis, yaw_counter_rotate );
-
-      q_mul( qcounteryaw, qfixup, animator->qfixuptotal );
-      q_normalize( animator->qfixuptotal );
-
-      v3f p1, p2;
-      m3x3_mulv( localplayer.rb.to_world, up, p1 );
-      m3x3_mulv( localplayer.rb.to_world, ndir, p2 );
-
-      vg_line_arrow( localplayer.rb.co, p1, 0.5f, VG__PINK );
-      vg_line_arrow( localplayer.rb.co, p2, 0.5f, VG__PINK );
-   }
-   else q_identity( animator->qfixuptotal );
-
-   if( state->activity == k_skate_activity_handplant ){
-      v3_copy( state->store_co, animator->root_co );
-      v4_copy( state->store_q, animator->root_q );
-      v3_zero( animator->root_v );
-   }
-   else {
-      rb_extrapolate( &localplayer.rb, animator->root_co, animator->root_q );
-      v3_copy( localplayer.rb.v, animator->root_v );
-   }
-
-   animator->handplant_t = state->handplant_t;
-}
-                        
-void player__skate_pose( void *_animator, player_pose *pose ){
-   struct skeleton *sk = &localplayer.skeleton;
-   struct player_skate_animator *animator = _animator;
-
-   pose->type = k_player_pose_type_ik;
-   v3_copy( animator->root_co, pose->root_co );
-   v4_copy( animator->root_q, pose->root_q );
-
-   /* transform */
-   v3f ext_up,ext_co;
-   q_mulv( pose->root_q, (v3f){0.0f,1.0f,0.0f}, ext_up );
-   v3_copy( pose->root_co, ext_co );
-   v3_muladds( pose->root_co, ext_up, -0.1f, pose->root_co );
-
-   /* apply flip rotation at midpoint */
-   q_mul( animator->qflip, pose->root_q, pose->root_q );
-   q_normalize( pose->root_q );
-
-   v3f rotation_point, rco;
-   v3_muladds( ext_co, ext_up, 0.5f, rotation_point );
-   v3_sub( pose->root_co, rotation_point, rco );
-   
-   q_mulv( animator->qflip, rco, rco );
-   v3_add( rco, rotation_point, pose->root_co );
-
-   /* ANIMATIONS 
-    * ---------------------------------------------------------------------- */
-
-   mdl_keyframe apose[32], bpose[32];
-   mdl_keyframe ground_pose[32];
-   {
-      /* stand/crouch */
-      f32 dir_frame   = animator->z * (15.0f/30.0f),
-          stand_blend = animator->offset[1]*-2.0f;
-
-      pose->board.lean = animator->board_lean;
-
-      stand_blend = vg_clampf( 1.0f-animator->local_cog[1], 0, 1 );
-
-      skeleton_sample_anim( sk, player_skate.anim_stand, dir_frame, apose );
-      skeleton_sample_anim( sk, player_skate.anim_highg, dir_frame, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, stand_blend, apose );
-
-      /* sliding */
-      f32 slide_frame = animator->x * 0.25f + 0.25f;
-      skeleton_sample_anim( sk, player_skate.anim_slide, slide_frame, bpose );
-
-      mdl_keyframe mirrored[32];
-      player_mirror_pose( bpose, mirrored );
-      skeleton_lerp_pose( sk, bpose, mirrored, animator->z, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, animator->slide, apose );
-
-      if( animator->reverse > 0.0f ){
-         skeleton_sample_anim( sk, player_skate.anim_push, animator->push_time, 
-                               bpose );
-      }
-      else{
-         skeleton_sample_anim( sk, player_skate.anim_push_reverse, 
-                               animator->push_time, bpose );
-      }
-      skeleton_lerp_pose( sk, apose, bpose, animator->push, apose );
-
-      struct skeleton_anim *jump_anim = animator->jump_dir?
-                                        player_skate.anim_ollie:
-                                        player_skate.anim_ollie_reverse;
-
-      f32 setup_blend = vg_minf( animator->jump, 1.0f );
-      skeleton_sample_anim_clamped( sk, jump_anim, animator->jump_time, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, setup_blend, ground_pose );
-   }
-   
-   mdl_keyframe air_pose[32];
-   {
-      float air_frame = (animator->airdir*0.5f+0.5f) * (15.0f/30.0f);
-      skeleton_sample_anim( sk, player_skate.anim_air, air_frame, apose );
-
-      float ang = atan2f( animator->grab[0], animator->grab[1] ),
-            ang_unit = (ang+VG_PIf) * (1.0f/VG_TAUf),
-            grab_frame = ang_unit * (15.0f/30.0f);
-
-      skeleton_sample_anim( sk, player_skate.anim_grabs, grab_frame, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, animator->grabbing, air_pose );
-   }
-
-   skeleton_lerp_pose( sk, ground_pose, air_pose, animator->fly, 
-                       pose->keyframes );
-
-   mdl_keyframe *kf_board    = &pose->keyframes[localplayer.id_board-1],
-                *kf_foot_l   = &pose->keyframes[localplayer.id_ik_foot_l-1],
-                *kf_foot_r   = &pose->keyframes[localplayer.id_ik_foot_r-1],
-                *kf_knee_l   = &pose->keyframes[localplayer.id_ik_knee_l-1],
-                *kf_knee_r   = &pose->keyframes[localplayer.id_ik_knee_r-1],
-                *kf_hip      = &pose->keyframes[localplayer.id_hip-1],
-                *kf_wheels[] = { &pose->keyframes[localplayer.id_wheel_r-1],
-                                 &pose->keyframes[localplayer.id_wheel_l-1] };
-
-
-   mdl_keyframe grind_pose[32];
-   {
-      f32 frame = animator->grind_balance * 0.5f;
-
-      skeleton_sample_anim( sk, player_skate.anim_grind, frame, apose );
-      skeleton_sample_anim( sk, player_skate.anim_grind_jump, frame, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, animator->jump, grind_pose );
-   }
-   skeleton_lerp_pose( sk, pose->keyframes, grind_pose, 
-                       animator->grind, pose->keyframes );
-   float add_grab_mod = 1.0f - animator->fly;
-
-   /* additive effects */
-   u32 apply_to[] = { localplayer.id_hip, 
-                      localplayer.id_ik_hand_l,
-                      localplayer.id_ik_hand_r,
-                      localplayer.id_ik_elbow_l,
-                      localplayer.id_ik_elbow_r };
-
-   float apply_rates[] = { 1.0f,
-                           0.75f,
-                           0.75f,
-                           0.75f,
-                           0.75f };
-
-   for( int i=0; i<VG_ARRAY_LEN(apply_to); i ++ ){
-      pose->keyframes[apply_to[i]-1].co[0] += animator->offset[0]*add_grab_mod;
-      pose->keyframes[apply_to[i]-1].co[2] += animator->offset[2]*add_grab_mod;
-   }
-
-#if 1
-   /* angle 'correction' */
-   v3f origin;
-   v3_add( sk->bones[localplayer.id_hip].co, kf_hip->co, origin );
-
-   for( int i=0; i<VG_ARRAY_LEN(apply_to); i ++ ){
-      mdl_keyframe *kf = &pose->keyframes[apply_to[i]-1];
-      keyframe_rotate_around( kf, origin, sk->bones[apply_to[i]].co,
-                              animator->qfixuptotal );
-   }
-#endif
-
-
-   if( animator->activity == k_skate_activity_handplant ){
-      struct skeleton_anim *anim = player_skate.anim_handplant;
-
-      mdl_keyframe hpose[32];
-      skeleton_sample_anim_clamped( sk, anim, animator->handplant_t, hpose );
-      if( animator->reverse < 0.0f )
-         player_mirror_pose( hpose, hpose );
-
-      mdl_keyframe *kf_world = &hpose[ localplayer.id_world -1 ];
-      m4x3f world, mmdl, world_view;
-      q_m3x3( kf_world->q, world );
-      v3_copy( kf_world->co, world[3] );
-
-      q_m3x3( pose->root_q, mmdl );
-      v3_copy( pose->root_co, mmdl[3] );
-
-      m4x3_mul( mmdl, world, world_view );
-
-      vg_line_arrow( world_view[3], world_view[0], 1.0f, 0xff0000ff );
-      vg_line_arrow( world_view[3], world_view[1], 1.0f, 0xff00ff00 );
-      vg_line_arrow( world_view[3], world_view[2], 1.0f, 0xffff0000 );
-
-      m4x3f invworld;
-      m4x3_invert_affine( world, invworld );
-      m4x3_mul( mmdl, invworld, world_view );
-
-      m3x3_q( world_view, pose->root_q );
-      v3_copy( world_view[3], pose->root_co );
-
-      f32 t        = animator->handplant_t,
-          frames   = anim->length-1,
-          length   = animator->activity == k_skate_activity_handplant?
-                        frames / anim->rate:
-                        999999,
-          end_dist = vg_minf( t, length - t )/k_anim_transition,
-          blend    = vg_smoothstepf( vg_minf(1,end_dist) );
-
-      skeleton_lerp_pose( sk, pose->keyframes, hpose, blend, pose->keyframes );
-   }
-
-
-   /* trick rotation */
-   v4f qtrick, qyaw, qpitch, qroll;
-   q_axis_angle( qyaw,   (v3f){0.0f,1.0f,0.0f}, animator->board_euler[0] );
-   q_axis_angle( qpitch, (v3f){1.0f,0.0f,0.0f}, animator->board_euler[1] );
-   q_axis_angle( qroll,  (v3f){0.0f,0.0f,1.0f}, animator->board_euler[2] );
-
-   q_mul( qyaw, qroll, qtrick );
-   q_mul( qpitch, qtrick, qtrick );
-   q_mul( kf_board->q, qtrick, kf_board->q );
-   q_normalize( kf_board->q );
-
-   kf_foot_l->co[2] = vg_lerpf( kf_foot_l->co[2], animator->foot_offset[0],
-                                 0.5f * animator->weight );
-   kf_foot_r->co[2] = vg_lerpf( kf_foot_r->co[2], animator->foot_offset[1],
-                                -0.5f * animator->weight );
-
-   kf_foot_l->co[1] += animator->slap;
-   kf_foot_r->co[1] += animator->slap;
-   kf_knee_l->co[1] += animator->slap;
-   kf_knee_r->co[1] += animator->slap;
-   kf_board->co[1]  += animator->slap * animator->subslap;
-   kf_hip->co[1] += animator->slap * 0.25f;
-
-   /* kickflip and shuvit are in the wrong order for some reason */
-   if( animator->trick_type == k_trick_type_kickflip ){
-      kf_foot_l->co[0] += animator->trick_foot * 0.15f;
-      kf_foot_r->co[0] -= animator->trick_foot * 0.15f;
-      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
-      kf_foot_r->co[1] -= animator->trick_foot * 0.18f;
-   }
-   else if( animator->trick_type == k_trick_type_shuvit ){
-      kf_foot_l->co[0] += animator->trick_foot * 0.2f;
-      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
-      kf_foot_r->co[0] -= animator->trick_foot * 0.1f;
-      kf_foot_r->co[1] += animator->trick_foot * 0.09f;
-   }
-   else if( animator->trick_type == k_trick_type_treflip ){
-      kf_foot_l->co[0] += animator->trick_foot * 0.2f;
-      kf_foot_r->co[0] -= animator->trick_foot * 0.15f;
-      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
-      kf_foot_r->co[1] -= animator->trick_foot * 0.18f;
-   }
-
-   /* 
-    * animation wishlist:
-    *    boardslide/grind jump animations
-    *    when tricking the slap should not appply or less apply
-    *    not animations however DONT target grinds that are vertically down.
-    */
-
-   /* truck rotation */
-   for( int i=0; i<2; i++ ){
-      float a = vg_minf( player_skate.truckv0[i][0], 1.0f );
-      a = -acosf( a ) * vg_signf( player_skate.truckv0[i][1] );
-
-      v4f q;
-      q_axis_angle( q, (v3f){0.0f,0.0f,1.0f}, a );
-      q_mul( q, kf_wheels[i]->q, kf_wheels[i]->q );
-      q_normalize( kf_wheels[i]->q );
-   }
-#if 1
-   {
-      mdl_keyframe
-         *kf_head    = &pose->keyframes[localplayer.id_head-1],
-         *kf_elbow_l = &pose->keyframes[localplayer.id_ik_elbow_l-1],
-         *kf_elbow_r = &pose->keyframes[localplayer.id_ik_elbow_r-1],
-         *kf_hand_l  = &pose->keyframes[localplayer.id_ik_hand_l-1],
-         *kf_hand_r  = &pose->keyframes[localplayer.id_ik_hand_r-1],
-         *kf_hip     = &pose->keyframes[localplayer.id_hip-1];
-
-      float warble = vg_perlin_fract_1d( vg.time, 2.0f, 2, 300 );
-            warble *= vg_maxf(animator->grind, fabsf(animator->weight)) * 0.3f;
-
-      v4f qrot;
-      q_axis_angle( qrot, (v3f){0.8f,0.7f,0.6f}, warble );
-
-      v3f origin = {0.0f,0.2f,0.0f};
-      keyframe_rotate_around( kf_hand_l, origin, 
-                              sk->bones[localplayer.id_ik_hand_l].co, qrot );
-      keyframe_rotate_around( kf_hand_r, origin, 
-                              sk->bones[localplayer.id_ik_hand_r].co, qrot );
-      keyframe_rotate_around( kf_hip, origin, 
-                              sk->bones[localplayer.id_hip].co, qrot );
-      keyframe_rotate_around( kf_elbow_r, origin, 
-                              sk->bones[localplayer.id_ik_elbow_r].co, qrot );
-      keyframe_rotate_around( kf_elbow_l, origin, 
-                              sk->bones[localplayer.id_ik_elbow_l].co, qrot );
-
-      q_inv( qrot, qrot );
-      q_mul( qrot, kf_head->q, kf_head->q );
-      q_normalize( kf_head->q );
-
-
-      /* hand placement */
-
-      u32 hand_id = animator->z < 0.5f? 
-                    localplayer.id_ik_hand_l: localplayer.id_ik_hand_r;
-
-      v3f sample_co;
-      m4x3f mmdl;
-      q_m3x3( pose->root_q, mmdl );
-      q_mulv( pose->root_q, pose->keyframes[hand_id-1].co, mmdl[3] );
-      v3_add( mmdl[3], pose->root_co, mmdl[3] );
-      m4x3_mulv( mmdl, sk->bones[hand_id].co, sample_co );
-
-      v3_muladds( sample_co, mmdl[1], 0.3f, sample_co );
-      vg_line_point( sample_co, 0.04f, 0xff0000ff );
-
-      v3f dir;
-      v3_muls( mmdl[1], -1.0f, dir );
-      ray_hit hit = { .dist = 1.5f };
-      if(ray_world( world_current_instance(), sample_co, dir, &hit, 0 )){
-         vg_line_cross( hit.pos, 0xff0000ff, 0.05f );
-         vg_line( sample_co, hit.pos, 0xffffffff );
-
-         f32 amt = vg_maxf( 0.0f, animator->slide-0.5f ) * 
-                   2.0f * fabsf(animator->z*2.0f-1.0f);
-
-         f32 d = (hit.dist - 0.3f) * amt;
-         pose->keyframes[hand_id-1].co[1] -= d;
-         kf_hip->co[1] -= d*0.4f;
-      }
-
-      /* skid */
-      f32 amt = vg_maxf(0.0f, (animator->slide - 0.5f) * 2.0f);
-      u8 skidders[] = { localplayer.id_ik_foot_l, 
-                        localplayer.id_ik_foot_r,
-                        localplayer.id_board };
-      v4f qskid;
-      q_axis_angle( qskid, (v3f){0,1,0}, -animator->steer[1]*0.2f );
-
-      for( u32 i=0; i<VG_ARRAY_LEN(skidders); i ++ ){
-         mdl_keyframe *kf = &pose->keyframes[ skidders[i]-1 ];
-         keyframe_rotate_around( kf, 
-               (v3f){0,0,0.4f*(animator->z*2.0f-1.0f)*amt}, 
-                                 sk->bones[skidders[i]].co, qskid );
-      }
-   }
-#endif
-}
-
-void player__skate_effects( void *_animator, m4x3f *final_mtx,
-                                   struct player_board *board,
-                                   struct player_effects_data *effect_data ){
-   struct skeleton *sk = &localplayer.skeleton;
-   struct player_skate_animator *animator = _animator;
-
-   v3f vp0, vp1, vpc;
-   if( board ){
-      v3_copy((v3f){0.0f,0.02f, board->truck_positions[0][2]}, vp1 );
-      v3_copy((v3f){0.0f,0.02f, board->truck_positions[1][2]}, vp0 );
-   }
-   else{
-      v3_zero( vp0 );
-      v3_zero( vp1 );
-   }
-
-   v3f *board_mtx = final_mtx[ localplayer.id_board ];
-   m4x3_mulv( board_mtx, vp0, vp0 );
-   m4x3_mulv( board_mtx, vp1, vp1 );
-   v3_add( vp0, vp1, vpc );
-   v3_muls( vpc, 0.5f, vpc );
-
-   if( animator->surface == k_surface_prop_sand ){
-      if( (animator->slide>0.4f) && (v3_length2(animator->root_v)>4.0f*4.0f) ){
-         v3f v, co;
-         v3_muls( animator->root_v, 0.5f, v );
-         v3_lerp( vp0, vp1, vg_randf64(&vg.rand), co );
-
-         effect_data->sand.colour = 0xff8ec4e6;
-         effect_spark_apply( &effect_data->sand, co, v, vg.time_delta * 8.0 );
-      }
-   }
-
-   if( animator->grind > 0.5f ){
-      int back = 0, front = 0, mid = 0;
-
-      if( animator->activity == k_skate_activity_grind_5050 ){
-         back = 1;
-         front = 1;
-      }
-      else if( animator->activity == k_skate_activity_grind_back50 ){
-         back = 1;
-      }
-      else if( animator->activity == k_skate_activity_grind_front50 ){
-         front = 1;
-      }
-      else if( animator->activity == k_skate_activity_grind_boardslide ){
-         mid = 1;
-      }
-
-      if( back ){
-         effect_spark_apply( &effect_data->spark, vp0,
-                              animator->root_v, vg.time_delta );
-      }
-
-      if( front ){
-         effect_spark_apply( &effect_data->spark, vp1,
-                              animator->root_v, vg.time_delta );
-      }
-
-      if( mid ){
-         effect_spark_apply( &effect_data->spark, vpc,
-                              animator->root_v, vg.time_delta );
-      }
-   }
-}
-
-void player__skate_post_animate(void){
-   struct player_skate_state *state = &player_skate.state;
-   localplayer.cam_velocity_influence = 1.0f;
-   localplayer.cam_dist = 1.8f;
-
-   v3f head = { 0.0f, 1.8f, 0.0f };
-   m4x3_mulv( localplayer.final_mtx[ localplayer.id_head ], 
-              head, state->head_position );
-   m4x3_mulv( localplayer.rb.to_local, 
-              state->head_position, state->head_position );
-}
-
-void player__skate_reset_animator(void){
-   struct player_skate_state *state = &player_skate.state;
-
-   memset( &player_skate.animator, 0, sizeof(player_skate.animator) );
-
-   if( state->activity <= k_skate_activity_air_to_grind ) 
-      player_skate.animator.fly = 1.0f;
-   else 
-      player_skate.animator.fly = 0.0f;
-}
-
-void player__skate_clear_mechanics(void)
-{
-   struct player_skate_state *state = &player_skate.state;
-   state->jump_charge    = 0.0f;
-   state->charging_jump  = 0;
-   state->jump_dir       = 0;
-   v3_zero( state->flip_axis );
-   state->flip_time      = 0.0f;
-   state->flip_rate      = 0.0f;
-   state->reverse        = 0.0f;
-   state->slip           = 0.0f;
-   state->grabbing       = 0.0f;
-   v2_zero( state->grab_mouse_delta );
-   state->slap           = 0.0f;
-   state->jump_time      = 0.0;
-   state->start_push     = 0.0;
-   state->cur_push       = 0.0;
-   state->air_start      = 0.0;
-
-   v3_zero( state->air_init_v );
-   v3_zero( state->air_init_co );
-
-   state->gravity_bias   = k_gravity;
-   v3_copy( localplayer.rb.co, state->prev_pos );
-   v4_copy( localplayer.rb.q, state->smoothed_rotation );
-   v3_zero( state->throw_v );
-   v3_zero( state->trick_vel );
-   v3_zero( state->trick_euler );
-   v3_zero( state->cog_v );
-   state->grind_cooldown = 0;
-   state->surface_cooldown = 0;
-   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 1.0f, state->cog );
-   v3_copy( localplayer.rb.to_world[1], state->up_dir );
-   v3_copy( localplayer.rb.to_world[1], player_skate.surface_picture );
-   v3_copy( localplayer.rb.co, state->prev_pos );
-   v3_zero( player_skate.weight_distribution );
-
-   v3f head = { 0.0f, 1.8f, 0.0f };
-   m4x3_mulv( localplayer.rb.to_world, head, state->head_position );
-}
-
-#include "network_compression.h"
-
-void player__skate_animator_exchange( bitpack_ctx *ctx, void *data ){
-   struct player_skate_animator *animator = data;
-   
-   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
-   bitpack_qquat( ctx, animator->root_q );
-
-   bitpack_qv3f( ctx, 8, -1.0f, 1.0f, animator->offset );
-   bitpack_qv3f( ctx, 8, -1.0f, 1.0f, animator->local_cog );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->slide );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->z );
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->x );
-
-   /* these could likely be pressed down into single bits if needed */
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->fly );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grind );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->stand );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->push );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->jump );        /*??*/
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->jump_charge ); /*??*/
-
-   /* just the sign bit? */
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->reverse );
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->delayed_slip_dir );
-   bitpack_bytes( ctx, 1, &animator->jump_dir );
-   bitpack_bytes( ctx, 1, &animator->trick_type );
-
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grind_balance );
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->airdir );
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->weight );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->trick_foot );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->slap );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->subslap );
-   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grabbing );
-
-   /* animator->wobble is ommited */
-
-   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->foot_offset );
-   bitpack_qquat( ctx, animator->qfixuptotal );
-   bitpack_qquat( ctx, animator->qflip );
-
-   bitpack_qv3f( ctx, 16, -100.0f, 100.0f, animator->board_euler );
-   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->board_lean );
-   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->steer );
-   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->grab );
-
-   bitpack_qf32( ctx, 16,  0.0f, 120.0f, &animator->push_time );
-   bitpack_qf32( ctx, 16,  0.0f, 120.0f, &animator->jump_time );
-   bitpack_qf32( ctx, 16,  0.0f, 4.0f, &animator->handplant_t );
-   bitpack_qv3f( ctx, 16, -100.0f, 100.0f, animator->root_v );
-   bitpack_bytes( ctx, 1, &animator->activity );
-}
-
-void player__skate_sfx_oneshot( u8 id, v3f pos, f32 volume ){
-   audio_lock();
-
-   if( id == k_player_skate_soundeffect_jump ){
-      audio_oneshot_3d( &audio_jumps[vg_randu32(&vg.rand)%2], 
-                        pos, 40.0f, volume );
-   }
-   else if( id == k_player_skate_soundeffect_tap ){
-      audio_oneshot_3d( &audio_taps[vg_randu32(&vg.rand)%4], 
-                        pos, 40.0f, volume );
-   }
-   else if( id == k_player_skate_soundeffect_land_good ){
-      audio_oneshot_3d( &audio_lands[vg_randu32(&vg.rand)%3], 
-                        pos, 40.0f, volume );
-   }
-   else if( id == k_player_skate_soundeffect_land_bad ){
-      audio_oneshot_3d( &audio_lands[vg_randu32(&vg.rand)%2+3], 
-                        pos, 40.0f, volume );
-   }
-   else if( id == k_player_skate_soundeffect_grind_metal ){
-      audio_oneshot_3d( &audio_board[3], pos, 40.0f, volume );
-   }
-   else if( id == k_player_skate_soundeffect_grind_wood ){
-      audio_oneshot_3d( &audio_board[8], pos, 40.0f, volume );
-   }
-
-   audio_unlock();
-}
diff --git a/player_skate.h b/player_skate.h
deleted file mode 100644 (file)
index 8adb024..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-#pragma once
-#include "vg/vg_audio.h"
-#include "player.h"
-#include "player_api.h"
-
-typedef struct jump_info jump_info;
-
-struct player_skate{
-   struct player_skate_state{
-      enum skate_activity{
-         k_skate_activity_air,
-         k_skate_activity_air_to_grind,
-         k_skate_activity_ground,
-         k_skate_activity_handplant,
-         k_skate_activity_undefined,
-         k_skate_activity_grind_any,
-         k_skate_activity_grind_boardslide,
-         k_skate_activity_grind_metallic,
-         k_skate_activity_grind_back50,
-         k_skate_activity_grind_front50,
-         k_skate_activity_grind_5050
-      }
-      activity,
-      activity_prev;
-
-      u32 grind_cooldown,
-          surface_cooldown;
-
-      f32 reverse, slip, delayed_slip_dir;
-      int manual_direction;
-
-      /* tricks */
-      v3f   flip_axis;
-      float flip_time,
-            flip_rate;
-
-      v3f   trick_vel,     /* measured in units of TAU/s */
-            trick_euler;   /* measured in units of TAU */
-      v3f trick_residualv, /* spring */
-          trick_residuald;
-
-      float trick_time;
-      enum  trick_type{
-         k_trick_type_none,
-         k_trick_type_kickflip,
-         k_trick_type_shuvit,
-         k_trick_type_treflip,
-      }
-      trick_type;
-      float gravity_bias;
-
-      f32 trick_input_collect;
-
-      v3f up_dir;
-      v3f head_position;
-
-      v3f throw_v;
-      v3f cog_v, cog;
-
-      float grabbing;
-      v2f grab_mouse_delta;
-
-      int charging_jump, jump_dir;
-      float jump_charge,
-            slap;
-
-      double jump_time;
-      double start_push,
-             cur_push;
-
-      v3f prev_pos;
-
-      /* initial launch conditions */
-      double air_start;
-      v3f    air_init_v,
-             air_init_co;
-
-      float land_dist;
-      v3f land_normal;
-      v4f smoothed_rotation;
-
-      f32 velocity_limit, grind_y_start, skid;
-      f32 handplant_t;
-
-      v3f store_cog_v, store_cog, store_co;
-      v4f store_smoothed, store_q;
-   }
-   state;
-
-   struct player_skate_animator {
-      v3f root_co;
-      v4f root_q;
-      v3f root_v;
-
-      v3f offset,
-          local_cog;
-
-      f32 slide,
-          skid,
-          z,
-          x,
-          fly,
-          grind,
-          grind_balance,
-          stand,
-          push,
-          jump,
-          airdir,
-          weight,
-          trick_foot,
-          slap,
-          subslap,
-          reverse,
-          delayed_slip_dir,
-          grabbing;
-
-      v2f wobble;
-      f32 foot_offset[2];
-
-      v4f qfixuptotal;
-      v4f qflip;
-
-      v3f board_euler;
-      f32 board_lean;
-      v2f steer, grab;
-
-      f32 jump_charge;
-
-      /* linear anims. TODO: we can union a bunch of variables here depending
-       * on activity. */
-      f32 push_time, jump_time, handplant_t;
-      u8 jump_dir;
-      u8 trick_type; /* todo: should encode grind type */
-      u8 activity, surface;
-   }
-   animator;
-
-   f32 collect_feedback;
-
-   /* animation /audio
-    * --------------------------------------------------------------*/
-   struct skeleton_anim *anim_stand, *anim_highg, *anim_slide,
-                        *anim_air, *anim_grind, *anim_grind_jump,
-                        *anim_push,  *anim_push_reverse,
-                        *anim_ollie, *anim_ollie_reverse,
-                        *anim_grabs, *anim_stop,
-                        *anim_handplant;
-
-   /* vectors representing the direction of the axels in localspace */
-   v3f truckv0[2];
-
-   audio_channel *aud_main, *aud_slide, *aud_air;
-   enum mdl_surface_prop surface, audio_surface;
-
-   int wheel_contacts[2];
-   float sample_change_cooldown;
-
-   enum {
-      k_skate_sample_concrete,
-      k_skate_sample_wood,
-      k_skate_sample_concrete_scrape_metal,
-      k_skate_sample_concrete_scrape_wood,
-      k_skate_sample_metal_scrape_generic
-   }
-   main_sample_type;
-
-   /*
-    * Physics 
-    * ----------------------------------------------------
-    */
-
-   float substep, substep_delta;
-
-   struct jump_info{
-      v3f   log[50];
-      v3f   n;
-      v3f   apex;
-      v3f   v;
-
-      float gravity;
-
-      int   log_length;
-      float score,
-            land_dist;
-
-      enum prediction_type{
-         k_prediction_none,
-         k_prediction_unset,
-         k_prediction_land,
-         k_prediction_grind
-      }
-      type;
-
-      u32   colour;
-   }
-   possible_jumps[36];
-   u32 possible_jump_count;
-
-   v3f surface_picture,
-       weight_distribution,
-       grind_vec,
-       grind_dir;
-
-   float grind_strength;
-   struct grind_limit{
-      v3f ra, n;
-      float p;
-   }
-   limits[3];
-   u32 limit_count;
-}
-extern player_skate;
-extern struct player_subsystem_interface player_subsystem_skate;
-
-enum player_skate_soundeffect {
-   k_player_skate_soundeffect_jump,
-   k_player_skate_soundeffect_tap,
-   k_player_skate_soundeffect_land_good,
-   k_player_skate_soundeffect_land_bad,
-   k_player_skate_soundeffect_grind_metal,
-   k_player_skate_soundeffect_grind_wood,
-};
-
-static float 
-   k_friction_lat          = 12.0f,
-   k_friction_resistance   = 0.01f,
-
-   k_max_push_speed        = 16.0f,
-   k_push_accel            = 10.0f,
-   k_push_cycle_rate       = 8.0f,
-
-   k_steer_ground          = 2.5f,
-   k_steer_air             = 3.6f,
-
-   k_jump_charge_speed     = (1.0f/0.4f),
-   k_jump_force            = 5.0f,
-
-   k_cog_spring            = 0.2f,
-   k_cog_damp              = 0.02f,
-   k_cog_mass_ratio        = 0.9f,
-
-   k_mmthrow_steer         = 1.0f,
-   k_mmthrow_scale         = 6.0f,
-   k_mmcollect_lat         = 2.0f,
-   k_mmcollect_vert        = 0.0f,
-   k_mmdecay               = 12.0f,
-   k_spring_angular        = 1.0f,
-
-   k_spring_force          = 300.0f,
-   k_spring_dampener       = 5.0f,
-
-   k_grind_spring          = 50.0f,
-   k_grind_aligment        = 10.0f,
-   k_grind_dampener        = 5.0f,
-
-   k_surface_spring        = 100.0f,
-   k_surface_dampener      = 40.0f,
-   k_manul_spring          = 200.0f,
-   k_manul_dampener        = 30.0f,
-   k_board_interia         = 8.0f,
-
-   k_grind_decayxy         = 30.0f,
-   k_grind_axel_min_vel    = 1.0f,
-   k_grind_axel_max_angle  = 0.95f, /* cosine(|a|) */
-   k_grind_axel_max_vangle = 0.4f,
-   k_grind_max_friction    = 3.0f,
-   k_grind_max_edge_angle  = 0.97f,
-
-   k_board_length          = 0.45f,
-   k_board_width           = 0.13f,
-   k_board_end_radius      = 0.1f,
-   k_board_radius          = 0.14f,    /* 0.07 */
-   
-   k_grind_balance         = -40.0f,
-   k_anim_transition       = 0.12f;
-
-static void player__skate_register(void)
-{
-   VG_VAR_F32( k_grind_dampener,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_spring,         flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_aligment,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_surface_spring,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_surface_dampener,     flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_board_interia,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_decayxy,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_axel_min_vel,   flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_axel_max_angle, flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_max_friction,   flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_grind_balance,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_friction_lat,         flags=VG_VAR_CHEAT );
-
-   VG_VAR_F32( k_cog_spring,           flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_cog_damp,             flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_cog_mass_ratio,       flags=VG_VAR_CHEAT );
-
-   VG_VAR_F32( k_spring_force,         flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_spring_dampener,      flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_spring_angular,       flags=VG_VAR_CHEAT );
-
-   VG_VAR_F32( k_mmthrow_scale,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_mmcollect_lat,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_mmcollect_vert,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_mmdecay,              flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_mmthrow_steer,        flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_anim_transition,      flags=VG_VAR_CHEAT );
-}
-
-void player__skate_bind         (void);
-void player__skate_pre_update   (void);
-void player__skate_update       (void);
-void player__skate_post_update  (void);
-void player__skate_im_gui       ( ui_context *ctx );
-void player__skate_animate      (void);
-void player__skate_pose         (void *animator, player_pose *pose);
-void player__skate_effects( void *_animator, m4x3f *final_mtx,
-                            struct player_board *board,
-                            struct player_effects_data *effect_data );
-void player__skate_post_animate (void);
-void player__skate_animator_exchange( bitpack_ctx *ctx, void *data );
-void player__skate_sfx_oneshot  ( u8 id, v3f pos, f32 volume );
-
-void player__skate_clear_mechanics(void);
-void player__skate_reset_animator(void);
-void player__approximate_best_trajectory(void);
-void player__skate_comp_audio( void *animator );
-void player__skate_kill_audio(void);
diff --git a/player_walk.c b/player_walk.c
deleted file mode 100644 (file)
index 1e15afc..0000000
+++ /dev/null
@@ -1,1210 +0,0 @@
-#include "vg/vg_rigidbody_collision.h"
-
-#include "skaterift.h"
-#include "player_walk.h"
-#include "player_skate.h"
-#include "player_dead.h"
-#include "player.h"
-#include "input.h"
-#include "audio.h"
-#include "scene_rigidbody.h"
-
-struct player_walk player_walk;
-struct player_subsystem_interface player_subsystem_walk = 
-{
-   .system_register = player__walk_register,
-   .bind = player__walk_bind,
-   .pre_update = player__walk_pre_update,
-   .update = player__walk_update,
-   .post_update = player__walk_post_update,
-   .im_gui = player__walk_im_gui,
-   .animate = player__walk_animate,
-   .post_animate = player__walk_post_animate,
-   .pose = player__walk_pose,
-   .network_animator_exchange = player__walk_animator_exchange,
-   .sfx_oneshot = player__walk_sfx_oneshot,
-
-   .animator_data = &player_walk.animator,
-   .animator_size = sizeof(player_walk.animator),
-   .name = "Walk"
-};
-
-
-static void player_walk_drop_in_vector( v3f vec ){
-   v3f axis, init_dir;
-   v3_cross( (v3f){0.0f,1.0f,0.0f}, player_walk.state.drop_in_normal, axis );
-   v3_cross( axis, player_walk.state.drop_in_normal, init_dir );
-   v3_normalize( init_dir );
-   v3_muls( init_dir, 4.25f, vec );
-}
-
-static float player_xyspeed2(void){
-   return v3_length2( (v3f){localplayer.rb.v[0], 0.0f, localplayer.rb.v[2]} );
-}
-
-static void player_walk_generic_to_skate( enum skate_activity init, f32 yaw ){
-   localplayer.subsystem = k_player_subsystem_skate;
-
-   v3f v;
-
-   if( player_xyspeed2() < 0.1f * 0.1f )
-      q_mulv( localplayer.rb.q, (v3f){0.0f,0.0f,1.6f}, v );
-   else
-      v3_copy( localplayer.rb.v, v );
-
-   player_skate.state.activity_prev = k_skate_activity_ground;
-   player_skate.state.activity = init;
-
-   v3f dir;
-   v3_copy( v, dir );
-   v3_normalize( dir );
-
-   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
-                 atan2f(-dir[0],-dir[2]) );
-   q_normalize( localplayer.rb.q );
-
-   q_mulv( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, player_skate.state.cog );
-   v3_add( player_skate.state.cog, localplayer.rb.co, player_skate.state.cog );
-
-   v3_copy( v, player_skate.state.cog_v );
-   v3_copy( v, localplayer.rb.v );
-
-   player__begin_holdout( (v3f){0.0f,0.0f,0.0f} );
-   player__skate_reset_animator();
-   player__skate_clear_mechanics();
-   rb_update_matrices( &localplayer.rb );
-   v3_copy( (v3f){yaw,0.0f,0.0f}, player_skate.state.trick_euler );
-
-   if( init == k_skate_activity_air )
-      player__approximate_best_trajectory();
-}
-
-static void player_walk_drop_in_to_skate(void){
-   localplayer.immobile = 0;
-   localplayer.subsystem = k_player_subsystem_skate;
-
-   player_skate.state.activity_prev = k_skate_activity_ground;
-   player_skate.state.activity = k_skate_activity_ground;
-
-   player__begin_holdout( (v3f){0,0,0} );
-   player__skate_clear_mechanics();
-   player__skate_reset_animator();
-
-   v3f init_velocity;
-   player_walk_drop_in_vector( init_velocity );
-
-   rb_update_matrices( &localplayer.rb );
-   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 1.0f, 
-               player_skate.state.cog );
-   v3_copy( init_velocity, player_skate.state.cog_v );
-   v3_copy( init_velocity, localplayer.rb.v );
-   v3_copy( init_velocity, localplayer.cam_control.cam_velocity_smooth );
-   v3_copy( (v3f){player_walk.animator.board_yaw+1.0f,0,0}, 
-            player_skate.state.trick_euler );
-}
-
-static void player_walk_drop_in_overhang_transform( f32 t, v3f co, v4f q ){
-   v3f axis;
-   v3_cross( (v3f){0,1,0}, player_walk.state.drop_in_normal, axis );
-   v3_normalize( axis );
-
-   float a = acosf( player_walk.state.drop_in_normal[1] ) * t;
-   q_axis_angle( q, axis, a );
-
-   float l = t * 0.5f,
-         heading_angle = player_walk.state.drop_in_angle;
-
-   v3f overhang;
-   overhang[0] = sinf( heading_angle ) * l;
-   overhang[1] = 0.28f * l;
-   overhang[2] = cosf( heading_angle ) * l;
-
-   q_mulv( q, overhang, overhang );
-   v3_add( player_walk.state.drop_in_target, overhang, co );
-}
-
-static int player_walk_scan_for_drop_in(void){
-   world_instance *world = world_current_instance();
-
-   v3f dir, center;
-   q_mulv( localplayer.rb.q, (v3f){0.0f,0.0f,1.0f}, dir );
-   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], -1.0f, center );
-
-   ray_hit samples[20];
-   int sample_count = 0;
-
-   for( int i=0; i<20; i ++ ){
-      float t = (float)i * (1.0f/19.0f),
-            s = sinf( t * VG_PIf * 0.25f ),
-            c = cosf( t * VG_PIf * 0.25f );
-
-      v3f ray_dir, pos;
-      v3_muls   ( localplayer.rb.to_world[1], -c, ray_dir );
-      v3_muladds( ray_dir, dir, -s, ray_dir );
-      v3_muladds( center, ray_dir, -2.0f, pos );
-
-      ray_hit *ray = &samples[ sample_count ];
-      ray->dist = 2.0f;
-
-      if( ray_world( world, pos, ray_dir, ray, 0 ) ){
-         vg_line( pos, ray->pos, VG__RED );
-         vg_line_point( ray->pos, 0.025f, VG__BLACK );
-         
-         sample_count ++;
-      }
-   }
-
-   float min_a = 0.70710678118654752f;
-   ray_hit *candidate = NULL;
-
-   if( sample_count >= 2 ){
-      for( int i=0; i<sample_count-1; i++ ){
-         ray_hit *s0 = &samples[i],
-                 *s1 = &samples[i+1];
-
-         float a = v3_dot( s0->normal, s1->normal );
-
-         if( (a < min_a) && (a >= -0.1f) && (s0->normal[1]>s1->normal[1]) ){
-            min_a = a;
-            candidate = s0;
-         }
-      }
-   }
-
-   if( candidate ){
-      v4f pa, pb, pc;
-      
-      ray_hit *s0 = candidate,
-              *s1 = candidate+1;
-
-      vg_line( s0->pos, s1->pos, VG__WHITE );
-
-      v3_copy( s0->normal, pa );
-      v3_copy( s1->normal, pb );
-      v3_cross( localplayer.rb.to_world[1], dir, pc );
-      v3_normalize( pc );
-
-      pa[3] = v3_dot( pa, s0->pos );
-      pb[3] = v3_dot( pb, s1->pos );
-      pc[3] = v3_dot( pc, localplayer.rb.co );
-
-      v3f edge;
-      if( plane_intersect3( pa, pb, pc, edge ) ){
-         v3_copy( edge, player_walk.state.drop_in_target );
-         v3_copy( s1->normal, player_walk.state.drop_in_normal );
-         v3_copy( localplayer.rb.co, player_walk.state.drop_in_start );
-
-         player_walk.state.drop_in_start_angle = player_get_heading_yaw();
-         player_walk.state.drop_in_angle = 
-            atan2f( player_walk.state.drop_in_normal[0],
-                    player_walk.state.drop_in_normal[2] );
-
-         /* TODO: scan multiple of these? */
-         v3f oco;
-         v4f oq;
-         player_walk_drop_in_overhang_transform( 1.0f, oco, oq );
-
-         v3f va = {0.0f,0.0f,-k_board_length - 0.3f},
-             vb = {0.0f,0.0f, k_board_length + 0.3f};
-
-         q_mulv( oq, va, va );
-         q_mulv( oq, vb, vb );
-         v3_add( oco, va, va );
-         v3_add( oco, vb, vb );
-
-         v3f v0;
-         v3_sub( vb, va, v0 );
-         v3_normalize( v0 );
-
-         ray_hit ray;
-         ray.dist = k_board_length*2.0f + 0.6f;
-         
-         if( ray_world( world, va, v0, &ray, 0 ) ){
-            vg_line( va, vb, VG__RED );
-            vg_line_point( ray.pos, 0.1f, VG__RED );
-            vg_error( "invalidated\n" );
-            return 0;
-         }
-
-         v3_muls( v0, -1.0f, v0 );
-         if( ray_world( world, vb, v0, &ray, 0 ) ){
-            vg_line( va, vb, VG__RED );
-            vg_line_point( ray.pos, 0.1f, VG__RED );
-            vg_error( "invalidated\n" );
-            return 0;
-         }
-
-         player_walk_drop_in_vector( localplayer.rb.v );
-         return 1;
-      }
-      else{
-         vg_error( "failed to find intersection of drop in\n" );
-      }
-   }
-
-   return 0;
-}
-
-static bool player__preupdate_anim( struct skeleton_anim *anim, f32 *t, 
-                                    f32 speed ){
-   f32 length = (f32)(anim->length-1) / anim->rate;
-   *t += (vg.time_delta * speed) / length;
-
-   if( *t >= 1.0f ) return 1;
-   else             return 0;
-}
-
-static void player_walk_pre_sit(void){
-   struct player_walk *w = &player_walk;
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-
-   vg_slewf( &w->state.transition_t, 1.0f, vg.time_delta );
-
-   if( button_down(k_srbind_sit) || (v2_length2(steer)>0.2f) ||
-       button_down(k_srbind_jump) ){
-      w->state.activity = k_walk_activity_sit_up;
-   }
-   return;
-}
-
-static void player_walk_pre_sit_up(void){
-   struct player_walk *w = &player_walk;
-   
-   if( w->state.transition_t > 0.0f )
-      vg_slewf( &w->state.transition_t, 0.0f, vg.time_delta );
-   else 
-      w->state.activity = k_walk_activity_ground;
-
-   if( button_down(k_srbind_sit) )
-      w->state.activity = k_walk_activity_sit;
-
-   return;
-}
-
-static void player_walk_pre_ground(void){
-   struct player_walk *w = &player_walk;
-
-   if( button_down(k_srbind_sit) ){
-      v3_zero( localplayer.rb.v );
-      w->state.activity = k_walk_activity_sit;
-      w->state.transition_t = 0.0f;
-      return;
-   }
-
-   if( button_down( k_srbind_use ) ){
-      if( player_walk_scan_for_drop_in() ){
-         w->state.activity = k_walk_activity_odrop_in;
-      }
-      else{
-         w->state.activity = k_walk_activity_oregular;
-      }
-
-      w->state.transition_t = 0.0f;
-   }
-
-   if( button_down( k_srbind_jump ) ){
-      w->state.jump_queued = 1;
-      w->state.jump_input_time = vg.time;
-   }
-}
-
-static void player_walk_pre_air(void){
-   struct player_walk *w = &player_walk;
-   if( button_down( k_srbind_use ) ){
-      w->state.activity = k_walk_activity_oair;
-      w->state.transition_t = 0.0f;
-   }
-
-   if( button_down( k_srbind_jump ) ){
-      w->state.jump_queued = 1;
-      w->state.jump_input_time = vg.time;
-   }
-}
-
-static void player_walk_pre_drop_in(void){
-   struct player_walk *w = &player_walk;
-   bool finished = player__preupdate_anim( w->anim_drop_in, 
-                                          &w->state.transition_t, 1.0f );
-   if( finished )
-      player_walk_drop_in_to_skate();
-}
-
-static void player_walk_pre_caveman(void){
-   struct player_walk *w = &player_walk;
-   bool finished = player__preupdate_anim( w->anim_jump_to_air,
-                                          &w->state.transition_t, 1.0f );
-   if( finished ){
-      player_walk_generic_to_skate( k_skate_activity_air, 
-                                    player_walk.animator.board_yaw );
-   }
-}
-
-static void player_walk_pre_running_start(void){
-   struct player_walk *w = &player_walk;
-   bool finished = player__preupdate_anim( w->anim_intro, 
-                                          &w->state.transition_t, 1.0f );
-   if( finished ){
-      /* TODO: get the derivative of the last keyframes to calculate new
-       * velocity for player */
-      player_walk_generic_to_skate( k_skate_activity_ground, 
-                                    player_walk.animator.board_yaw+1.0f );
-   }
-}
-
-static void player_walk_pre_popoff(void){
-   struct player_walk *w = &player_walk;
-   bool finished = player__preupdate_anim( w->anim_popoff, 
-                                          &w->state.transition_t, 1.0f );
-
-   if( finished ){
-      w->state.activity = k_walk_activity_ground;
-      w->animator.board_yaw += 1.0f;
-   }
-}
-
-void player__walk_pre_update(void){
-   struct player_walk *w = &player_walk;
-
-   if( localplayer.immobile ) return;
-   else player_look( localplayer.angles, skaterift.time_rate );
-
-   enum walk_activity a = w->state.activity;
-
-   if     ( a == k_walk_activity_sit )       player_walk_pre_sit();
-   else if( a == k_walk_activity_sit_up )    player_walk_pre_sit_up();
-   else if( a == k_walk_activity_ground )    player_walk_pre_ground();
-   else if( a == k_walk_activity_air )       player_walk_pre_air();
-   else if( a == k_walk_activity_odrop_in )  player_walk_pre_drop_in();
-   else if( a == k_walk_activity_oair )      player_walk_pre_caveman();
-   else if( a == k_walk_activity_oregular )  player_walk_pre_running_start();
-   else if( a == k_walk_activity_ipopoff )   player_walk_pre_popoff();
-}
-
-static int player_walk_normal_standable( v3f n ){
-   return n[1] > 0.70710678118f;
-}
-
-static void player_accelerate( v3f v, v3f movedir, f32 speed, f32 accel ){
-   float currentspeed = v3_dot( v, movedir ),
-         addspeed     = speed - currentspeed;
-
-   if( addspeed <= 0 )
-      return;
-
-   float accelspeed = accel * vg.time_fixed_delta * speed;
-
-   if( accelspeed > addspeed )
-      accelspeed = addspeed;
-
-   v3_muladds( v, movedir, accelspeed, v );
-}
-
-static void player_friction( v3f v, f32 friction ){
-   float speed = v3_length( v ),
-         drop  = 0.0f,
-         control = vg_maxf( speed, k_stopspeed );
-
-   if( speed < 0.04f )
-      return;
-
-   drop += control * friction * vg.time_fixed_delta;
-
-   float newspeed = vg_maxf( 0.0f, speed - drop );
-   newspeed /= speed;
-
-   v3_muls( v, newspeed, v );
-}
-
-static void player_walk_custom_filter( world_instance *world,
-                                          rb_ct *man, int len, f32 w ){
-   for( int i=0; i<len; i++ ){
-      rb_ct *ci = &man[i];
-      if( ci->type == k_contact_type_disabled ||
-          ci->type == k_contact_type_edge ) 
-         continue;
-
-
-      float d1 = v3_dot( ci->co, ci->n );
-
-      for( int j=0; j<len; j++ ){
-         if( j == i )
-            continue;
-
-         rb_ct *cj = &man[j];
-         if( cj->type == k_contact_type_disabled ) 
-            continue;
-
-         struct world_surface *si = world_contact_surface( world, ci ),
-                              *sj = world_contact_surface( world, cj );
-
-         if(  (sj->info.flags & k_material_flag_walking) &&
-             !(si->info.flags & k_material_flag_walking)){
-            continue;
-         }
-         
-         float d2 = v3_dot( cj->co, ci->n ),
-               d  = d2-d1;
-
-         if( fabsf( d ) <= w ){
-            cj->type = k_contact_type_disabled;
-         }
-      }
-   }
-}
-
-static void player_walk_update_generic(void){
-   struct player_walk *w = &player_walk;
-
-   if( (w->state.activity != k_walk_activity_oregular) &&
-       (w->state.activity != k_walk_activity_oair) ){
-      joystick_state( k_srjoystick_steer, w->state.steer );
-      w->state.steer[2] = button_press(k_srbind_run)? k_runspeed: k_walkspeed;
-      if( v2_length2(w->state.steer)>1.0f )
-         v2_normalize(w->state.steer);
-   }
-
-   v3_copy( localplayer.rb.co, w->state.prev_pos );
-   v3_zero( localplayer.rb.w );
-
-   world_instance *world = world_current_instance();
-   if( !world_water_player_safe( world, 0.4f ) ) return;
-
-   enum walk_activity prev_state = w->state.activity;
-
-   w->collider.h = 2.0f;
-   w->collider.r = 0.3f;
-
-   m4x3f mtx;
-   m3x3_copy( localplayer.rb.to_world, mtx );
-   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
-
-   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__WHITE );
-
-   rb_ct manifold[64];
-   int len;
-
-   float yaw = localplayer.angles[0];
-
-   v3f forward_dir = { -sinf(yaw),         0.0f,  cosf(yaw) };
-   v3f right_dir   = {  forward_dir[2],    0.0f, -forward_dir[0] };
-
-   /* 
-    * Collision detection
-    */
-
-   len = rb_capsule__scene( mtx, &w->collider, NULL, 
-                            world->geo_bh, manifold, 0 );
-   player_walk_custom_filter( world, manifold, len, 0.01f );
-   len = rb_manifold_apply_filtered( manifold, len );
-
-   v3f surface_avg = { 0.0f, 0.0f, 0.0f };
-
-   w->state.activity = k_walk_activity_air;
-   w->surface = k_surface_prop_concrete;
-
-   for( int i=0; i<len; i++ ){
-      rb_ct *ct = &manifold[i];
-      rb_debug_contact( ct );
-
-      if( player_walk_normal_standable( ct->n ) ){
-         w->state.activity = k_walk_activity_ground;
-
-         v3_add( surface_avg, ct->n, surface_avg );
-
-         struct world_surface *surf = world_contact_surface( world, ct );
-         if( surf->info.surface_prop > w->surface )
-            w->surface = surf->info.surface_prop;
-      }
-
-      rb_prepare_contact( ct, vg.time_fixed_delta );
-   }
-
-   /* 
-    * Move & Friction
-    */
-   float accel_speed = 0.0f, nominal_speed = 0.0f;
-   v3f movedir;
-
-   v3_muls( right_dir, w->state.steer[0], movedir );
-   v3_muladds( movedir, forward_dir, w->state.steer[1], movedir );
-
-   if( w->state.activity == k_walk_activity_ground ){
-      v3_normalize( surface_avg );
-
-      v3f tx, ty;
-      v3_tangent_basis( surface_avg, tx, ty );
-
-      if( v2_length2(w->state.steer) > 0.001f ){
-         /* clip movement to the surface */
-         float d = v3_dot(surface_avg,movedir);
-         v3_muladds( movedir, surface_avg, -d, movedir );
-      }
-
-      accel_speed = k_walk_accel;
-      nominal_speed = w->state.steer[2];
-
-      /* jump */
-      if( w->state.jump_queued ){
-         w->state.jump_queued = 0;
-
-         f32 t = vg.time - w->state.jump_input_time;
-         if( t < PLAYER_JUMP_EPSILON ){
-            localplayer.rb.v[1] = 5.0f;
-            w->state.activity = k_walk_activity_air;
-            prev_state = k_walk_activity_air;
-            accel_speed = k_walk_air_accel;
-            nominal_speed = k_airspeed;
-         }
-      }
-      else{
-         player_friction( localplayer.rb.v, k_walk_friction );
-      }
-   }
-   else{
-      accel_speed = k_walk_air_accel;
-      nominal_speed = k_airspeed;
-   }
-
-   if( v2_length2( w->state.steer ) > 0.001f ){
-      player_accelerate( localplayer.rb.v, movedir, 
-                         nominal_speed, accel_speed );
-      v3_normalize( movedir );
-   }
-   
-   /*
-    * Resolve velocity constraints
-    */
-   for( int j=0; j<5; j++ ){
-      for( int i=0; i<len; i++ ){
-         rb_ct *ct = &manifold[i];
-         
-         /*normal */
-         float vn = -v3_dot( localplayer.rb.v, ct->n );
-
-         float temp = ct->norm_impulse;
-         ct->norm_impulse = vg_maxf( temp + vn, 0.0f );
-         vn = ct->norm_impulse - temp;
-
-         v3_muladds( localplayer.rb.v, ct->n, vn, localplayer.rb.v );
-      }
-   }
-
-   /* stepping */
-   if( w->state.activity == k_walk_activity_ground||
-       prev_state == k_walk_activity_ground ){
-      float max_dist = 0.4f;
-
-      v3f pa, pb;
-      v3_copy( localplayer.rb.co, pa );
-      pa[1] += w->collider.r + max_dist;
-      v3_add( pa, (v3f){0, -max_dist * 2.0f, 0}, pb );
-      vg_line( pa, pb, 0xff000000 );
-
-      v3f n;
-      float t;
-      if( spherecast_world( world, pa, pb, 
-                            w->collider.r, &t, n, 0 ) != -1 ){
-         if( player_walk_normal_standable(n) ){
-            v3_lerp( pa, pb, t, localplayer.rb.co );
-            localplayer.rb.co[1] += -w->collider.r - k_penetration_slop;
-            w->state.activity = k_walk_activity_ground;
-
-            float d = -v3_dot(n,localplayer.rb.v);
-            v3_muladds( localplayer.rb.v, n, d, localplayer.rb.v );
-            localplayer.rb.v[1] += -k_gravity * vg.time_fixed_delta;
-         }
-      }
-   }
-
-   /* 
-    * Depenetrate
-    */
-   v3f dt;
-   rb_depenetrate( manifold, len, dt );
-   v3_add( dt, localplayer.rb.co, localplayer.rb.co );
-
-   /* integrate */
-   if( w->state.activity == k_walk_activity_air ){
-      localplayer.rb.v[1] += -k_gravity*vg.time_fixed_delta;
-   }
-
-   if( localplayer.immobile ){
-      localplayer.rb.v[0] = 0.0f;
-      localplayer.rb.v[2] = 0.0f;
-   }
-
-   v3_muladds( localplayer.rb.co, localplayer.rb.v, vg.time_fixed_delta, 
-               localplayer.rb.co );
-   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
-   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__GREEN );
-
-   /* 
-    * CCD routine 
-    * ---------------------------------------------------
-    *
-    */
-   v3f lwr_prev,
-       lwr_now,
-       lwr_offs = { 0.0f, w->collider.r, 0.0f };
-
-   v3_add( lwr_offs, w->state.prev_pos, lwr_prev );
-   v3_add( lwr_offs, localplayer.rb.co, lwr_now );
-
-   v3f movedelta;
-   v3_sub( localplayer.rb.co, w->state.prev_pos, movedelta );
-
-   float movedist = v3_length( movedelta );
-
-   if( movedist > 0.3f ){
-      float t, sr = w->collider.r-0.04f;
-      v3f n;
-
-      if( spherecast_world( world, lwr_prev, lwr_now, sr, &t, n, 0 ) != -1 ){
-         v3_lerp( lwr_prev, lwr_now, vg_maxf(0.01f,t), localplayer.rb.co );
-         localplayer.rb.co[1] -= w->collider.r;
-         rb_update_matrices( &localplayer.rb );
-         v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
-         vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__RED);
-      }
-   }
-
-   u32 id = world_intersect_gates(world, localplayer.rb.co, w->state.prev_pos);
-   if( id ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
-      m4x3_mulv( gate->transport, localplayer.rb.co, localplayer.rb.co );
-      m3x3_mulv( gate->transport, localplayer.rb.v,  localplayer.rb.v );
-
-      v4f transport_rotation;
-      m3x3_q( gate->transport, transport_rotation );
-      q_mul( transport_rotation, localplayer.rb.q, localplayer.rb.q );
-      q_normalize( localplayer.rb.q );
-      rb_update_matrices( &localplayer.rb );
-      player__pass_gate( id );
-   }
-   rb_update_matrices( &localplayer.rb );
-
-   if( (prev_state == k_walk_activity_oregular) ||
-       (prev_state == k_walk_activity_oair) ||
-       (prev_state == k_walk_activity_ipopoff) ){
-      w->state.activity = prev_state;
-   }
-
-   w->move_speed = vg_minf( v2_length( (v2f){ localplayer.rb.v[0], 
-                                              localplayer.rb.v[2] } ),
-                            k_runspeed );
-}
-
-void player__walk_post_update(void){
-   struct player_walk *w = &player_walk;
-
-   m4x3f mtx;
-   m3x3_copy( localplayer.rb.to_world, mtx );
-   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
-
-   float substep = vg.time_fixed_extrapolate;
-   v3_muladds( mtx[3], localplayer.rb.v, vg.time_fixed_delta*substep, mtx[3] );
-   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__YELOW );
-
-   /* Calculate header */
-   v3f v;
-   if( (player_xyspeed2() > 0.1f*0.1f) ){
-      f32 r = 0.3f;
-      if( (w->state.activity == k_walk_activity_ground) ||
-          (w->state.activity == k_walk_activity_ipopoff) ||
-          (w->state.activity == k_walk_activity_oregular) ){
-         r = 0.07f;
-      }
-
-      f32 ta = atan2f( localplayer.rb.v[0], localplayer.rb.v[2] );
-      v4f qt;
-      q_axis_angle( qt, (v3f){0,1,0}, ta );
-      q_nlerp( localplayer.rb.q, qt, vg.time_delta/r, localplayer.rb.q );
-   }
-
-   vg_line_point( w->state.drop_in_target, 0.1f, VG__GREEN );
-   v3f p1;
-   v3_muladds( w->state.drop_in_target, w->state.drop_in_normal, 0.3f, p1 );
-   vg_line( w->state.drop_in_target, p1, VG__GREEN );
-   v3_muladds( w->state.drop_in_target, localplayer.rb.to_world[1], 0.3f, p1 );
-   vg_line( w->state.drop_in_target, p1, VG__GREEN );
-
-   float a = player_get_heading_yaw();
-   p1[0] = sinf( a );
-   p1[1] = 0.0f;
-   p1[2] = cosf( a );
-
-   v3_add( localplayer.rb.co, p1, p1 );
-   vg_line( localplayer.rb.co, p1, VG__PINK );
-
-   int walk_phase = 0;
-   if( vg_fractf(w->state.walk_timer) > 0.5f )
-      walk_phase = 1;
-   else
-      walk_phase = 0;
-
-   if( (w->state.step_phase != walk_phase) && 
-       (w->state.activity == k_walk_activity_ground ) )
-   {
-      audio_lock();
-      if( w->surface == k_surface_prop_concrete ){
-         audio_oneshot_3d( 
-               &audio_footsteps[vg_randu32(&vg.rand) % 4],
-               localplayer.rb.co, 40.0f, 1.0f 
-         );
-      }
-      else if( w->surface == k_surface_prop_grass ){
-         audio_oneshot_3d( 
-           &audio_footsteps_grass[ vg_randu32(&vg.rand) % 6 ],
-            localplayer.rb.co, 40.0f, 1.0f 
-         );
-      }
-      else if( w->surface == k_surface_prop_wood ){
-         audio_oneshot_3d( 
-           &audio_footsteps_wood[ vg_randu32(&vg.rand) % 6 ],
-            localplayer.rb.co, 40.0f, 1.0f 
-         );
-      }
-      audio_unlock();
-   }
-
-   w->state.step_phase = walk_phase;
-}
-
-void player__walk_update(void){
-   struct player_walk *w = &player_walk;
-
-   if( (w->state.activity == k_walk_activity_air) ||
-       (w->state.activity == k_walk_activity_ground) ||
-       (w->state.activity == k_walk_activity_oair) ||
-       (w->state.activity == k_walk_activity_oregular) ||
-       (w->state.activity == k_walk_activity_ipopoff) ){
-      player_walk_update_generic();
-   }
-}
-
-static void player_walk_animate_drop_in(void){
-   struct player_walk *w = &player_walk;
-   struct player_walk_animator *animator = &w->animator;
-   struct skeleton_anim *anim = w->anim_drop_in;
-
-   f32 length = (f32)(anim->length-1) / anim->rate,
-       time   = w->state.transition_t;
-
-   f32 walk_yaw = vg_alerpf( w->state.drop_in_start_angle, 
-                             w->state.drop_in_angle, animator->transition_t );
-   v3_lerp( w->state.drop_in_start, w->state.drop_in_target,
-            animator->transition_t, localplayer.rb.co );
-
-   q_axis_angle( localplayer.rb.q, (v3f){0,1,0}, walk_yaw + VG_PIf );
-
-   /* the drop in bit */
-   v3f final_co;
-   v4f final_q;
-   player_walk_drop_in_overhang_transform( animator->transition_t, 
-                                           final_co, final_q );
-
-   q_mul( final_q, localplayer.rb.q, localplayer.rb.q );
-   v3_lerp( localplayer.rb.co, final_co, animator->transition_t, 
-            localplayer.rb.co );
-
-   rb_update_matrices( &localplayer.rb );
-
-   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 
-               -0.1f*animator->transition_t, localplayer.rb.co );
-
-   v3_copy( localplayer.rb.co, animator->root_co );
-   v4_copy( localplayer.rb.q, animator->root_q );
-
-   /* for the camera purposes only */
-   v3f init_velocity;
-   player_walk_drop_in_vector( init_velocity );
-   v3_muls( init_velocity, animator->transition_t, localplayer.rb.v );
-   v3_copy( localplayer.rb.v, 
-            localplayer.cam_control.cam_velocity_smooth );
-}
-
-static void player_walk_animate_generic(void){
-   struct player_walk *w = &player_walk;
-   struct player_walk_animator *animator = &w->animator;
-
-   v4f _null;
-   rb_extrapolate( &localplayer.rb, animator->root_co, _null );
-
-   f32 walk_yaw = player_get_heading_yaw(),
-       head_yaw = localplayer.angles[0] + VG_PIf,
-       y = vg_angle_diff( head_yaw, -walk_yaw ),
-       p = vg_clampf( localplayer.angles[1], 
-                        -k_sit_pitch_limit, k_sit_pitch_limit );
-
-   if( fabsf(y) > k_sit_yaw_limit ){
-      y = 0.0f;
-      p = 0.0f;
-   }
-
-   animator->yaw = vg_lerpf( animator->yaw, y, vg.time_delta*2.0f );
-   animator->pitch = vg_lerpf( animator->pitch, p, vg.time_delta*2.8f );
-   q_axis_angle( animator->root_q, (v3f){0,1,0}, walk_yaw + VG_PIf );
-
-   v4f qrev;
-   q_axis_angle( qrev, (v3f){0,1,0}, VG_TAUf*0.5f );
-   q_mul( localplayer.rb.q, qrev, animator->root_q );
-}
-
-void player__walk_animate(void){
-   struct player_walk *w = &player_walk;
-   player_pose *pose = &localplayer.pose;
-   struct player_walk_animator *animator = &w->animator;
-
-   animator->activity = w->state.activity;
-   animator->transition_t = w->state.transition_t;
-
-   {
-      f32 fly = (w->state.activity == k_walk_activity_air)? 1.0f: 0.0f, 
-          rate;
-
-      if( w->state.activity == k_walk_activity_air ) rate = 2.4f;
-      else rate = 9.0f;
-
-      animator->fly = vg_lerpf( animator->fly, fly, rate*vg.time_delta );
-      animator->run = vg_lerpf( animator->run, w->move_speed, 
-                                8.0f*vg.time_delta);
-   }
-
-   if( animator->run > 0.025f ){
-      f32 walk_norm = 30.0f/(float)w->anim_walk->length,
-          run_norm  = 30.0f/(float)w->anim_run->length,
-          l;
-
-      if( animator->run <= k_walkspeed )
-         l = (animator->run / k_walkspeed) * walk_norm;
-      else {
-         l = vg_lerpf( walk_norm, run_norm, 
-                      (animator->run-k_walkspeed) / (k_runspeed-k_walkspeed) );
-      }
-      w->state.walk_timer += l * vg.time_delta;
-   }
-   else
-      w->state.walk_timer = 0.0f;
-
-   animator->walk_timer = w->state.walk_timer;
-
-   player_walk_animate_generic();
-   if( w->state.activity == k_walk_activity_odrop_in ){
-      player_walk_animate_drop_in();
-   }
-
-   if( (w->state.activity == k_walk_activity_odrop_in) ||
-       (w->state.activity == k_walk_activity_oregular) ||
-       (w->state.activity == k_walk_activity_oair) ){
-      localplayer.cam_velocity_influence = w->animator.transition_t;
-   }
-   else if( w->state.activity == k_walk_activity_ipopoff ){
-      localplayer.cam_velocity_influence = 1.0f-w->animator.transition_t;
-   }
-   else
-      localplayer.cam_velocity_influence = 0.0f;
-
-   if( w->state.activity == k_walk_activity_sit ){
-      localplayer.cam_dist = 3.8f;
-   }
-   else {
-      localplayer.cam_dist = 1.8f;
-   }
-}
-
-static void player_walk_pose_sit( struct player_walk_animator *animator,
-                                  player_pose *pose )
-{
-   mdl_keyframe bpose[32];
-
-   struct player_walk *w = &player_walk;
-   struct skeleton *sk = &localplayer.skeleton;
-
-   f32 t  = animator->transition_t,
-       st = t * ((f32)(w->anim_sit->length-1)/30.0f);
-   skeleton_sample_anim( sk, w->anim_sit, st, bpose );
-
-   v4f qy,qp;
-   f32 *qh = bpose[localplayer.id_head-1].q;
-   q_axis_angle( qy, (v3f){0,1,0}, animator->yaw*0.5f*t );
-   q_axis_angle( qp, (v3f){0,0,1}, animator->pitch*t );
-   q_mul( qy, qh, qh );
-   q_mul( qh, qp, qh );
-   q_normalize( qh );
-
-   qh = bpose[localplayer.id_chest-1].q;
-   q_axis_angle( qy, (v3f){0,1,0}, animator->yaw*0.5f*t );
-   q_mul( qy, qh, qh );
-   q_normalize( qh );
-
-   skeleton_lerp_pose( sk, pose->keyframes, bpose, 
-                       vg_minf(1.0f,t*10.0f), pose->keyframes );
-}
-
-enum walk_transition_type {
-   k_walk_transition_in,
-   k_walk_transition_out,
-   k_walk_transition_outin,
-};
-
-static void player_walk_pose_transition( 
-      struct player_walk_animator *animator, struct skeleton_anim *anim,
-      enum walk_transition_type type,
-      mdl_keyframe apose[32], f32 *mask, player_pose *pose ){
-
-   mdl_keyframe bpose[32];
-
-   struct player_walk *w = &player_walk;
-   struct skeleton *sk = &localplayer.skeleton;
-   
-   f32 length   = (f32)(anim->length-1) / anim->rate,
-       t        = animator->transition_t * length,
-       blend    = 1.0f;
-
-   if( type == k_walk_transition_in || type == k_walk_transition_outin )
-      blend = vg_minf( blend, length-t );
-
-   if( type == k_walk_transition_out || type == k_walk_transition_outin )
-      blend = vg_minf( blend, t );
-
-   blend = vg_smoothstepf( vg_minf(1,blend/k_anim_transition) );
-
-   skeleton_sample_anim_clamped( sk, anim, t, bpose );
-
-   mdl_keyframe *kf_board = &bpose[localplayer.id_board-1];
-   f32 yaw = animator->board_yaw * VG_TAUf * 0.5f;
-
-   v4f qyaw;
-   q_axis_angle( qyaw, (v3f){0,1,0}, yaw );
-   q_mul( kf_board->q, qyaw, kf_board->q );
-   q_normalize( kf_board->q );
-
-   if( mask ){
-      for( i32 i=0; i<sk->bone_count-1; i ++ )
-         keyframe_lerp( apose+i, bpose+i, blend*mask[i], pose->keyframes+i );
-   }
-   else
-      skeleton_lerp_pose( sk, apose, bpose, blend, pose->keyframes );
-}
-
-void player__walk_pose( void *_animator, player_pose *pose ){
-   struct player_walk *w = &player_walk;
-   struct player_walk_animator *animator = _animator;
-   struct skeleton *sk = &localplayer.skeleton;
-
-   v3_copy( animator->root_co, pose->root_co );
-   v4_copy( animator->root_q, pose->root_q );
-   pose->board.lean = 0.0f;
-   pose->type = k_player_pose_type_ik;
-
-   float walk_norm = (float)w->anim_walk->length/30.0f,
-         run_norm  = (float)w->anim_run->length/30.0f,
-         t = animator->walk_timer;
-
-   /* walk/run */
-   mdl_keyframe apose[32], bpose[32];
-   if( animator->run <= k_walkspeed ){ 
-      /* walk / idle */
-      f32 l = vg_minf( 1, (animator->run/k_walkspeed)*6.0f );
-      skeleton_sample_anim( sk, w->anim_idle, vg.time*0.1f, apose );
-      skeleton_sample_anim( sk, w->anim_walk, t*walk_norm, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, l, apose );
-   }
-   else { 
-      /* walk / run */
-      f32 l = (animator->run-k_walkspeed) / (k_runspeed-k_walkspeed);
-      skeleton_sample_anim( sk, w->anim_walk, t*walk_norm, apose );
-      skeleton_sample_anim( sk, w->anim_run, t*run_norm, bpose );
-      skeleton_lerp_pose( sk, apose, bpose, l, apose );
-   }
-
-   /* air */
-   skeleton_sample_anim( sk, w->anim_jump, vg.time*0.6f, bpose );
-   skeleton_lerp_pose( sk, apose, bpose, animator->fly, apose );
-
-   mdl_keyframe *kf_board = &apose[localplayer.id_board-1];
-   f32 yaw = animator->board_yaw;
-
-   if( animator->activity == k_walk_activity_ipopoff )
-      if( animator->transition_t > 0.5f )
-         yaw += 1.0f;
-
-   v4f qyaw;
-   q_axis_angle( qyaw, (v3f){0,1,0}, yaw * VG_TAUf * 0.5f );
-   q_mul( kf_board->q, qyaw, kf_board->q );
-   q_normalize( kf_board->q );
-
-   /* sit */
-   if( (animator->activity == k_walk_activity_sit) ||
-       (animator->activity == k_walk_activity_sit_up) )
-   {
-      skeleton_copy_pose( sk, apose, pose->keyframes );
-      player_walk_pose_sit( animator, pose );
-   }
-   else if( animator->activity == k_walk_activity_odrop_in ){
-      player_walk_pose_transition( 
-            animator, w->anim_drop_in, k_walk_transition_out, apose, 
-            NULL, pose );
-   }
-   else if( animator->activity == k_walk_activity_oair ){
-      player_walk_pose_transition(
-            animator, w->anim_jump_to_air, k_walk_transition_out, apose, 
-            NULL, pose );
-   }
-   else if( animator->activity == k_walk_activity_oregular ){
-      player_walk_pose_transition( 
-            animator, w->anim_intro, k_walk_transition_out, apose, 
-            NULL, pose );
-   }
-   else if( animator->activity == k_walk_activity_ipopoff ){
-      if( animator->run > 0.2f ){
-         f32 t = 1.0f-vg_minf( animator->run-0.2f, 1.0f ),
-             mask[ 32 ];
-
-         for( u32 i=0; i<32; i ++ ) 
-            mask[i] = 1.0f;
-
-         mask[ localplayer.id_ik_foot_l-1 ] = t;
-         mask[ localplayer.id_ik_foot_r-1 ] = t;
-         mask[ localplayer.id_ik_knee_l-1 ] = t;
-         mask[ localplayer.id_ik_knee_r-1 ] = t;
-         mask[ localplayer.id_hip-1 ] = t;
-         player_walk_pose_transition( 
-               animator, w->anim_popoff, k_walk_transition_in, apose,
-               mask, pose );
-      }
-      else{
-         player_walk_pose_transition( 
-               animator, w->anim_popoff, k_walk_transition_in, apose,
-               NULL, pose );
-      }
-   }
-   else {
-      skeleton_copy_pose( sk, apose, pose->keyframes );
-   }
-}
-
-void player__walk_post_animate(void){
-   /* 
-    * Camera 
-    */
-   struct player_walk *w = &player_walk;
-
-}
-
-void player__walk_im_gui( ui_context *ctx )
-{
-   struct player_walk *w = &player_walk;
-   player__debugtext( ctx, 1, "V:  %5.2f %5.2f %5.2f (%5.2fm/s)",
-      localplayer.rb.v[0], localplayer.rb.v[1], localplayer.rb.v[2],
-      v3_length(localplayer.rb.v) );
-   player__debugtext( ctx,
-                      1, "CO: %5.2f %5.2f %5.2f",localplayer.rb.co[0],
-                                                 localplayer.rb.co[1],
-                                                 localplayer.rb.co[2] );
-   player__debugtext( ctx, 1, "transition: %5.2f ", w->state.transition_t );
-   player__debugtext( ctx, 1, "activity: %s\n",
-                           (const char *[]){ "air",
-                                             "ground",
-                                             "sit",
-                                             "sit_up",
-                                             "inone",
-                                             "ipopoff",
-                                             "oair",
-                                             "odrop_in",
-                                             "oregular" }
-                                             [w->state.activity] );
-   player__debugtext( ctx, 1, "surface: %s\n",
-                           (const char *[]){ "concrete",
-                                             "wood",
-                                             "grass",
-                                             "tiles",
-                                             "metal",
-                                             "snow",
-                                             "sand" }
-                                             [w->surface] );
-}
-
-void player__walk_bind(void){
-   struct player_walk *w = &player_walk;
-   struct skeleton *sk = &localplayer.skeleton;
-
-   w->anim_idle         = skeleton_get_anim( sk, "idle_cycle+y" );
-   w->anim_walk         = skeleton_get_anim( sk, "walk+y" );
-   w->anim_run          = skeleton_get_anim( sk, "run+y" );
-   w->anim_jump         = skeleton_get_anim( sk, "jump+y" );
-   w->anim_jump_to_air  = skeleton_get_anim( sk, "jump_to_air" );
-   w->anim_drop_in      = skeleton_get_anim( sk, "drop_in" );
-   w->anim_intro        = skeleton_get_anim( sk, "into_skate" );
-   w->anim_sit          = skeleton_get_anim( sk, "sit" );
-   w->anim_popoff       = skeleton_get_anim( sk, "pop_off_short" );
-}
-
-void player__walk_transition( bool grounded, f32 board_yaw ){
-   struct player_walk *w = &player_walk;
-   w->state.activity = k_walk_activity_air;
-
-   if( grounded ){
-      w->state.activity = k_walk_activity_ipopoff;
-   }
-
-   w->state.transition_t = 0.0f;
-   w->state.jump_queued = 0;
-   w->state.jump_input_time = 0.0;
-   w->state.walk_timer = 0.0f;
-   w->state.step_phase = 0;
-   w->animator.board_yaw = fmodf( board_yaw, 2.0f );
-   rb_update_matrices( &localplayer.rb );
-}
-
-void player__walk_reset(void)
-{
-   struct player_walk *w = &player_walk;
-   w->state.activity = k_walk_activity_air;
-   w->state.transition_t = 0.0f;
-
-   v3f fwd = { 0.0f, 0.0f, 1.0f };
-   q_mulv( localplayer.rb.q, fwd, fwd );
-   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
-                 atan2f(fwd[0], fwd[2]) );
-
-   rb_update_matrices( &localplayer.rb );
-}
-
-void player__walk_animator_exchange( bitpack_ctx *ctx, void *data ){
-   struct player_walk_animator *animator = data;
-
-   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
-   bitpack_qquat( ctx, animator->root_q );
-   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->fly );
-   bitpack_qf32( ctx, 8, 0.0f, k_runspeed, &animator->run );
-   bitpack_qf32( ctx, 16, 0.0f, 120.0f, &animator->walk_timer );
-
-   for( int i=0; i<1; i++ ){ /* without this you get a warning from gcc. lol */
-      bitpack_bytes( ctx, 8, &animator->activity );
-   }
-
-   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->transition_t );
-
-   if( (animator->activity == k_walk_activity_sit) || 
-       (animator->activity == k_walk_activity_sit_up) ){
-      bitpack_qf32( ctx, 8, -k_sit_yaw_limit, k_sit_yaw_limit, &animator->yaw );
-      bitpack_qf32( ctx, 8, -k_sit_pitch_limit, k_sit_pitch_limit, 
-                     &animator->pitch );
-   }
-
-   bitpack_qf32( ctx, 16, -100.0f, 100.0f, &animator->board_yaw );
-}
-
-void player__walk_sfx_oneshot( u8 id, v3f pos, f32 volume )
-{
-   audio_lock();
-
-   if( id == k_player_walk_soundeffect_splash ){
-      audio_oneshot_3d( &audio_splash, pos, 40.0f, 1.0f );
-   }
-
-   audio_unlock();
-}
diff --git a/player_walk.h b/player_walk.h
deleted file mode 100644 (file)
index 5da0350..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-#pragma once
-#include "player.h"
-#include "player_api.h"
-#include "vg/vg_rigidbody.h"
-
-#define PLAYER_JUMP_EPSILON 0.1     /* 100ms jump allowance */
-
-struct player_walk
-{
-   rb_capsule collider;
-
-   struct player_walk_state{
-      v3f prev_pos;
-      v3f drop_in_target,
-          drop_in_start,
-          drop_in_normal;
-
-      float drop_in_start_angle,
-            drop_in_angle;
-
-      enum walk_activity{
-         k_walk_activity_air,
-         k_walk_activity_ground,
-         k_walk_activity_sit,
-         k_walk_activity_sit_up,
-
-         /* transitions */
-         k_walk_activity_inone,
-         k_walk_activity_ipopoff,
-         k_walk_activity_oair,
-         k_walk_activity_odrop_in,
-         k_walk_activity_oregular,
-
-         k_walk_activity_max,
-      }
-      activity;
-
-      f32 transition_t;
-
-      int jump_queued;
-      f64 jump_input_time;
-
-      f32 walk_timer;
-      int step_phase;
-      v3f steer;
-   }
-   state;
-
-   f32 move_speed;
-
-   enum mdl_surface_prop surface;
-   struct skeleton_anim *anim_walk, *anim_run, *anim_idle, *anim_jump,
-                        *anim_jump_to_air, *anim_drop_in, *anim_intro,
-                        *anim_sit, *anim_popoff;
-
-   struct player_walk_animator {
-      v3f root_co;
-      v4f root_q;
-      f32 fly,
-          run,
-          walk;
-
-      f32 walk_timer, yaw, pitch, board_yaw;
-
-      enum walk_activity activity;
-      f32 transition_t;
-   }
-   animator;
-}
-extern player_walk;
-extern struct player_subsystem_interface player_subsystem_walk;
-
-enum player_walk_soundeffect {
-   k_player_walk_soundeffect_splash
-};
-
-static f32
-   k_walkspeed             = 4.4f,
-   k_runspeed              = 10.0f,
-   k_airspeed              = 1.2f,
-   k_stopspeed             = 4.0f,
-   k_walk_accel            = 10.0f,
-   k_walk_air_accel        = 7.0f,
-   k_walk_friction         = 6.0f,
-   k_walk_step_height      = 0.2f,
-
-   k_sit_yaw_limit         = VG_PIf/1.7f,
-   k_sit_pitch_limit       = VG_PIf/4.0f;
-
-static void player__walk_register(void)
-{
-   VG_VAR_F32( k_walkspeed,      flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_runspeed,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_stopspeed,      flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_airspeed,       flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_walk_friction,  flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_walk_air_accel, flags=VG_VAR_CHEAT );
-   VG_VAR_F32( k_walk_accel,     flags=VG_VAR_CHEAT );
-}
-
-void player__walk_pre_update  (void);
-void player__walk_update      (void);
-void player__walk_post_update (void);
-void player__walk_animate     (void);
-void player__walk_pose        (void *animator, player_pose *pose);
-void player__walk_post_animate(void);
-void player__walk_im_gui      ( ui_context *ctx );
-void player__walk_bind        (void);
-void player__walk_reset       (void);
-void player__walk_restore     (void);
-void player__walk_animator_exchange( bitpack_ctx *ctx, void *data );
-void player__walk_transition( bool grounded, f32 board_yaw );
-void player__walk_sfx_oneshot( u8 id, v3f pos, f32 volume );
diff --git a/render.c b/render.c
deleted file mode 100644 (file)
index fada4a5..0000000
--- a/render.c
+++ /dev/null
@@ -1,258 +0,0 @@
-#include "render.h"
-#include "vg/vg_engine.h"
-#include "vg/vg_platform.h"
-#include "vg/vg_framebuffer.h"
-
-static void async_render_init( void *payload, u32 size )
-{
-   f32 rh = 0x1p-4f, ih = 0.3f;
-
-   float quad[] = { 
-      0.00f,0.00f, 1.00f,1.00f, 0.00f,1.00f,    /* fsquad */
-      0.00f,0.00f, 1.00f,0.00f, 1.00f,1.00f,    
-
-      0.00f,0.00f, 1.00f,rh,     0.00f,rh,    /* fsquad1 */
-      0.00f,0.00f, 1.00f,0.00f,  1.00f,rh,
-      0.00f,1.00f, 0.00f,1.0f-rh,1.00f,1.0f-rh,
-      0.00f,1.00f, 1.00f,1.0f-rh,1.00f,1.0f,   
-
-      /* 9x9 debug grid */
-      /* row0 */
-      0.00f,0.00f, 0.30f,0.30f, 0.00f,0.30f,
-      0.00f,0.00f, 0.30f,0.00f, 0.30f,0.30f,
-      0.30f,0.00f, 0.60f,0.30f, 0.30f,0.30f,
-      0.30f,0.00f, 0.60f,0.00f, 0.60f,0.30f,
-      0.60f,0.00f, 0.90f,0.30f, 0.60f,0.30f,
-      0.60f,0.00f, 0.90f,0.00f, 0.90f,0.30f,
-      /* row1 */
-      0.00f,0.30f, 0.30f,0.60f, 0.00f,0.60f,
-      0.00f,0.30f, 0.30f,0.30f, 0.30f,0.60f,
-      0.30f,0.30f, 0.60f,0.60f, 0.30f,0.60f,
-      0.30f,0.30f, 0.60f,0.30f, 0.60f,0.60f,
-      0.60f,0.30f, 0.90f,0.60f, 0.60f,0.60f,
-      0.60f,0.30f, 0.90f,0.30f, 0.90f,0.60f,
-      /* row2 */
-      0.00f,0.60f, 0.30f,0.90f, 0.00f,0.90f,
-      0.00f,0.60f, 0.30f,0.60f, 0.30f,0.90f,
-      0.30f,0.60f, 0.60f,0.90f, 0.30f,0.90f,
-      0.30f,0.60f, 0.60f,0.60f, 0.60f,0.90f,
-      0.60f,0.60f, 0.90f,0.90f, 0.60f,0.90f,
-      0.60f,0.60f, 0.90f,0.60f, 0.90f,0.90f,
-
-      0.00f,ih, 1.00f,ih+rh, 0.00f,ih+rh,    /* fsquad2 */
-      0.00f,ih, 1.00f,ih,    1.00f,ih+rh,
-   };
-
-   glGenVertexArrays( 1, &g_render.fsquad.vao );
-   glGenBuffers( 1, &g_render.fsquad.vbo );
-   glBindVertexArray( g_render.fsquad.vao );
-   glBindBuffer( GL_ARRAY_BUFFER, g_render.fsquad.vbo );
-   glBufferData( GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW );
-   glBindVertexArray( g_render.fsquad.vao );
-   glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 
-                          sizeof(float)*2, (void*)0 );
-   glEnableVertexAttribArray( 0 );
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   g_render.ready = 1;
-}
-
-void render_init(void)
-{
-   vg_console_reg_var( "blur_strength", &k_blur_strength, k_var_dtype_f32, 0 );
-   vg_console_reg_var( "render_scale", &k_render_scale,
-                       k_var_dtype_f32, VG_VAR_PERSISTENT );
-   vg_console_reg_var( "fov", &k_fov, k_var_dtype_f32, VG_VAR_PERSISTENT );
-   vg_console_reg_var( "cam_height", &k_cam_height, 
-                        k_var_dtype_f32, VG_VAR_PERSISTENT );
-   vg_console_reg_var( "blur_effect", &k_blur_effect, 
-                        k_var_dtype_i32, VG_VAR_PERSISTENT );
-
-   void *alloc = vg_mem.rtmemory;
-   
-   /* 
-    * Main framebuffer
-    */
-   g_render.fb_main = vg_framebuffer_allocate( alloc, 3, 1 );
-   g_render.fb_main->display_name = "main";
-   g_render.fb_main->resolution_div = 1;
-   g_render.fb_main->attachments[0] = (vg_framebuffer_attachment)
-   {
-      "colour", k_framebuffer_attachment_type_texture,
-
-      .internalformat = GL_RGB,
-      .format         = GL_RGB,
-      .type           = GL_UNSIGNED_BYTE,
-      .attachment     = GL_COLOR_ATTACHMENT0
-   };
-   g_render.fb_main->attachments[1] = (vg_framebuffer_attachment)
-   {
-      "motion", k_framebuffer_attachment_type_texture,
-
-      .quality        = k_framebuffer_quality_high_only,
-      .internalformat = GL_RG16F,
-      .format         = GL_RG,
-      .type           = GL_FLOAT,
-      .attachment     = GL_COLOR_ATTACHMENT1
-   };
-   g_render.fb_main->attachments[2] = (vg_framebuffer_attachment)
-   {
-      "depth_stencil", k_framebuffer_attachment_type_texture_depth,
-      .internalformat = GL_DEPTH24_STENCIL8,
-      .format         = GL_DEPTH_STENCIL,
-      .type           = GL_UNSIGNED_INT_24_8,
-      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
-   };
-   vg_framebuffer_create( g_render.fb_main );
-   
-   /* 
-    * Water reflection
-    */
-   g_render.fb_water_reflection = vg_framebuffer_allocate( alloc, 2, 1 );
-   g_render.fb_water_reflection->display_name = "water_reflection";
-   g_render.fb_water_reflection->resolution_div = 2;
-   g_render.fb_water_reflection->attachments[0] = (vg_framebuffer_attachment) 
-   {
-      "colour", k_framebuffer_attachment_type_texture,
-      .internalformat = GL_RGB,
-      .format         = GL_RGB,
-      .type           = GL_UNSIGNED_BYTE,
-      .attachment     = GL_COLOR_ATTACHMENT0
-   };
-   g_render.fb_water_reflection->attachments[1] = (vg_framebuffer_attachment) 
-   {
-      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
-      .internalformat = GL_DEPTH24_STENCIL8,
-      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
-   };
-   vg_framebuffer_create( g_render.fb_water_reflection );
-
-   /*
-    * Thid rendered view from the perspective of the camera, but just 
-    * captures stuff thats under the water
-    */
-   g_render.fb_water_beneath = vg_framebuffer_allocate( alloc, 2, 1 );
-   g_render.fb_water_beneath->display_name = "water_beneath";
-   g_render.fb_water_beneath->resolution_div = 2;
-   g_render.fb_water_beneath->attachments[0] = (vg_framebuffer_attachment) 
-   {
-      "colour", k_framebuffer_attachment_type_texture,
-      .internalformat = GL_RED,
-      .format         = GL_RED,
-      .type           = GL_UNSIGNED_BYTE,
-      .attachment     = GL_COLOR_ATTACHMENT0
-   };
-   g_render.fb_water_beneath->attachments[1] = (vg_framebuffer_attachment) 
-   {
-      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
-      .internalformat = GL_DEPTH24_STENCIL8,
-      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
-   };
-   vg_framebuffer_create( g_render.fb_water_beneath );
-
-   /* 
-    * Workshop preview
-    */
-   g_render.fb_workshop_preview = vg_framebuffer_allocate( alloc, 2, 1 );
-   g_render.fb_workshop_preview->display_name = "workshop_preview";
-   g_render.fb_workshop_preview->resolution_div = 0;
-   g_render.fb_workshop_preview->fixed_w = WORKSHOP_PREVIEW_WIDTH;
-   g_render.fb_workshop_preview->fixed_h = WORKSHOP_PREVIEW_HEIGHT;
-   g_render.fb_workshop_preview->attachments[0] = (vg_framebuffer_attachment) 
-   {
-      "colour", k_framebuffer_attachment_type_texture,
-      .internalformat = GL_RGB,
-      .format         = GL_RGB,
-      .type           = GL_UNSIGNED_BYTE,
-      .attachment     = GL_COLOR_ATTACHMENT0
-   };
-   g_render.fb_workshop_preview->attachments[1] = (vg_framebuffer_attachment) 
-   {
-      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
-      .internalformat = GL_DEPTH24_STENCIL8,
-      .attachment = GL_DEPTH_STENCIL_ATTACHMENT
-   };
-   vg_framebuffer_create( g_render.fb_workshop_preview );
-   
-   /*
-    * Network status
-    */
-   g_render.fb_network_status = vg_framebuffer_allocate( alloc, 1, 1 );
-   g_render.fb_network_status->display_name = "network_status_ui";
-   g_render.fb_network_status->resolution_div = 0;
-   g_render.fb_network_status->fixed_w = 128;
-   g_render.fb_network_status->fixed_h = 48;
-   g_render.fb_network_status->attachments[0] = (vg_framebuffer_attachment) 
-   {
-      "colour", k_framebuffer_attachment_type_texture,
-      .internalformat = GL_RGB,
-      .format         = GL_RGB,
-      .type           = GL_UNSIGNED_BYTE,
-      .attachment     = GL_COLOR_ATTACHMENT0
-   };
-   vg_framebuffer_create( g_render.fb_network_status );
-
-   vg_async_call( async_render_init, NULL, 0 );
-}
-
-/*
- * Utility
- */
-void render_fsquad(void)
-{
-   glBindVertexArray( g_render.fsquad.vao );
-   glDrawArrays( GL_TRIANGLES, 0, 6 );
-}
-
-void render_fsquad1(void)
-{
-   glBindVertexArray( g_render.fsquad.vao );
-   glDrawArrays( GL_TRIANGLES, 6, 6+6 );
-}
-
-void render_fsquad2(void)
-{
-   glBindVertexArray( g_render.fsquad.vao );
-   glDrawArrays( GL_TRIANGLES, 66+6,6 );
-}
-
-void postprocess_to_screen( vg_framebuffer *fb )
-{
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   glViewport( 0,0, vg.window_x, vg.window_y );
-
-   glEnable(GL_BLEND);
-   glDisable(GL_DEPTH_TEST);
-   glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
-   glBlendEquation(GL_FUNC_ADD);
-
-   v2f inverse;
-   vg_framebuffer_inverse_ratio( fb, inverse );
-
-   if( k_blur_effect )
-   {
-      shader_blitblur_use();
-      shader_blitblur_uTexMain( 0 );
-      shader_blitblur_uTexMotion( 1 );
-      shader_blitblur_uBlurStrength( k_blur_strength / 
-                                     (vg.time_frame_delta*60.0) );
-      shader_blitblur_uInverseRatio( inverse );
-
-      inverse[0] -= 0.0001f;
-      inverse[1] -= 0.0001f;
-      shader_blitblur_uClampUv( inverse );
-      shader_blitblur_uOverrideDir( g_render.blur_override );
-
-      vg_framebuffer_bind_texture( fb, 0, 0 );
-      vg_framebuffer_bind_texture( fb, 1, 1 );
-   }
-   else
-   {
-      shader_blit_use();
-      shader_blit_uTexMain( 0 );
-      shader_blit_uInverseRatio( inverse );
-      vg_framebuffer_bind_texture( fb, 0, 0 );
-   }
-
-   render_fsquad();
-}
diff --git a/render.h b/render.h
deleted file mode 100644 (file)
index 765f41b..0000000
--- a/render.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-#pragma once
-#include "common.h"
-#include "model.h"
-#include "shader_props.h"
-#include "vg/vg_framebuffer.h"
-#include "vg/vg_camera.h"
-
-#include "shaders/blit.h"
-#include "shaders/blitblur.h"
-#include "shaders/blitcolour.h"
-#include "shaders/blit_transition.h"
-
-#define WORKSHOP_PREVIEW_WIDTH  504
-#define WORKSHOP_PREVIEW_HEIGHT 336
-
-static f32 k_render_scale  = 1.0f;
-static i32 k_blur_effect   = 1;
-static f32 k_blur_strength = 0.3f;
-static f32 k_fov           = 0.86f;
-static f32 k_cam_height    = 0.8f;
-
-/* 
- * All standard buffers used in rendering
- */
-struct pipeline
-{
-   glmesh fsquad;
-
-   vg_framebuffer *fb_main,
-                  *fb_water_reflection,
-                  *fb_water_beneath,
-                  *fb_workshop_preview,
-                  *fb_network_status;
-   int ready;
-
-   v2f blur_override;
-   vg_camera cam;
-}
-static g_render;
-
-void render_init(void);
-void render_fsquad(void);
-void render_fsquad1(void);
-void render_fsquad2(void);
-void postprocess_to_screen( vg_framebuffer *fb );
diff --git a/save.c b/save.c
deleted file mode 100644 (file)
index 73eaa49..0000000
--- a/save.c
+++ /dev/null
@@ -1,195 +0,0 @@
-#include "skaterift.h"
-#include "save.h"
-#include "addon.h"
-#include "vg/vg_msg.h"
-#include "vg/vg_log.h"
-#include "vg/vg_loader.h"
-#include "world.h"
-#include "player.h"
-
-static const char *str_skaterift_main_save = "save.bkv";
-static f64 last_autosave;
-
-void savedata_file_write( savedata_file *file )
-{
-   savedata_file *sav = file;
-   FILE *fp = fopen( sav->path, "wb" );
-   if( fp ){
-      fwrite( sav->buf, sav->len, 1, fp );
-      fclose( fp );
-      vg_success( "savedata written to '%s'\n", sav->path );
-   }
-   else {
-      vg_error( "Error writing savedata (%s)\n", sav->path );
-   }
-}
-
-void savedata_group_write( savedata_group *group )
-{
-   for( u32 i=0; i<group->file_count; i++ ){
-      savedata_file_write( &group->files[i] );
-   }
-}
-
-void savedata_file_read( savedata_file *file )
-{
-   FILE *fp = fopen( file->path, "rb" );
-   if( fp ){
-      file->len = fread( file->buf, 1, sizeof(file->buf), fp );
-      fclose( fp );
-   }
-   else{
-      file->len = 0;
-      vg_warn( "Error reading savedata (%s)\n", file->path );
-   }
-}
-
-static void skaterift_write_addon_alias( vg_msg *msg, const char *key,
-                                         addon_alias *alias ){
-   if( alias->workshop_id ) 
-      vg_msg_wkvnum( msg, key, k_vg_msg_u64, 1, &alias->workshop_id );
-   else
-      vg_msg_wkvstr( msg, key, alias->foldername );
-}
-
-static void skaterift_write_viewslot( vg_msg *msg, const char *key,
-                                      enum addon_type type, u16 cache_id ){
-   if( !cache_id ) return;
-
-   struct addon_cache *cache = &addon_system.cache[type];
-   addon_cache_entry *entry = vg_pool_item( &cache->pool, cache_id );
-   addon_reg *reg = entry->reg_ptr;
-
-   if( reg )
-      skaterift_write_addon_alias( msg, key, &reg->alias );
-}
-
-void skaterift_read_addon_alias( vg_msg *msg, const char *key,
-                                 enum addon_type type, 
-                                 addon_alias *alias )
-{
-   alias->foldername[0] = '\0';
-   alias->workshop_id = 0;
-   alias->type = type;
-
-   vg_msg_cmd kv;
-   if( vg_msg_getkvcmd( msg, key, &kv ) ){
-      if( kv.code == k_vg_msg_kvstring ){
-         vg_strncpy( kv.value, alias->foldername, sizeof(alias->foldername),
-                     k_strncpy_allow_cutoff );
-      }
-      else
-         vg_msg_cast( kv.value, kv.code, &alias->workshop_id, k_vg_msg_u64 );
-   }
-}
-
-static void skaterift_populate_world_savedata( savedata_file *file,
-                                               enum world_purpose which ){
-   file->path[0] = '\0';
-   file->len = 0;
-   addon_reg *reg = world_static.instance_addons[ which ];
-
-   if( !reg ){
-      vg_error( "Tried to save unspecified world (reg was null)\n" );
-      return;
-   }
-
-   skaterift_world_get_save_path( which, file->path );
-
-   vg_msg sav;
-   vg_msg_init( &sav, file->buf, sizeof(file->buf) );
-
-   world_instance *instance = &world_static.instances[which];
-   world_entity_serialize( instance, &sav );
-
-   vg_msg_frame( &sav, "player" );
-   {
-      vg_msg_wkvnum( &sav, "position", k_vg_msg_float|k_vg_msg_32b, 3, 
-                     (which == world_static.active_instance)? 
-                           localplayer.rb.co:
-                           instance->player_co );
-   }
-   vg_msg_end_frame( &sav );
-
-   file->len = sav.cur.co;
-}
-
-static void skaterift_populate_main_savedata( savedata_file *file )
-{
-   strcpy( file->path, str_skaterift_main_save );
-
-   vg_msg sav;
-   vg_msg_init( &sav, file->buf, sizeof(file->buf) );
-   vg_msg_wkvnum( &sav, "ach", k_vg_msg_u32, 1, &skaterift.achievements );
-
-   vg_msg_frame( &sav, "player" );
-   {
-      skaterift_write_viewslot( &sav, "board", k_addon_type_board, 
-                                localplayer.board_view_slot );
-      skaterift_write_viewslot( &sav, "playermodel", k_addon_type_player,
-                                localplayer.playermodel_view_slot );
-   }
-   vg_msg_end_frame( &sav );
-
-   file->len = sav.cur.co;
-}
-
-void skaterift_read_main_savedata( savedata_file *file )
-{
-   strcpy( file->path, str_skaterift_main_save );
-   savedata_file_read( file );
-}
-
-int skaterift_autosave( int async )
-{
-   if( async )
-      if( !vg_loader_availible() ) return 0;
-
-   u32 save_files = 2;
-   if( world_static.instances[k_world_purpose_client].status 
-         == k_world_status_loaded ){
-      save_files ++;
-   }
-
-   vg_linear_clear( vg_async.buffer );
-   u32 size = sizeof(savedata_group) + sizeof(savedata_file) * save_files;
-
-   savedata_group *group;
-   if( async ){
-      size = vg_align8( size );
-      group = vg_linear_alloc( vg_async.buffer, size );
-   }
-   else
-      group = alloca( size );
-
-   group->file_count = save_files;
-   skaterift_populate_main_savedata( &group->files[0] );
-   skaterift_populate_world_savedata( &group->files[1], k_world_purpose_hub );
-
-   if( world_static.instances[ k_world_purpose_client ].status 
-         == k_world_status_loaded ){
-      skaterift_populate_world_savedata( &group->files[2], 
-                                          k_world_purpose_client );
-   }
-
-   if( async )
-      vg_loader_start( (void *)savedata_group_write, group );
-   else
-      savedata_group_write( group );
-
-   return 1;
-}
-
-void skaterift_autosave_synchronous(void)
-{
-   skaterift_autosave(0);
-}
-
-void skaterift_autosave_update(void)
-{
-   if( vg.time - last_autosave > 20.0 ){
-      if( skaterift_autosave(1) ){
-         last_autosave = vg.time;
-      }
-   }
-}
diff --git a/save.h b/save.h
deleted file mode 100644 (file)
index acda301..0000000
--- a/save.h
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-#include "vg/vg_msg.h"
-#include "addon.h"
-
-typedef struct savedata_file savedata_file;
-typedef struct savedata_group savedata_group;
-
-struct savedata_group {
-   u32 file_count;
-   struct savedata_file {
-      char path[128];
-      u8  buf[2048];
-      u32 len;
-   }
-   files[];
-};
-
-void savedata_file_read( savedata_file *file );
-void savedata_file_write( savedata_file *file );
-void savedata_group_write( savedata_group *group );
-int skaterift_autosave(int async);
-void skaterift_autosave_synchronous(void);
-void skaterift_autosave_update(void);
-void skaterift_read_addon_alias( vg_msg *msg, const char *key,
-                                 enum addon_type type, 
-                                 addon_alias *alias );
-
-void skaterift_read_main_savedata( savedata_file *file );
diff --git a/scene.c b/scene.c
deleted file mode 100644 (file)
index a94fbca..0000000
--- a/scene.c
+++ /dev/null
@@ -1,402 +0,0 @@
-#include "scene.h"
-
-u32 scene_mem_required( scene_context *ctx )
-{
-   u32 vertex_length = vg_align8(ctx->max_vertices * sizeof(scene_vert)),
-       index_length  = vg_align8(ctx->max_indices  * sizeof(u32));
-
-   return vertex_length + index_length;
-}
-
-void scene_init( scene_context *ctx, u32 max_vertices, u32 max_indices )
-{
-   ctx->vertex_count = 0;
-   ctx->indice_count = 0;
-   ctx->max_vertices = max_vertices;
-   ctx->max_indices = max_indices;
-   ctx->arrindices = NULL; /* must be filled out by user */
-   ctx->arrvertices = NULL;
-
-   memset( &ctx->submesh, 0, sizeof(mdl_submesh) );
-
-   v3_fill( ctx->bbx[0],  999999.9f );
-   v3_fill( ctx->bbx[1], -999999.9f );
-}
-
-void scene_supply_buffer( scene_context *ctx, void *buffer )
-{
-   u32 vertex_length = vg_align8( ctx->max_vertices * sizeof(scene_vert) );
-
-   ctx->arrvertices = buffer;
-   ctx->arrindices  = (u32*)(((u8*)buffer) + vertex_length);
-}
-
-void scene_vert_pack_norm( scene_vert *vert, v3f norm, f32 blend )
-{
-   v3f n;
-   v3_muls( norm, 127.0f, n );
-   v3_minv( n, (v3f){  127.0f,  127.0f,  127.0f }, n );
-   v3_maxv( n, (v3f){ -127.0f, -127.0f, -127.0f }, n );
-   vert->norm[0] = n[0];
-   vert->norm[1] = n[1];
-   vert->norm[2] = n[2];
-   vert->norm[3] = blend * 127.0f;
-}
-
-/* 
- * Append a model into the scene with a given transform
- */
-void scene_add_mdl_submesh( scene_context *ctx, mdl_context *mdl, 
-                            mdl_submesh *sm, m4x3f transform )
-{
-   if( ctx->vertex_count + sm->vertex_count > ctx->max_vertices ){
-      vg_fatal_error( "Scene vertex buffer overflow (%u exceeds %u)\n",
-                        ctx->vertex_count + sm->vertex_count, 
-                        ctx->max_vertices );
-   }
-
-   if( ctx->indice_count + sm->indice_count > ctx->max_indices ){
-      vg_fatal_error( "Scene index buffer overflow (%u exceeds %u)\n",
-                        ctx->indice_count + sm->indice_count,
-                        ctx->max_indices );
-   }
-
-   mdl_vert   *src_verts = mdl_arritm( &mdl->verts, sm->vertex_start );
-   scene_vert *dst_verts = &ctx->arrvertices[ ctx->vertex_count ];
-
-   u32 *src_indices    =  mdl_arritm( &mdl->indices, sm->indice_start ),
-       *dst_indices    = &ctx->arrindices[ ctx->indice_count ];
-   
-   /* Transform and place vertices */
-   boxf bbxnew;
-   box_init_inf( bbxnew );
-   m4x3_expand_aabb_aabb( transform, bbxnew, sm->bbx );
-   box_concat( ctx->bbx, bbxnew );
-
-   m3x3f normal_matrix;
-   m3x3_copy( transform, normal_matrix );
-   v3_normalize( normal_matrix[0] );
-   v3_normalize( normal_matrix[1] );
-   v3_normalize( normal_matrix[2] );
-   
-   for( u32 i=0; i<sm->vertex_count; i++ ){
-      mdl_vert   *src   = &src_verts[i];
-      scene_vert *pvert = &dst_verts[i];
-
-      m4x3_mulv( transform, src->co, pvert->co );
-
-      v3f normal;
-      m3x3_mulv( normal_matrix, src->norm, normal );
-      scene_vert_pack_norm( pvert, normal, src->colour[0]*(1.0f/255.0f) );
-      
-      v2_copy( src->uv, pvert->uv );
-   }
-
-   u32 real_indices = 0;
-   for( u32 i=0; i<sm->indice_count/3; i++ ){
-      u32 *src = &src_indices[i*3],
-          *dst = &dst_indices[real_indices];
-
-      v3f ab, ac, tn;
-      v3_sub( src_verts[src[2]].co, src_verts[src[0]].co, ab );
-      v3_sub( src_verts[src[1]].co, src_verts[src[0]].co, ac );
-      v3_cross( ac, ab, tn );
-
-#if 0
-      if( v3_length2( tn ) <= 0.00001f )
-         continue;
-#endif
-
-      dst[0] = src[0] + ctx->vertex_count;
-      dst[1] = src[1] + ctx->vertex_count;
-      dst[2] = src[2] + ctx->vertex_count;
-
-      real_indices += 3;
-   }
-
-   if( real_indices != sm->indice_count )
-      vg_warn( "Zero area triangles in model\n" );
-
-   ctx->vertex_count += sm->vertex_count;
-   ctx->indice_count += real_indices;
-}
-
-/*
- * One by one adders for simplified access (mostly procedural stuff)
- */
-void scene_push_tri( scene_context *ctx, u32 tri[3] )
-{
-   if( ctx->indice_count + 3 > ctx->max_indices )
-      vg_fatal_error( "Scene indice buffer overflow (%u exceeds %u)\n",
-                        ctx->indice_count+3, ctx->max_indices );
-
-   u32 *dst = &ctx->arrindices[ ctx->indice_count ];
-
-   dst[0] = tri[0];
-   dst[1] = tri[1];
-   dst[2] = tri[2];
-
-   ctx->indice_count += 3;
-}
-
-void scene_push_vert( scene_context *ctx, scene_vert *v )
-{
-   if( ctx->vertex_count + 1 > ctx->max_vertices )
-      vg_fatal_error( "Scene vertex buffer overflow (%u exceeds %u)\n",
-                        ctx->vertex_count+1, ctx->max_vertices );
-
-   scene_vert *dst = &ctx->arrvertices[ ctx->vertex_count ];
-   *dst = *v;
-
-   ctx->vertex_count ++;
-}
-
-void scene_copy_slice( scene_context *ctx, mdl_submesh *sm )
-{
-   sm->indice_start = ctx->submesh.indice_start;
-   sm->indice_count = ctx->indice_count - sm->indice_start;
-
-   sm->vertex_start = ctx->submesh.vertex_start;
-   sm->vertex_count = ctx->vertex_count - sm->vertex_start;
-   
-   ctx->submesh.indice_start = ctx->indice_count;
-   ctx->submesh.vertex_start = ctx->vertex_count;
-}
-
-void scene_set_vertex_flags( scene_context *ctx, 
-                             u32 start, u32 count, u16 flags )
-{
-   for( u32 i=0; i<count; i++ )
-      ctx->arrvertices[ start + i ].flags = flags;
-}
-
-struct scene_upload_info{
-   scene_context *ctx;
-   glmesh *mesh;
-};
-
-void async_scene_upload( void *payload, u32 size )
-{
-   struct scene_upload_info *info = payload;
-
-   //assert( mesh->loaded == 0 );
-   
-   glmesh *mesh = info->mesh;
-   scene_context *ctx = info->ctx;
-
-   glGenVertexArrays( 1, &mesh->vao );
-   glGenBuffers( 1, &mesh->vbo );
-   glGenBuffers( 1, &mesh->ebo );
-   glBindVertexArray( mesh->vao );
-
-   size_t stride = sizeof(scene_vert);
-
-   glBindBuffer( GL_ARRAY_BUFFER, mesh->vbo );
-   glBufferData( GL_ARRAY_BUFFER, ctx->vertex_count*stride, 
-                 ctx->arrvertices, GL_STATIC_DRAW );
-
-   glBindVertexArray( mesh->vao );
-   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh->ebo );
-   glBufferData( GL_ELEMENT_ARRAY_BUFFER, ctx->indice_count*sizeof(u32),
-                 ctx->arrindices, GL_STATIC_DRAW );
-   
-   /* 0: coordinates */
-   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
-   glEnableVertexAttribArray( 0 );
-
-   /* 1: normal */
-   glVertexAttribPointer( 1, 4, GL_BYTE, GL_TRUE,
-         stride, (void *)offsetof(scene_vert, norm) );
-   glEnableVertexAttribArray( 1 );
-
-   /* 2: uv */
-   glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 
-         stride, (void *)offsetof(scene_vert, uv) );
-   glEnableVertexAttribArray( 2 );
-
-   mesh->indice_count = ctx->indice_count;
-   mesh->loaded = 1;
-
-   vg_info( "Scene upload ( XYZ_f32 UV_f32 XYZW_i8 )[ u32 ]\n" );
-   vg_info( "   indices:%u\n", ctx->indice_count );
-   vg_info( "   verts:%u\n",   ctx->vertex_count );
-}
-
-void scene_upload_async( scene_context *ctx, glmesh *mesh )
-{
-   vg_async_item *call = vg_async_alloc( sizeof(struct scene_upload_info) );
-
-   struct scene_upload_info *info = call->payload;
-   info->mesh = mesh;
-   info->ctx = ctx;
-
-   vg_async_dispatch( call, async_scene_upload );
-}
-
-vg_async_item *scene_alloc_async( scene_context *scene, glmesh *mesh,
-                                  u32 max_vertices, u32 max_indices )
-{
-   scene_init( scene, max_vertices, max_indices );
-   u32 buf_size = scene_mem_required( scene );
-
-   u32 hdr_size = vg_align8(sizeof(struct scene_upload_info));
-   vg_async_item *call = vg_async_alloc( hdr_size + buf_size );
-
-   struct scene_upload_info *info = call->payload;
-
-   info->mesh = mesh;
-   info->ctx = scene;
-
-   void *buffer = ((u8*)call->payload)+hdr_size;
-   scene_supply_buffer( scene, buffer );
-
-   return call;
-}
-
-
-/*
- * BVH implementation
- */
-
-static void scene_bh_expand_bound( void *user, boxf bound, u32 item_index )
-{
-   scene_context *s = user;
-   scene_vert *pa = &s->arrvertices[ s->arrindices[item_index*3+0] ],
-              *pb = &s->arrvertices[ s->arrindices[item_index*3+1] ],
-              *pc = &s->arrvertices[ s->arrindices[item_index*3+2] ];
-   
-  box_addpt( bound, pa->co );
-  box_addpt( bound, pb->co );
-  box_addpt( bound, pc->co );
-}
-
-static float scene_bh_centroid( void *user, u32 item_index, int axis )
-{
-   scene_context *s = user;
-   scene_vert *pa = &s->arrvertices[ s->arrindices[item_index*3+0] ],
-              *pb = &s->arrvertices[ s->arrindices[item_index*3+1] ],
-              *pc = &s->arrvertices[ s->arrindices[item_index*3+2] ];
-
-   #if 0
-
-   float min, max;
-
-   min = vg_minf( pa->co[axis], pb->co[axis] );
-   max = vg_maxf( pa->co[axis], pb->co[axis] );
-   min = vg_minf( min, pc->co[axis] );
-   max = vg_maxf( max, pc->co[axis] );
-
-   return (min+max) * 0.5f;
-
-   #else
-   return (pa->co[axis] + pb->co[axis] + pc->co[axis]) * (1.0f/3.0f);
-   #endif
-}
-
-static void scene_bh_swap( void *user, u32 ia, u32 ib )
-{
-   scene_context *s = user;
-
-   u32 *ti = &s->arrindices[ia*3];
-   u32 *tj = &s->arrindices[ib*3];
-
-   u32 temp[3];
-   temp[0] = ti[0];
-   temp[1] = ti[1];
-   temp[2] = ti[2];
-
-   ti[0] = tj[0];
-   ti[1] = tj[1];
-   ti[2] = tj[2];
-
-   tj[0] = temp[0];
-   tj[1] = temp[1];
-   tj[2] = temp[2];
-}
-
-static void scene_bh_debug( void *user, u32 item_index )
-{
-   scene_context *s = user;
-   u32 idx = item_index*3;
-   scene_vert *pa = &s->arrvertices[ s->arrindices[ idx+0 ] ],
-              *pb = &s->arrvertices[ s->arrindices[ idx+1 ] ],
-              *pc = &s->arrvertices[ s->arrindices[ idx+2 ] ];
-
-   vg_line( pa->co, pb->co, 0xff0000ff );
-   vg_line( pb->co, pc->co, 0xff0000ff );
-   vg_line( pc->co, pa->co, 0xff0000ff );
-}
-
-static void scene_bh_closest( void *user, u32 index, v3f point, v3f closest )
-{
-   scene_context *s = user;
-
-   v3f positions[3];
-   u32 *tri = &s->arrindices[ index*3 ];
-   for( int i=0; i<3; i++ )
-      v3_copy( s->arrvertices[tri[i]].co, positions[i] );
-
-   closest_on_triangle_1( point, positions, closest );
-}
-
-bh_system bh_system_scene = 
-{
-   .expand_bound = scene_bh_expand_bound,
-   .item_centroid = scene_bh_centroid,
-   .item_closest = scene_bh_closest,
-   .item_swap = scene_bh_swap,
-   .item_debug = scene_bh_debug,
-};
-
-/*
- * An extra step is added onto the end to calculate the hit normal
- */
-int scene_raycast( scene_context *s, bh_tree *bh, 
-                   v3f co, v3f dir, ray_hit *hit, u16 ignore )
-{
-   hit->tri = NULL;
-
-   bh_iter it;
-   bh_iter_init_ray( 0, &it, co, dir, hit->dist );
-   i32 idx;
-
-   while( bh_next( bh, &it, &idx ) ){
-      u32 *tri = &s->arrindices[ idx*3 ];
-
-      if( s->arrvertices[tri[0]].flags & ignore )  continue;
-
-      v3f vs[3];
-      for( u32 i=0; i<3; i++ )
-         v3_copy( s->arrvertices[tri[i]].co, vs[i] );
-      
-      f32 t;
-      if( ray_tri( vs, co, dir, &t, 0 ) ){
-         if( t < hit->dist ){
-            hit->dist = t;
-            hit->tri = tri;
-         }
-      }
-   }
-
-   if( hit->tri ){
-      v3f v0, v1;
-      
-      float *pa = s->arrvertices[hit->tri[0]].co,
-            *pb = s->arrvertices[hit->tri[1]].co,
-            *pc = s->arrvertices[hit->tri[2]].co;
-
-      v3_sub( pa, pb, v0 );
-      v3_sub( pc, pb, v1 );
-      v3_cross( v1, v0, hit->normal );
-      v3_normalize( hit->normal );
-      v3_muladds( co, dir, hit->dist, hit->pos );
-   }
-
-   return hit->tri?1:0;
-}
-
-bh_tree *scene_bh_create( void *lin_alloc, scene_context *s )
-{
-   u32 triangle_count = s->indice_count / 3;
-   return bh_create( lin_alloc, &bh_system_scene, s, triangle_count, 2 );
-}
diff --git a/scene.h b/scene.h
deleted file mode 100644 (file)
index 8d6f57a..0000000
--- a/scene.h
+++ /dev/null
@@ -1,61 +0,0 @@
-#pragma once
-#include "vg/vg_bvh.h"
-#include "vg/vg_async.h"
-#include "common.h"
-#include "model.h"
-
-typedef struct scene_context scene_context;
-typedef struct scene_vert scene_vert;
-
-#pragma pack(push,1)
-
-/* 32 byte vertexs, we don't care about the normals too much,
- *    maybe possible to bring down uv to i16s too */
-struct scene_vert
-{
-   v3f co;        /* 3*32 */
-   v2f uv;        /* 2*32 */
-   i8  norm[4];   /* 4*8 */
-   u16 flags; /* only for the cpu. its junk on the gpu */
-   u16 unused[3];
-};
-
-#pragma pack(pop)
-
-/* 
- * 1. this should probably be a CONTEXT based approach unlike this mess.
- * take a bit of the mdl_context ideas and redo this header. its messed up
- * pretty bad right now.
- */
-
-struct scene_context
-{
-   scene_vert *arrvertices;
-   u32 *arrindices;
-
-   u32 vertex_count, indice_count,
-       max_vertices, max_indices;
-
-   boxf bbx;
-   mdl_submesh submesh;
-};
-
-extern bh_system bh_system_scene;
-bh_tree *scene_bh_create( void *lin_alloc, scene_context *s );
-int scene_raycast( scene_context *s, bh_tree *bh, 
-                   v3f co, v3f dir, ray_hit *hit, u16 ignore );
-vg_async_item *scene_alloc_async( scene_context *scene, glmesh *mesh,
-                                  u32 max_vertices, u32 max_indices );
-void scene_copy_slice( scene_context *ctx, mdl_submesh *sm );
-void scene_push_vert( scene_context *ctx, scene_vert *v );
-void scene_vert_pack_norm( scene_vert *vert, v3f norm, f32 blend );
-void scene_push_tri( scene_context *ctx, u32 tri[3] );
-void scene_add_mdl_submesh( scene_context *ctx, mdl_context *mdl, 
-                            mdl_submesh *sm, m4x3f transform );
-void scene_set_vertex_flags( scene_context *ctx, 
-                             u32 start, u32 count, u16 flags );
-void scene_supply_buffer( scene_context *ctx, void *buffer );
-void scene_init( scene_context *ctx, u32 max_vertices, u32 max_indices );
-u32 scene_mem_required( scene_context *ctx );
-void async_scene_upload( void *payload, u32 size );
-void scene_upload_async( scene_context *ctx, glmesh *mesh );
diff --git a/scene_rigidbody.h b/scene_rigidbody.h
deleted file mode 100644 (file)
index 57ff1ff..0000000
+++ /dev/null
@@ -1,247 +0,0 @@
-#pragma once
-
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
- *
- * Describes intereactions between vg rigidbody objects and skaterift's scene
- * description
- */
-
-#include "scene.h"
-#include "vg/vg_rigidbody.h"
-#include "vg/vg_rigidbody_collision.h"
-
-static int rb_sphere__scene( m4x3f mtxA, f32 r,
-                             m4x3f mtxB, bh_tree *scene_bh, rb_ct *buf, 
-                             u16 ignore ){
-   scene_context *sc = scene_bh->user;
-
-   int count = 0;
-
-   boxf box;
-   v3_sub( mtxA[3], (v3f){ r,r,r }, box[0] );
-   v3_add( mtxA[3], (v3f){ r,r,r }, box[1] );
-
-   bh_iter it;
-   i32 idx;
-   bh_iter_init_box( 0, &it, box );
-   
-   while( bh_next( scene_bh, &it, &idx ) ){
-      u32 *ptri = &sc->arrindices[ idx*3 ];
-      v3f tri[3];
-
-      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
-
-      for( int j=0; j<3; j++ )
-         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
-      
-      buf[ count ].element_id = ptri[0];
-
-      vg_line( tri[0],tri[1],0x70ff6000 );
-      vg_line( tri[1],tri[2],0x70ff6000 );
-      vg_line( tri[2],tri[0],0x70ff6000 );
-
-      int contact = rb_sphere__triangle( mtxA, r, tri, &buf[count] );
-      count += contact;
-
-      if( count == 16 ){
-         vg_warn( "Exceeding sphere_vs_scene capacity. Geometry too dense!\n" );
-         return count;
-      }
-   }
-
-   return count;
-}
-
-static int rb_box__scene( m4x3f mtxA, boxf bbx,
-                          m4x3f mtxB, bh_tree *scene_bh, 
-                          rb_ct *buf, u16 ignore ){
-   scene_context *sc = scene_bh->user;
-   v3f tri[3];
-
-   v3f extent, center;
-   v3_sub( bbx[1], bbx[0], extent );
-   v3_muls( extent, 0.5f, extent );
-   v3_add( bbx[0], extent, center );
-
-   f32 r = v3_length(extent);
-   boxf world_bbx;
-   v3_fill( world_bbx[0], -r );
-   v3_fill( world_bbx[1],  r );
-   for( int i=0; i<2; i++ ){
-      v3_add( center, world_bbx[i], world_bbx[i] );
-      v3_add( mtxA[3], world_bbx[i], world_bbx[i] );
-   }
-
-   m4x3f to_local;
-   m4x3_invert_affine( mtxA, to_local );
-
-   bh_iter it;
-   bh_iter_init_box( 0, &it, world_bbx );
-   int idx;
-   int count = 0;
-
-   vg_line_boxf( world_bbx, VG__RED );
-   
-   while( bh_next( scene_bh, &it, &idx ) ){
-      u32 *ptri = &sc->arrindices[ idx*3 ];
-      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
-
-      for( int j=0; j<3; j++ )
-         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
-
-      if( rb_box_triangle_sat( extent, center, to_local, tri ) ){
-         vg_line(tri[0],tri[1],0xff50ff00 );
-         vg_line(tri[1],tri[2],0xff50ff00 );
-         vg_line(tri[2],tri[0],0xff50ff00 );
-      }
-      else{
-         vg_line(tri[0],tri[1],0xff0000ff );
-         vg_line(tri[1],tri[2],0xff0000ff );
-         vg_line(tri[2],tri[0],0xff0000ff );
-         continue;
-      }
-
-      v3f v0,v1,n;
-      v3_sub( tri[1], tri[0], v0 );
-      v3_sub( tri[2], tri[0], v1 );
-      v3_cross( v0, v1, n );
-
-      if( v3_length2( n ) <= 0.00001f ){
-#ifdef RIGIDBODY_CRY_ABOUT_EVERYTHING
-         vg_error( "Zero area triangle!\n" );
-#endif
-         return 0;
-      }
-
-      v3_normalize( n );
-
-      /* find best feature */
-      f32 best = v3_dot( mtxA[0], n );
-      int axis = 0;
-
-      for( int i=1; i<3; i++ ){
-         f32 c = v3_dot( mtxA[i], n );
-
-         if( fabsf(c) > fabsf(best) ){
-            best = c;
-            axis = i;
-         }
-      }
-
-      v3f manifold[4];
-
-      if( axis == 0 ){
-         f32 px = best > 0.0f? bbx[0][0]: bbx[1][0];
-         manifold[0][0] = px;
-         manifold[0][1] = bbx[0][1];
-         manifold[0][2] = bbx[0][2];
-         manifold[1][0] = px;
-         manifold[1][1] = bbx[1][1];
-         manifold[1][2] = bbx[0][2];
-         manifold[2][0] = px;
-         manifold[2][1] = bbx[1][1];
-         manifold[2][2] = bbx[1][2];
-         manifold[3][0] = px;
-         manifold[3][1] = bbx[0][1];
-         manifold[3][2] = bbx[1][2];
-      }
-      else if( axis == 1 ){
-         f32 py = best > 0.0f? bbx[0][1]: bbx[1][1];
-         manifold[0][0] = bbx[0][0];
-         manifold[0][1] = py;
-         manifold[0][2] = bbx[0][2];
-         manifold[1][0] = bbx[1][0];
-         manifold[1][1] = py;
-         manifold[1][2] = bbx[0][2];
-         manifold[2][0] = bbx[1][0];
-         manifold[2][1] = py;
-         manifold[2][2] = bbx[1][2];
-         manifold[3][0] = bbx[0][0];
-         manifold[3][1] = py;
-         manifold[3][2] = bbx[1][2];
-      }
-      else{
-         f32 pz = best > 0.0f? bbx[0][2]: bbx[1][2];
-         manifold[0][0] = bbx[0][0];
-         manifold[0][1] = bbx[0][1];
-         manifold[0][2] = pz;
-         manifold[1][0] = bbx[1][0];
-         manifold[1][1] = bbx[0][1];
-         manifold[1][2] = pz;
-         manifold[2][0] = bbx[1][0];
-         manifold[2][1] = bbx[1][1];
-         manifold[2][2] = pz;
-         manifold[3][0] = bbx[0][0];
-         manifold[3][1] = bbx[1][1];
-         manifold[3][2] = pz;
-      }
-   
-      for( int j=0; j<4; j++ )
-         m4x3_mulv( mtxA, manifold[j], manifold[j] );
-
-      vg_line( manifold[0], manifold[1], 0xffffffff );
-      vg_line( manifold[1], manifold[2], 0xffffffff );
-      vg_line( manifold[2], manifold[3], 0xffffffff );
-      vg_line( manifold[3], manifold[0], 0xffffffff );
-
-      for( int j=0; j<4; j++ ){
-         rb_ct *ct = buf+count;
-
-         v3_copy( manifold[j], ct->co );
-         v3_copy( n, ct->n );
-
-         f32 l0 = v3_dot( tri[0], n ),
-               l1 = v3_dot( manifold[j], n );
-
-         ct->p = (l0-l1)*0.5f;
-         if( ct->p < 0.0f )
-            continue;
-
-         ct->type = k_contact_type_default;
-         count ++;
-
-         if( count >= 12 )
-            return count;
-      }
-   }
-   return count;
-}
-
-/* mtxB is defined only for tradition; it is not used currently */
-static int rb_capsule__scene( m4x3f mtxA, rb_capsule *c,
-                              m4x3f mtxB, bh_tree *scene_bh, 
-                              rb_ct *buf, u16 ignore ){
-   int count = 0;
-
-   boxf bbx;
-   v3_sub( mtxA[3], (v3f){ c->h, c->h, c->h }, bbx[0] );
-   v3_add( mtxA[3], (v3f){ c->h, c->h, c->h }, bbx[1] );
-   
-   scene_context *sc = scene_bh->user;
-   
-   bh_iter it;
-   bh_iter_init_box( 0, &it, bbx );
-   i32 idx;
-   while( bh_next( scene_bh, &it, &idx ) ){
-      u32 *ptri = &sc->arrindices[ idx*3 ];
-      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
-
-      v3f tri[3];
-      for( int j=0; j<3; j++ )
-         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
-      
-      buf[ count ].element_id = ptri[0];
-
-      int contact = rb_capsule__triangle( mtxA, c, tri, &buf[count] );
-      count += contact;
-
-      if( count >= 16 ){
-         vg_warn("Exceeding capsule_vs_scene capacity. Geometry too dense!\n");
-         return count;
-      }
-   }
-
-   return count;
-}
-
diff --git a/shader_props.h b/shader_props.h
deleted file mode 100644 (file)
index 29e79f5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-
-struct shader_props_standard
-{
-   u32 tex_diffuse;
-};
-
-struct shader_props_terrain
-{
-   u32 tex_diffuse;
-   v2f blend_offset;
-   v4f sand_colour;
-};
-
-struct shader_props_vertex_blend
-{
-   u32 tex_diffuse;
-   v2f blend_offset;
-};
-
-struct shader_props_water
-{
-   v4f shore_colour;
-   v4f deep_colour;
-   f32 fog_scale;
-   f32 fresnel;
-   f32 water_sale;
-   v4f wave_speed;
-};
-
-struct shader_props_cubemapped
-{
-   u32 tex_diffuse;
-   u32 cubemap_entity;
-   v4f tint;
-};
diff --git a/skaterift.c b/skaterift.c
deleted file mode 100644 (file)
index 36e087f..0000000
+++ /dev/null
@@ -1,679 +0,0 @@
-/*
- * =============================================================================
- *
- * Copyright  .        . .       -----, ,----- ,---.   .---.
- * 2021-2024  |\      /| |           /  |      |    | |    /|
- *            | \    / | +--        /   +----- +---'  |   / |
- *            |  \  /  | |         /    |      |   \  |  /  |
- *            |   \/   | |        /     |      |    \ | /   |
- *            '        ' '--' [] '----- '----- '     ' '---'  SOFTWARE
- *
- * =============================================================================
- */
-
-#define SR_ALLOW_REWIND_HUB
-
-#ifdef _WIN32
- #include <winsock2.h>
-#endif
-
-/* 
- *     system headers
- * --------------------- */
-
-#include "vg/vg_opt.h"
-#include "vg/vg_loader.h"
-#include "vg/vg_io.h"
-
-#include "skaterift.h"
-#include "steam.h"
-#include "render.h"
-#include "world.h"
-#include "font.h"
-#include "player.h"
-#include "network.h"
-#include "menu.h"
-#include "vehicle.h"
-#include "save.h"
-#include "player_remote.h"
-#include "particle.h"
-#include "trail.h"
-#include "freecam.h"
-#include "ent_tornado.h"
-#include "ent_miniworld.h"
-#include "ent_skateshop.h"
-#include "ent_npc.h"
-#include "ent_camera.h"
-#include "world_map.h"
-#include "gui.h"
-#include "workshop.h"
-#include "audio.h"
-#include "player_render.h"
-#include "control_overlay.h"
-#include "client.h"
-
-struct skaterift_globals skaterift = 
-{ 
-   .time_rate = 1.0f,
-   .hub_world = "maps/dev_hub",
-};
-
-void game_launch_opt(void)
-{
-   const char *arg;
-   if( (arg = vg_long_opt_arg( "world" )) )
-      skaterift.hub_world = arg;
-}
-
-static void async_skaterift_player_start( void *payload, u32 size ){
-   world_switch_instance(0);
-}
-
-static void skaterift_restore_state(void)
-{
-   savedata_file sav;
-   skaterift_read_main_savedata( &sav );
-
-   vg_msg kvsav;
-   vg_msg_init( &kvsav, sav.buf, sizeof(sav.buf) );
-
-   u32 ach;
-   vg_msg_getkvintg( &kvsav, "ach", k_vg_msg_u32, &ach, NULL );
-   skaterift.achievements |= ach;
-
-   u32 board_reg_id = time(NULL) % addon_count( k_addon_type_board, 0 ),
-       player_reg_id = (time(NULL)+44) % addon_count( k_addon_type_player, 0 );
-
-   vg_msg_cursor orig = kvsav.cur;
-   if( vg_msg_seekframe( &kvsav, "player" ) ){
-      addon_alias q;
-
-      /* board */
-      skaterift_read_addon_alias( &kvsav, "board", k_addon_type_board, &q );
-      u32 reg_id = addon_match( &q );
-      if( reg_id != 0xffffffff ) 
-         board_reg_id = reg_id;
-
-      /* playermodel */
-      skaterift_read_addon_alias( &kvsav, "playermodel", 
-                                  k_addon_type_player, &q );
-      reg_id = addon_match( &q );
-      if( reg_id != 0xffffffff ) 
-         player_reg_id = reg_id;
-   }
-
-   localplayer.board_view_slot = 
-      addon_cache_create_viewer( k_addon_type_board, board_reg_id );
-   localplayer.playermodel_view_slot = 
-      addon_cache_create_viewer( k_addon_type_player, player_reg_id );
-
-   kvsav.cur = orig;
-}
-
-static addon_reg *skaterift_mount_world_unloadable( const char *path, u32 ext ){
-   addon_reg *reg = addon_mount_local_addon( path, k_addon_type_world, ".mdl" );
-   if( !reg ) vg_fatal_error( "world not found\n" );
-   reg->flags |= (ADDON_REG_HIDDEN | ext);
-   return reg;
-}
-
-static void skaterift_load_world_content(void){
-   /* hub world */
-   addon_reg *hub = skaterift_mount_world_unloadable( skaterift.hub_world, 0 );
-   skaterift_mount_world_unloadable( "maps/mp_spawn", 
-         ADDON_REG_CITY|ADDON_REG_PREMIUM );
-   skaterift_mount_world_unloadable( "maps/mp_mtzero", 
-         ADDON_REG_MTZERO|ADDON_REG_PREMIUM );
-   skaterift_mount_world_unloadable( "maps/dev_tutorial", 0 );
-   skaterift_mount_world_unloadable( "maps/dev_flatworld", 0 );
-   skaterift_mount_world_unloadable( "maps/mp_line1", ADDON_REG_PREMIUM );
-
-   world_static.load_state = k_world_loader_load;
-
-   struct world_load_args args = {
-      .purpose = k_world_purpose_hub,
-      .reg = hub
-   };
-   skaterift_world_load_thread( &args );
-}
-
-static void skaterift_load_player_content(void)
-{
-   particle_alloc( &particles_grind, 300 );
-   particle_alloc( &particles_env, 200 );
-
-   player_load_animation_reference( "models/ch_none.mdl" );
-   player_model_load( &localplayer.fallback_model, "models/ch_none.mdl" );
-   player__bind();
-   player_board_load( &localplayer.fallback_board, "models/board_none.mdl" );
-}
-
-void game_load(void)
-{
-   vg_console_reg_cmd( "load_world", skaterift_load_world_command, NULL );
-   vg_console_reg_var( "immobile", &localplayer.immobile, k_var_dtype_i32, 0 );
-   vg_loader_step( menu_init, NULL );
-
-   vg_loader_step( control_overlay_init, NULL );
-   vg_loader_step( world_init, NULL );
-   vg_loader_step( vehicle_init, NULL );
-   vg_loader_step( gui_init, NULL );
-
-   vg_loader_step( player_init, NULL );
-   vg_loader_step( player_ragdoll_init, NULL );
-   vg_loader_step( npc_init, NULL );
-
-   /* content stuff */
-   vg_loader_step( addon_system_init, NULL );
-   vg_loader_step( workshop_init, NULL );
-   vg_loader_step( skateshop_init, NULL );
-   vg_loader_step( ent_tornado_init, NULL );
-   vg_loader_step( skaterift_replay_init, NULL );
-   vg_loader_step( skaterift_load_player_content, NULL );
-
-   vg_bake_shaders();
-   vg_loader_step( audio_init, NULL );
-
-   vg_loader_step( skaterift_load_world_content, NULL );
-   vg_async_call( async_skaterift_player_start, NULL, 0 );
-   vg_async_stall();
-
-   vg_console_load_autos();
-
-   addon_mount_content_folder( k_addon_type_player, 
-                               "playermodels", ".mdl" );
-   addon_mount_content_folder( k_addon_type_board, "boards", ".mdl" );
-   addon_mount_content_folder( k_addon_type_world, "maps", ".mdl" );
-   addon_mount_workshop_items();
-   vg_async_call( async_addon_reg_update, NULL, 0 );
-   vg_async_stall();
-
-   skaterift_restore_state();
-   update_ach_models();
-
-   vg_loader_step( NULL, skaterift_autosave_synchronous );
-}
-
-static void draw_origin_axis(void)
-{
-   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 1.0f, 0.0f, 0.0f }, 0xffff0000 );
-   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 0.0f, 1.0f, 0.0f }, 0xff00ff00 );
-   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 0.0f, 0.0f, 1.0f }, 0xff0000ff );
-}
-void skaterift_change_client_world_preupdate(void);
-
-/* 
- * UPDATE LOOP
- * ---------------------------------------------------------------------------*/
-
-void vg_pre_update(void)
-{
-   skaterift_preupdate_inputs();
-
-   steam_update();
-   skaterift_change_client_world_preupdate();
-
-   if( !g_client.loaded ) return;
-
-   draw_origin_axis();
-   addon_system_pre_update();
-   skateshop_world_preview_preupdate();
-   network_update();
-
-   /* time rate */
-   f32 target = 1;
-   if( skaterift.activity & k_skaterift_replay )
-      target = 0;
-
-   v3f listen_co;
-   v3_copy( localplayer.rb.co, listen_co );
-
-   if( skaterift.activity & k_skaterift_menu )
-   {
-      if( menu.bg_cam )
-      {
-         v3_copy( menu.bg_cam->transform.co, listen_co );
-      }
-      else target = 0;
-   }
-
-   vg_slewf( &skaterift.time_rate, target, vg.time_frame_delta * (1.0f/0.3f) );
-   vg.time_rate = vg_smoothstepf( skaterift.time_rate );
-   
-   /* TODO: how can we compress this? */
-   ent_miniworld_preupdate();
-   world_entity_focus_preupdate();
-
-   if( skaterift.activity != k_skaterift_menu )
-   {
-      player__pre_update();
-   }
-
-   skaterift_replay_pre_update();
-   remote_sfx_pre_update();
-   skateshop_world_preupdate( world_current_instance() );
-
-   world_update( world_current_instance(), localplayer.rb.co );
-   audio_ambient_sprites_update( world_current_instance(), listen_co );
-   world_map_pre_update();
-}
-
-void vg_fixed_update(void)
-{
-   if( !g_client.loaded ) return;
-
-   world_routes_fixedupdate( world_current_instance() );
-   player__update();
-   vehicle_update_fixed();
-}
-
-void vg_post_update(void)
-{
-   if( !g_client.loaded ) return;
-
-   player__post_update();
-
-   float dist;
-   int sample_index;
-   world_audio_sample_distances( localplayer.rb.co, &sample_index, &dist );
-
-   audio_lock();
-   vg_dsp.echo_distances[sample_index] = dist;
-
-   v3f ears = { 1.0f,0.0f,0.0f };
-   m3x3_mulv( g_render.cam.transform, ears, ears );
-   v3_copy( ears, vg_audio.external_listener_ears );
-   v3_copy( g_render.cam.transform[3], vg_audio.external_listener_pos );
-
-   if( localplayer.gate_waiting ){
-      m4x3_mulv( localplayer.gate_waiting->transport,
-                 vg_audio.external_listener_pos, 
-                 vg_audio.external_listener_pos );
-   }
-
-   v3_copy( localplayer.rb.v, vg_audio.external_lister_velocity );
-   audio_unlock();
-
-   vehicle_update_post();
-   skaterift_autosave_update();
-}
-
-/*
- * RENDERING
- * ---------------------------------------------------------------------------*/
-
-static void render_player_transparent(void)
-{
-   if( (skaterift.activity == k_skaterift_menu) &&
-       (menu.page == k_menu_page_main) && 
-       (menu.main_index == k_menu_main_guide) )
-   {
-      return;
-   }
-
-   static vg_camera small_cam;      /* DOES NOT NEED TO BE STATIC BUT MINGW 
-                                    SAIS OTHERWISE */
-
-   m4x3_copy( g_render.cam.transform, small_cam.transform );
-
-   small_cam.fov = g_render.cam.fov;
-   small_cam.nearz = 0.05f;
-   small_cam.farz  = 60.0f;
-
-   vg_camera_update_view( &small_cam );
-   vg_camera_update_projection( &small_cam );
-   vg_camera_finalize( &small_cam );
-
-   /* Draw player to window buffer and blend background ontop */
-   player__render( &small_cam );
-}
-
-static world_instance *get_view_world(void)
-{
-   if( (skaterift.activity & k_skaterift_menu) &&
-       (menu.page == k_menu_page_main) && 
-       (menu.main_index == k_menu_main_guide) )
-   {
-      return &world_static.instances[0];
-   }
-
-   world_instance *view_world = world_current_instance();
-   if( localplayer.gate_waiting && 
-         (localplayer.gate_waiting->flags & k_ent_gate_nonlocal) ){
-      view_world = &world_static.instances[world_static.active_instance ^ 0x1];
-   }
-
-   return view_world;
-}
-
-static void render_scene(void)
-{
-   /* Draw world */
-   glEnable( GL_DEPTH_TEST );
-
-   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ )
-   {
-      if( world_static.instances[i].status == k_world_status_loaded )
-      {
-         world_prerender( &world_static.instances[i] );
-      }
-   }
-
-   if( menu_viewing_map() )
-   {
-      world_instance *world = world_current_instance();
-      glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
-      
-      v3f bg;
-      v3_muls( world->ub_lighting.g_daysky_colour,
-                  world->ub_lighting.g_day_phase - 
-                  world->ub_lighting.g_sunset_phase*0.1f, bg );
-
-      v3_muladds( bg, world->ub_lighting.g_sunset_colour,
-                  (1.0f-0.5f)*world->ub_lighting.g_sunset_phase, bg );
-
-      v3_muladds( bg, world->ub_lighting.g_nightsky_colour,
-                  (1.0f-world->ub_lighting.g_day_phase), bg );
-
-      glClearColor( bg[0], bg[1], bg[2], 0.0f );
-      glClear( GL_COLOR_BUFFER_BIT );
-      glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, 
-                                    GL_COLOR_ATTACHMENT1 } );
-
-      m4x3f identity;
-      m4x3_identity( identity );
-      render_world_override( world, world, identity, &g_render.cam, 
-                             world_map.close_spawn, 
-                             (v4f){world->tar_min, world->tar_max, 1.0f, 0.0f});
-      render_world_routes( world, world, identity, &g_render.cam, 0, 1 );
-      return;
-   }
-
-   world_instance *view_world = get_view_world();
-   render_world( view_world, &g_render.cam, 0, 0, 1, 1 );
-
-   particle_system_update( &particles_grind, vg.time_delta );
-   //particle_system_debug( &particles_grind );
-   particle_system_prerender( &particles_grind );
-   particle_system_render( &particles_grind, &g_render.cam );
-   
-   ent_tornado_pre_update();
-   particle_system_update( &particles_env, vg.time_delta );
-   particle_system_prerender( &particles_env );
-   particle_system_render( &particles_env, &g_render.cam );
-
-   player_glide_render_effects( &g_render.cam );
-
-   /* 
-    * render transition 
-    */
-   if( global_miniworld.transition == 0 ) 
-      return;
-
-   world_instance *holdout_world = NULL;
-   f32 t = 0.0f;
-
-   if( global_miniworld.transition == 1 ){
-      holdout_world = &world_static.instances[ k_world_purpose_hub ];
-      t = global_miniworld.t;
-   }
-   else{
-      holdout_world = &world_static.instances[ k_world_purpose_client ];
-      t = 1.0f-global_miniworld.t;
-   }
-
-   if( holdout_world->status != k_world_status_loaded )
-      return;
-
-   t = vg_smoothstepf( t );
-
-   glEnable( GL_STENCIL_TEST );
-   glDisable( GL_DEPTH_TEST );
-   glStencilOp( GL_KEEP, GL_KEEP, GL_REPLACE );  
-   glStencilFunc( GL_ALWAYS, 1, 0xFF ); 
-   glStencilMask( 0xFF );
-
-   shader_blit_transition_use();
-   shader_blit_transition_uInverseRatio( (v2f){1.0f,1.0f} );
-   shader_blit_transition_uT( -(sqrtf(2)+0.5f) * t );
-
-   render_fsquad();
-   render_world( holdout_world, &global_miniworld.cam, 1, 0, 1, 1 );
-}
-
-static void skaterift_composite_maincamera(void)
-{
-   vg_camera_lerp( &localplayer.cam, &world_static.focus_cam,
-                vg_smoothstepf(world_static.focus_strength), &g_render.cam );
-
-   if( skaterift.activity == k_skaterift_replay )
-   {
-      if( player_replay.use_freecam )
-      {
-         freecam_preupdate();
-         v3_copy( player_replay.replay_freecam.pos, g_render.cam.pos );
-         v3_copy( player_replay.replay_freecam.angles, g_render.cam.angles );
-         g_render.cam.fov = player_replay.replay_freecam.fov;
-      }
-      else
-      {
-         skaterift_get_replay_cam( &g_render.cam );
-      }
-   }
-
-   g_render.cam.nearz = 0.1f;
-   g_render.cam.farz  = 2100.0f;
-
-   if( (skaterift.activity == k_skaterift_menu) && menu.bg_cam )
-   {
-      ent_camera_unpack( menu.bg_cam, &g_render.cam );
-   }
-
-   if( menu_viewing_map() )
-   {
-      vg_camera_copy( &world_map.cam, &g_render.cam );
-      g_render.cam.nearz = 4.0f;
-      g_render.cam.farz = 3100.0f;
-   }
-
-   if( global_miniworld.transition ){
-      f32 dt = vg.time_frame_delta / 2.0f,
-          s  = vg_signf( global_miniworld.transition );
-      global_miniworld.t += s * dt;
-
-      if( (global_miniworld.t > 1.0f) || (global_miniworld.t < 0.0f) ){
-         global_miniworld.t = vg_clampf( global_miniworld.t, 0.0f, 1.0f );
-         global_miniworld.transition = 0;
-      }
-   }
-
-   vg_camera_update_transform( &g_render.cam );
-   vg_camera_update_view( &g_render.cam );
-   vg_camera_update_projection( &g_render.cam );
-   vg_camera_finalize( &g_render.cam );
-}
-
-static void render_main_game(void)
-{
-   if( skaterift.activity == k_skaterift_replay )
-   {
-      player__animate_from_replay( &player_replay.local );
-   }
-   else{
-      player__animate();
-      skaterift_record_frame( &player_replay.local,
-                              localplayer.deferred_frame_record );
-      localplayer.deferred_frame_record = 0;
-   }
-   animate_remote_players();
-   player__pre_render();
-
-   skaterift_composite_maincamera();
-
-   /* --------------------------------------------------------------------- */
-   if( !menu_viewing_map() )
-   {
-      world_instance *world = world_current_instance();
-      render_world_cubemaps( world );
-
-      ent_gate *nlg = world->rendering_gate;
-      if( nlg && (nlg->flags & k_ent_gate_nonlocal) )
-         render_world_cubemaps( &world_static.instances[nlg->target] );
-   }
-
-   /* variable res target */
-   vg_framebuffer_bind( g_render.fb_main, k_render_scale );
-   glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT );
-
-   render_scene();
-   glEnable( GL_DEPTH_TEST );
-
-   /* full res target */
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   glViewport( 0,0, vg.window_x, vg.window_y );
-
-   render_player_transparent(); /* needs to read the depth buffer before we fuck
-                                   it up with the oblique rendering inside the
-                                   portals */
-
-   /* continue with variable rate */
-   if( !global_miniworld.transition && !menu_viewing_map() )
-   {
-      vg_framebuffer_bind( g_render.fb_main, k_render_scale );
-      render_world_gates( get_view_world(), &g_render.cam );
-   }
-
-   /* composite */
-
-   if( (skaterift.activity == k_skaterift_menu) && menu.bg_blur )
-      v2_muls( (v2f){ 0.04f, 0.001f }, 1.0f-skaterift.time_rate, 
-               g_render.blur_override );
-   else 
-      v2_zero( g_render.blur_override );
-   postprocess_to_screen( g_render.fb_main );
-
-   skaterift_replay_post_render();
-   control_overlay_render();
-}
-
-void vg_render(void)
-{
-   if( !g_client.loaded )
-   {
-      vg_loader_render();
-      return;
-   }
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-
-   glViewport( 0,0, vg.window_x, vg.window_y );
-   glDisable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-
-   glClearColor( 1.0f, 0.0f, 0.0f, 0.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-
-   render_main_game();
-   m4x4_copy( g_render.cam.mtx.pv, vg.pv );
-   
-   /* Other shite */
-   glDisable(GL_BLEND);
-   glDisable(GL_DEPTH_TEST);
-   vg_lines_drawall();
-   glViewport( 0,0, vg.window_x, vg.window_y );
-
-   gui_render_icons();
-}
-
-void vg_gui( ui_context *ctx )
-{
-   if( !g_client.loaded ) return;
-
-   gui_draw( ctx );
-
-   if( k_light_editor )
-      imgui_world_light_edit( ctx, world_current_instance() );
-   
-   vg_ui.tex_bg = g_render.fb_main->attachments[0].id;
-   vg_framebuffer_inverse_ratio( g_render.fb_main, vg_ui.bg_inverse_ratio );
-
-   menu_gui( ctx );
-   player__im_gui( ctx );
-   world_instance *world = world_current_instance();
-
-   world_routes_imgui( ctx, world );
-   skaterift_replay_imgui( ctx );
-   workshop_form_gui( ctx );
-   remote_player_network_imgui( ctx, vg.pv );
-
-   if( menu_viewing_map() )
-   {
-      remote_players_imgui_world( ctx, world_current_instance(), 
-                                  vg.pv, 2000.0f, 0 );
-      remote_players_imgui_lobby( ctx );
-   }
-   else 
-   {
-      remote_players_chat_imgui( ctx ); /* TODO: conditional */
-      remote_players_imgui_world( ctx, world_current_instance(), 
-                                  vg.pv, 100.0f, 1 );
-   }
-}
-
-#include "addon.c"
-#include "addon_types.c"
-#include "audio.c"
-#include "ent_challenge.c"
-#include "ent_glider.c"
-#include "entity.c"
-#include "ent_miniworld.c"
-#include "ent_objective.c"
-#include "ent_region.c"
-#include "ent_relay.c"
-#include "ent_route.c"
-#include "ent_skateshop.c"
-#include "ent_tornado.c"
-#include "ent_traffic.c"
-#include "freecam.c"
-#include "menu.c"
-#include "network.c"
-#include "particle.c"
-#include "player_basic_info.c"
-#include "player.c"
-#include "player_common.c"
-#include "player_dead.c"
-#include "player_drive.c"
-#include "player_effects.c"
-#include "player_glide.c"
-#include "player_ragdoll.c"
-#include "player_remote.c"
-#include "player_render.c"
-#include "player_replay.c"
-#include "player_skate.c"
-#include "player_walk.c"
-#include "render.c"
-#include "save.c"
-#include "scene.c"
-#include "steam.c"
-#include "trail.c"
-#include "vehicle.c"
-#include "workshop.c"
-#include "world_audio.c"
-#include "world.c"
-#include "world_entity.c"
-#include "world_gate.c"
-#include "world_gen.c"
-#include "world_load.c"
-#include "world_map.c"
-#include "world_physics.c"
-#include "world_render.c"
-#include "world_routes.c"
-#include "world_routes_ui.c"
-#include "world_sfd.c"
-#include "world_volumes.c"
-#include "world_water.c"
-#include "ent_npc.c"
-#include "model.c"
-#include "control_overlay.c"
-#include "ent_camera.c"
diff --git a/skaterift.h b/skaterift.h
deleted file mode 100644 (file)
index e480d2e..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma once
-#define SKATERIFT
-#define SKATERIFT_APPID 2103940
-
-#include "vg/vg_engine.h"
-#include "vg/vg_camera.h"
-
-enum skaterift_rt 
-{
-   k_skaterift_rt_workshop_preview,
-   k_skaterift_rt_server_status,
-   k_skaterift_rt_max
-};
-
-struct skaterift_globals
-{
-   f32 time_rate;
-   
-   enum skaterift_activity {
-      k_skaterift_default    = 0x00,
-      k_skaterift_replay     = 0x01,
-      k_skaterift_ent_focus  = 0x02,
-      k_skaterift_menu       = 0x04,
-   }
-   activity;
-   GLuint rt_textures[k_skaterift_rt_max];
-
-   u32 achievements;
-   int demo_mode;
-
-   const char *hub_world;
-}
-extern skaterift;
diff --git a/skaterift_lib.c b/skaterift_lib.c
deleted file mode 100644 (file)
index 95b8141..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-#define QOI_IMPLEMENTATION
-#include "vg/submodules/qoi/qoi.h"
-#include "vg/vg_platform.h"
-#include "vg/vg_m.h"
-
-u8 *qoi_encode_rgbaf32( f32 *data, u32 width, u32 height, int *length )
-{
-   u8 *buf = (u8 *)data;
-   for( u32 i=0; i<width*height*4; i ++ )
-   {
-      buf[i] = vg_clampf( data[i] * 255.0f, 0.0f, 255.0f );
-   }
-
-   qoi_desc desc = 
-   {
-      .channels=4,  
-      .colorspace=0, 
-      .width=width, 
-      .height=height
-   };
-
-   return qoi_encode( buf, &desc, length );
-}
-
-void qoi_free( u8 *ptr )
-{
-   free( ptr );
-}
-
-#include "vg/vg_tool.h"
-#include "vg/vg_tool.c"
diff --git a/skeleton.h b/skeleton.h
deleted file mode 100644 (file)
index 9729b64..0000000
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-#include "vg/vg_lines.h"
-#include "model.h"
-
-struct skeleton
-{
-   struct skeleton_bone
-   {
-      v3f co, end;
-      u32 parent;
-
-      u32 flags;
-      int defer;
-
-      mdl_keyframe kf;
-      mdl_bone *orig_bone;
-
-      u32 collider;
-      boxf hitbox;
-      const char *name;
-   }
-   *bones;
-   u32 bone_count;
-
-   struct skeleton_anim
-   {
-      const char *name;
-      u32 length;
-
-      float rate;
-      mdl_keyframe *anim_data;
-   }
-   *anims;
-   u32 anim_count;
-
-#if 0
-   m4x3f *final_mtx;
-#endif
-
-   struct skeleton_ik
-   {
-      u32 lower, upper, target, pole;
-      m3x3f ia, ib;
-   }
-   *ik;
-   u32 ik_count;
-
-   u32 
-       collider_count,
-       bindable_count;
-};
-
-static u32 skeleton_bone_id( struct skeleton *skele, const char *name )
-{
-   for( u32 i=1; i<skele->bone_count; i++ ){
-      if( !strcmp( skele->bones[i].name, name ))
-         return i;
-   }
-
-   vg_error( "skeleton_bone_id( *, \"%s\" );\n", name );
-   vg_fatal_error( "Bone does not exist\n" );
-
-   return 0;
-}
-
-static void keyframe_copy_pose( mdl_keyframe *kfa, mdl_keyframe *kfb, 
-                                   int num )
-{
-   for( int i=0; i<num; i++ )
-      kfb[i] = kfa[i];
-}
-
-
-/* apply a rotation from the perspective of root */
-static void keyframe_rotate_around( mdl_keyframe *kf, 
-                                    v3f origin, v3f offset, v4f q )
-{
-   v3f v0, co;
-   v3_add( kf->co, offset, co );
-   v3_sub( co, origin, v0 );
-   q_mulv( q, v0, v0 );
-   v3_add( v0, origin, co );
-   v3_sub( co, offset, kf->co );
-
-   q_mul( q, kf->q, kf->q );
-   q_normalize( kf->q );
-}
-
-static void keyframe_lerp( mdl_keyframe *kfa, mdl_keyframe *kfb, f32 t,
-                           mdl_keyframe *kfd ){
-   v3_lerp( kfa->co, kfb->co, t, kfd->co );
-   q_nlerp( kfa->q,  kfb->q,  t, kfd->q );
-   v3_lerp( kfa->s,  kfb->s,  t, kfd->s );
-}
-
-/*
- * Lerp between two sets of keyframes and store in dest. Rotations use Nlerp.
- */
-static void keyframe_lerp_pose( mdl_keyframe *kfa, mdl_keyframe *kfb, 
-                                float t, mdl_keyframe *kfd, int count ){
-   if( t <= 0.0001f ){
-      keyframe_copy_pose( kfa, kfd, count );
-      return;
-   }
-   else if( t >= 0.9999f ){
-      keyframe_copy_pose( kfb, kfd, count );
-      return;
-   }
-
-   for( int i=0; i<count; i++ )
-      keyframe_lerp( kfa+i, kfb+i, t, kfd+i );
-}
-
-static 
-void skeleton_lerp_pose( struct skeleton *skele,
-                         mdl_keyframe *kfa, mdl_keyframe *kfb, float t,
-                         mdl_keyframe *kfd )
-{
-   keyframe_lerp_pose( kfa, kfb, t, kfd, skele->bone_count-1 );
-}
-
-static void skeleton_copy_pose( struct skeleton *skele,
-                                   mdl_keyframe *kfa, mdl_keyframe *kfd )
-{
-   keyframe_copy_pose( kfa, kfd, skele->bone_count-1 );
-}
-
-/*
- * Sample animation between 2 closest frames using time value. Output is a
- * keyframe buffer that is allocated with an appropriate size
- */
-static void skeleton_sample_anim( struct skeleton *skele,
-                                  struct skeleton_anim *anim,
-                                  float time,
-                                  mdl_keyframe *output )
-{
-   f32 animtime  = fmodf( time*anim->rate, anim->length ),
-       animframe = floorf( animtime ),
-       t = animtime - animframe;
-
-   u32 frame = (u32)animframe % anim->length,
-       next  = (frame+1) % anim->length;
-
-   mdl_keyframe *base  = anim->anim_data + (skele->bone_count-1)*frame,
-                *nbase = anim->anim_data + (skele->bone_count-1)*next;
-
-   skeleton_lerp_pose( skele, base, nbase, t, output );
-}
-
-static int skeleton_sample_anim_clamped( struct skeleton *skele,
-                                         struct skeleton_anim *anim,
-                                         float time,
-                                         mdl_keyframe *output )
-{
-   float end = (float)(anim->length-1) / anim->rate;
-   skeleton_sample_anim( skele, anim, vg_minf( end, time ), output );
-
-   if( time > end )
-      return 0;
-   else
-      return 1;
-}
-
-typedef enum anim_apply
-{
-   k_anim_apply_always,
-   k_anim_apply_defer_ik,
-   k_anim_apply_deffered_only,
-   k_anim_apply_absolute
-}
-anim_apply;
-
-static 
-int should_apply_bone( struct skeleton *skele, u32 id, anim_apply type )
-{
-   struct skeleton_bone *sb = &skele->bones[ id ],
-                        *sp = &skele->bones[ sb->parent ];
-
-   if( type == k_anim_apply_defer_ik ){
-      if( ((sp->flags & k_bone_flag_ik) && !(sb->flags & k_bone_flag_ik)) 
-          || sp->defer )
-      {
-         sb->defer = 1;
-         return 0;
-      }
-      else{
-         sb->defer = 0;
-         return 1;
-      }
-   }
-   else if( type == k_anim_apply_deffered_only ){
-      if( sb->defer )
-         return 1;
-      else
-         return 0;
-   }
-
-   return 1;
-}
-
-/*
- * Apply block of keyframes to skeletons final pose
- */
-static void skeleton_apply_pose( struct skeleton *skele, mdl_keyframe *pose,
-                                 anim_apply passtype, m4x3f *final_mtx ){
-   if( passtype == k_anim_apply_absolute ){
-      for( u32 i=1; i<skele->bone_count; i++ ){
-         mdl_keyframe *kf = &pose[i-1];
-
-         v3f *posemtx = final_mtx[i];
-
-         q_m3x3( kf->q, posemtx );
-         m3x3_scale( posemtx, kf->s );
-         v3_copy( kf->co, posemtx[3] );
-      }
-      return;
-   }
-
-   m4x3_identity( final_mtx[0] );
-   skele->bones[0].defer = 0;
-   skele->bones[0].flags &= ~k_bone_flag_ik;
-
-   for( u32 i=1; i<skele->bone_count; i++ ){
-      struct skeleton_bone *sb = &skele->bones[i],
-                           *sp = &skele->bones[sb->parent];
-      
-      if( !should_apply_bone( skele, i, passtype ) )
-         continue;
-
-      sb->defer = 0;
-
-      /* process pose */
-      m4x3f posemtx;
-
-      v3f temp_delta;
-      v3_sub( skele->bones[i].co, skele->bones[sb->parent].co, temp_delta );
-
-      /* pose matrix */
-      mdl_keyframe *kf = &pose[i-1];
-      q_m3x3( kf->q, posemtx );
-      m3x3_scale( posemtx, kf->s );
-      v3_copy( kf->co, posemtx[3] );
-      v3_add( temp_delta, posemtx[3], posemtx[3] );
-
-      /* final matrix */
-      m4x3_mul( final_mtx[ sb->parent ], posemtx, final_mtx[i] );
-   }
-}
-
-/* 
- * Take the final matrices and decompose it into an absolute positioned anim
- */
-static void skeleton_decompose_mtx_absolute( struct skeleton *skele, 
-                                             mdl_keyframe *anim,
-                                             m4x3f *final_mtx ){
-   for( u32 i=1; i<skele->bone_count; i++ ){
-      struct skeleton_bone *sb = &skele->bones[i];
-      mdl_keyframe *kf = &anim[i-1];
-      m4x3_decompose( final_mtx[i], kf->co, kf->q, kf->s );
-   }
-}
-
-/* 
- * creates the reference inverse matrix for an IK bone, as it has an initial 
- * intrisic rotation based on the direction that the IK is setup..
- */
-static void skeleton_inverse_for_ik( struct skeleton *skele,
-                                     v3f ivaxis,
-                                     u32 id, m3x3f inverse )
-{
-   v3_copy( ivaxis, inverse[0] );
-   v3_copy( skele->bones[id].end, inverse[1] );
-   v3_normalize( inverse[1] );
-   v3_cross( inverse[0], inverse[1], inverse[2] );
-   m3x3_transpose( inverse, inverse );
-}
-
-/*
- * Creates inverse rotation matrices which the IK system uses.
- */
-static void skeleton_create_inverses( struct skeleton *skele )
-{
-   /* IK: inverse 'plane-bone space' axis '(^axis,^bone,...)[base] */
-   for( u32 i=0; i<skele->ik_count; i++ ){
-      struct skeleton_ik *ik = &skele->ik[i];
-
-      m4x3f inverse;
-      v3f iv0, iv1, ivaxis;
-      v3_sub( skele->bones[ik->target].co, skele->bones[ik->lower].co, iv0 );
-      v3_sub( skele->bones[ik->pole].co,   skele->bones[ik->lower].co, iv1 );
-      v3_cross( iv0, iv1, ivaxis );
-      v3_normalize( ivaxis );
-
-      skeleton_inverse_for_ik( skele, ivaxis, ik->lower, ik->ia );
-      skeleton_inverse_for_ik( skele, ivaxis, ik->upper, ik->ib );
-   }
-}
-
-/*
- * Apply a model matrix to all bones, should be done last
- */
-static 
-void skeleton_apply_transform( struct skeleton *skele, m4x3f transform,
-                               m4x3f *final_mtx )
-{
-   for( u32 i=0; i<skele->bone_count; i++ ){
-      struct skeleton_bone *sb = &skele->bones[i];
-      m4x3_mul( transform, final_mtx[i], final_mtx[i] );
-   }
-}
-
-/*
- * Apply an inverse matrix to all bones which maps vertices from bind space into
- * bone relative positions
- */
-static void skeleton_apply_inverses( struct skeleton *skele, m4x3f *final_mtx ){
-   for( u32 i=0; i<skele->bone_count; i++ ){
-      struct skeleton_bone *sb = &skele->bones[i];
-      m4x3f inverse;
-      m3x3_identity( inverse );
-      v3_negate( sb->co, inverse[3] );
-
-      m4x3_mul( final_mtx[i], inverse, final_mtx[i] );
-   }
-}
-
-/*
- * Apply all IK modifiers (2 bone ik reference from blender is supported)
- */
-static void skeleton_apply_ik_pass( struct skeleton *skele, m4x3f *final_mtx ){
-   for( u32 i=0; i<skele->ik_count; i++ ){
-      struct skeleton_ik *ik = &skele->ik[i];
-      
-      v3f v0, /* base -> target */
-          v1, /* base -> pole */
-          vaxis;
-
-      v3f co_base,
-          co_target,
-          co_pole;
-
-      v3_copy( final_mtx[ik->lower][3], co_base );
-      v3_copy( final_mtx[ik->target][3], co_target );
-      v3_copy( final_mtx[ik->pole][3], co_pole );
-
-      v3_sub( co_target, co_base, v0 );
-      v3_sub( co_pole, co_base, v1 );
-      v3_cross( v0, v1, vaxis );
-      v3_normalize( vaxis );
-      v3_normalize( v0 );
-      v3_cross( vaxis, v0, v1 );
-
-      /* localize problem into [x:v0,y:v1] 2d plane */
-      v2f base = { v3_dot( v0, co_base   ), v3_dot( v1, co_base   ) },
-          end  = { v3_dot( v0, co_target ), v3_dot( v1, co_target ) },
-          knee;
-
-      /* Compute angles (basic trig)*/
-      v2f delta;
-      v2_sub( end, base, delta );
-
-      float 
-         l1 = v3_length( skele->bones[ik->lower].end ),
-         l2 = v3_length( skele->bones[ik->upper].end ),
-         d = vg_clampf( v2_length(delta), fabsf(l1 - l2), l1+l2-0.00001f ),
-         c = acosf( (l1*l1 + d*d - l2*l2) / (2.0f*l1*d) ),
-         rot = atan2f( delta[1], delta[0] ) + c - VG_PIf/2.0f;
-
-      knee[0] = sinf(-rot) * l1;
-      knee[1] = cosf(-rot) * l1;
-
-      m4x3_identity( final_mtx[ik->lower] );
-      m4x3_identity( final_mtx[ik->upper] );
-
-      /* create rotation matrix */
-      v3f co_knee;
-      v3_muladds( co_base, v0, knee[0], co_knee );
-      v3_muladds( co_knee, v1, knee[1], co_knee );
-      vg_line( co_base, co_knee, 0xff00ff00 );
-
-      m4x3f transform;
-      v3_copy( vaxis, transform[0] );
-      v3_muls( v0, knee[0], transform[1] );
-      v3_muladds( transform[1], v1, knee[1], transform[1] );
-      v3_normalize( transform[1] );
-      v3_cross( transform[0], transform[1], transform[2] );
-      v3_copy( co_base, transform[3] );
-
-      m3x3_mul( transform, ik->ia, transform );
-      m4x3_copy( transform, final_mtx[ik->lower] );
-
-      /* upper/knee bone */
-      v3_copy( vaxis, transform[0] );
-      v3_sub( co_target, co_knee, transform[1] );
-      v3_normalize( transform[1] );
-      v3_cross( transform[0], transform[1], transform[2] );
-      v3_copy( co_knee, transform[3] );
-
-      m3x3_mul( transform, ik->ib, transform );
-      m4x3_copy( transform, final_mtx[ik->upper] );
-   }
-}
-
-/*
- * Applies the typical operations that you want for an IK rig: 
- *    Pose, IK, Pose(deferred), Inverses, Transform
- */
-static void skeleton_apply_standard( struct skeleton *skele, mdl_keyframe *pose,
-                                     m4x3f transform, m4x3f *final_mtx ){
-   skeleton_apply_pose( skele, pose, k_anim_apply_defer_ik, final_mtx );
-   skeleton_apply_ik_pass( skele, final_mtx );
-   skeleton_apply_pose( skele, pose, k_anim_apply_deffered_only, final_mtx );
-   skeleton_apply_inverses( skele, final_mtx );
-   skeleton_apply_transform( skele, transform, final_mtx );
-}
-
-/*
- * Get an animation by name
- */
-static struct skeleton_anim *skeleton_get_anim( struct skeleton *skele,
-                                                   const char *name ){
-   for( u32 i=0; i<skele->anim_count; i++ ){
-      struct skeleton_anim *anim = &skele->anims[i];
-
-      if( !strcmp( anim->name, name ) )
-         return anim;
-   }
-
-   vg_error( "skeleton_get_anim( *, \"%s\" )\n", name );
-   vg_fatal_error( "Invalid animation name\n" );
-
-   return NULL;
-}
-
-static void skeleton_alloc_from( struct skeleton *skele,
-                                    void *lin_alloc,
-                                    mdl_context *mdl,
-                                    mdl_armature *armature ){
-   skele->bone_count     = armature->bone_count+1;
-   skele->anim_count     = armature->anim_count;
-   skele->ik_count       = 0;
-   skele->collider_count = 0;
-
-   for( u32 i=0; i<armature->bone_count; i++ ){
-      mdl_bone *bone = mdl_arritm( &mdl->bones, armature->bone_start+i );
-
-      if( bone->flags & k_bone_flag_ik )
-         skele->ik_count ++;
-
-      if( bone->collider )
-         skele->collider_count ++;
-   }
-
-   u32 bone_size = sizeof(struct skeleton_bone) * skele->bone_count,
-       ik_size   = sizeof(struct skeleton_ik)   * skele->ik_count,
-       mtx_size  = sizeof(m4x3f)                * skele->bone_count,
-       anim_size = sizeof(struct skeleton_anim) * skele->anim_count;
-
-   skele->bones      = vg_linear_alloc( lin_alloc, bone_size );
-   skele->ik         = vg_linear_alloc( lin_alloc, ik_size );
-   //skele->final_mtx  = vg_linear_alloc( lin_alloc, mtx_size );
-   skele->anims      = vg_linear_alloc( lin_alloc, anim_size );
-
-   memset( skele->bones, 0, bone_size );
-   memset( skele->ik, 0, ik_size );
-   //memset( skele->final_mtx, 0, mtx_size );
-   memset( skele->anims, 0, anim_size );
-}
-
-static void skeleton_fatal_err(void){
-   vg_fatal_error( "Skeleton setup failed" );
-}
-
-/* Setup an animated skeleton from model. mdl's metadata should stick around */
-static void skeleton_setup( struct skeleton *skele,
-                            void *lin_alloc, mdl_context *mdl ){
-   u32 ik_count = 0, collider_count = 0;
-   skele->bone_count = 0;
-   skele->bones = NULL;
-   //skele->final_mtx = NULL;
-   skele->anims = NULL;
-
-   if( !mdl->armatures.count ){
-      vg_error( "No skeleton in model\n" );
-      skeleton_fatal_err();
-   }
-
-   mdl_armature *armature = mdl_arritm( &mdl->armatures, 0 );
-   skeleton_alloc_from( skele, lin_alloc, mdl, armature );
-
-   for( u32 i=0; i<armature->bone_count; i++ ){
-      mdl_bone *bone = mdl_arritm( &mdl->bones, armature->bone_start+i );
-      struct skeleton_bone *sb = &skele->bones[i+1];
-
-      v3_copy( bone->co, sb->co );
-      v3_copy( bone->end, sb->end );
-
-      sb->parent = bone->parent;
-      sb->name   = mdl_pstr( mdl, bone->pstr_name );
-      sb->flags  = bone->flags;
-      sb->collider = bone->collider;
-      sb->orig_bone = bone;
-
-      if( sb->flags & k_bone_flag_ik ){
-         skele->bones[ sb->parent ].flags |= k_bone_flag_ik;
-         
-         if( ik_count == skele->ik_count ){
-            vg_error( "Too many ik bones, corrupt model file\n" );
-            skeleton_fatal_err();
-         }
-
-         struct skeleton_ik *ik = &skele->ik[ ik_count ++ ];
-         ik->upper = i+1;
-         ik->lower = bone->parent;
-         ik->target = bone->ik_target;
-         ik->pole = bone->ik_pole;
-      }
-
-      box_copy( bone->hitbox, sb->hitbox );
-
-      if( bone->collider ){
-         if( collider_count == skele->collider_count ){
-            vg_error( "Too many collider bones\n" );
-            skeleton_fatal_err();
-         }
-
-         collider_count ++;
-      }
-   }
-
-   /* fill in implicit root bone */
-   v3_zero( skele->bones[0].co );
-   v3_copy( (v3f){0.0f,1.0f,0.0f}, skele->bones[0].end );
-   skele->bones[0].parent = 0xffffffff;
-   skele->bones[0].flags = 0;
-   skele->bones[0].name = "[root]";
-   
-   /* process animation quick refs */
-   for( u32 i=0; i<skele->anim_count; i++ ){
-      mdl_animation *anim = 
-         mdl_arritm( &mdl->animations, armature->anim_start+i );
-
-      skele->anims[i].rate       = anim->rate;
-      skele->anims[i].length     = anim->length;
-      skele->anims[i].name       = mdl_pstr(mdl, anim->pstr_name);
-      skele->anims[i].anim_data  = 
-         mdl_arritm( &mdl->keyframes, anim->offset );
-
-      vg_info( "animation[ %f, %u ] '%s'\n", anim->rate,
-                                             anim->length,
-                                             skele->anims[i].name );
-   }
-
-   skeleton_create_inverses( skele );
-   vg_success( "Loaded skeleton with %u bones\n", skele->bone_count );
-   vg_success( "                     %u colliders\n", skele->collider_count );
-}
-
-static void skeleton_debug( struct skeleton *skele, m4x3f *final_mtx ){
-   for( u32 i=1; i<skele->bone_count; i ++ ){
-      struct skeleton_bone *sb = &skele->bones[i];
-
-      v3f p0, p1;
-      v3_copy( sb->co, p0 );
-      v3_add( p0, sb->end, p1 );
-
-      m4x3_mulv( final_mtx[i], p0, p0 );
-      m4x3_mulv( final_mtx[i], p1, p1 );
-
-      if( sb->flags & k_bone_flag_deform ){
-         if( sb->flags & k_bone_flag_ik ){
-            vg_line( p0, p1, 0xff0000ff );
-         }
-         else{
-            vg_line( p0, p1, 0xffcccccc );
-         }
-      }
-      else
-         vg_line( p0, p1, 0xff00ffff );
-   }
-}
diff --git a/src/addon.c b/src/addon.c
new file mode 100644 (file)
index 0000000..6769b5a
--- /dev/null
@@ -0,0 +1,881 @@
+#include "vg/vg_engine.h"
+#include "vg/vg_io.h"
+#include "vg/vg_loader.h"
+#include "addon.h"
+#include "addon_types.h"
+#include "vg/vg_msg.h"
+#include "steam.h"
+#include "workshop.h"
+#include <string.h>
+
+struct addon_system addon_system;
+
+u32 addon_count( enum addon_type type, u32 ignoreflags )
+{
+   if( ignoreflags ){
+      u32 typecount = 0, count = 0;
+      for( u32 i=0; typecount<addon_count( type, 0 ); i++ ){
+         addon_reg *reg = &addon_system.registry[i];
+         if( reg->alias.type == type ){
+            typecount ++;
+
+            if( reg->flags & ignoreflags )
+               continue;
+            
+            count ++;
+         }
+      }
+
+      return count;
+   }
+   else
+      return addon_system.registry_type_counts[ type ];
+}
+
+
+/* these kind of suck, oh well. */
+addon_reg *get_addon_from_index( enum addon_type type, u32 index, 
+                                 u32 ignoreflags )
+{
+   u32 typecount = 0, count = 0;
+   for( u32 i=0; typecount<addon_count(type,0); i++ ){
+      addon_reg *reg = &addon_system.registry[i];
+      if( reg->alias.type == type ){
+         typecount ++;
+
+         if( reg->flags & ignoreflags )
+            continue;
+
+         if( index == count )
+            return reg;
+
+         count ++;
+      }
+   }
+
+   return NULL;
+}
+
+u32 get_index_from_addon( enum addon_type type, addon_reg *a )
+{
+   u32 count = 0;
+   for( u32 i=0; count<addon_system.registry_type_counts[type]; i++ ){
+      addon_reg *reg = &addon_system.registry[i];
+      if( reg->alias.type == type ){
+         if( reg == a )
+            return count;
+
+         count ++;
+      }
+   }
+
+   return 0xffffffff;
+}
+
+u32 addon_match( addon_alias *alias )
+{
+   if( alias->type == k_addon_type_none ) return 0xffffffff;
+
+   u32 foldername_djb2 = 0;
+   if( !alias->workshop_id )
+      foldername_djb2 = vg_strdjb2( alias->foldername );
+
+   u32 count = 0;
+   for( u32 i=0; count<addon_system.registry_type_counts[alias->type]; i++ ){
+      addon_reg *reg = &addon_system.registry[i];
+      if( reg->alias.type == alias->type ){
+         
+         if( alias->workshop_id ){
+            if( alias->workshop_id == reg->alias.workshop_id )
+               return count;
+         }
+         else{
+            if( reg->foldername_hash == foldername_djb2 ){
+               if( !strcmp( reg->alias.foldername, alias->foldername ) ){
+                  return count;
+               }
+            }
+         }
+
+         count ++;
+      }
+   }
+
+   return 0xffffffff;
+}
+
+/*
+ * Create a string version of addon alias in buf
+ */
+void addon_alias_uid( addon_alias *alias, char buf[ADDON_UID_MAX] )
+{
+   if( alias->workshop_id ){
+      snprintf( buf, 128, "sr%03d-steam-"PRINTF_U64, 
+            alias->type, alias->workshop_id );
+   }
+   else {
+      snprintf( buf, 128, "sr%03d-local-%s",
+            alias->type, alias->foldername );
+   }
+}
+
+/*
+ * equality check
+ */
+int addon_alias_eq( addon_alias *a, addon_alias *b )
+{
+   if( a->type == b->type ){
+      if( a->workshop_id == b->workshop_id ){
+         if( a->workshop_id )
+            return 1;
+         else
+            return !strcmp( a->foldername, b->foldername );
+      }
+      else
+         return 0;
+   }
+   else return 0;
+}
+
+/*
+ * make alias represent NULL.
+ */
+void invalidate_addon_alias( addon_alias *alias )
+{
+   alias->type = k_addon_type_none;
+   alias->workshop_id = 0;
+   alias->foldername[0] = '\0';
+}
+
+/*
+ * parse uid to alias. returns 1 if successful
+ */
+int addon_uid_to_alias( const char *uid, addon_alias *alias )
+{
+/*           1
+ * 01234567890123
+ * sr&&&-@@@@@-#*
+ *    |    |   |
+ *  type   |   id
+ *         |
+ *     location
+ */
+   if( strlen(uid) < 13 ){
+      invalidate_addon_alias( alias );
+      return 0;
+   }
+   if( !((uid[0] == 's') && (uid[1] == 'r')) ){
+      invalidate_addon_alias( alias );
+      return 0;
+   }
+
+   char type[4];
+   memcpy( type, uid+2, 3 );
+   type[3] = '\0';
+   alias->type = atoi(type);
+
+   char location[6];
+   memcpy( location, uid+6, 5 );
+   location[5] = '\0';
+
+   if( !strcmp(location,"steam") )
+      alias->workshop_id = atoll( uid+12 );
+   else if( !strcmp(location,"local") ){
+      alias->workshop_id = 0;
+      vg_strncpy( uid+12, alias->foldername, 64, k_strncpy_always_add_null );
+   }
+   else{
+      invalidate_addon_alias( alias );
+      return 0;
+   }
+
+   return 1;
+}
+
+void addon_system_init( void )
+{
+   u32 reg_size   = sizeof(addon_reg)*ADDON_MOUNTED_MAX;
+   addon_system.registry = vg_linear_alloc( vg_mem.rtmemory, reg_size );
+
+   for( u32 type=0; type<k_addon_type_max; type++ ){
+      struct addon_type_info *inf = &addon_type_infos[type];
+      struct addon_cache *cache = &addon_system.cache[type];
+
+      if( inf->cache_count ){
+         /* create the allocations pool */
+         u32 alloc_size = sizeof(struct addon_cache_entry)*inf->cache_count;
+         cache->allocs = vg_linear_alloc( vg_mem.rtmemory, alloc_size );
+         memset( cache->allocs, 0, alloc_size );
+
+         cache->pool.buffer = cache->allocs;
+         cache->pool.count  = inf->cache_count;
+         cache->pool.stride = sizeof( struct addon_cache_entry );
+         cache->pool.offset = offsetof( struct addon_cache_entry, poolnode );
+         vg_pool_init( &cache->pool );
+
+         /* create the real memory */
+         u32 cache_size = inf->cache_stride*inf->cache_count;
+         cache->items = vg_linear_alloc( vg_mem.rtmemory, cache_size );
+         cache->stride = inf->cache_stride;
+         memset( cache->items, 0, cache_size );
+
+         for( i32 j=0; j<inf->cache_count; j++ ){
+            struct addon_cache_entry *alloc = &cache->allocs[j];
+            alloc->reg_ptr   = NULL;
+            alloc->reg_index = 0xffffffff;
+         }
+      }
+   }
+}
+
+/*
+ * Scanning routines
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * Reciever for scan completion. copies the registry counts back into main fred
+ */
+void async_addon_reg_update( void *data, u32 size )
+{
+   vg_info( "Registry update notify\n" );
+   
+   for( u32 i=0; i<k_addon_type_max; i++ ){
+      addon_system.registry_type_counts[i] = 0;
+   }
+
+   for( u32 i=0; i<addon_system.registry_count; i++ ){
+      enum addon_type type = addon_system.registry[i].alias.type;
+      addon_system.registry_type_counts[ type ] ++;
+   }
+}
+
+static void addon_set_foldername( addon_reg *reg, const char name[64] ){
+   vg_strncpy( name, reg->alias.foldername, 64, k_strncpy_always_add_null );
+   reg->foldername_hash = vg_strdjb2( reg->alias.foldername );
+}
+
+/*
+ * Create a new registry 
+ */
+static addon_reg *addon_alloc_reg( PublishedFileId_t workshop_id,
+                                      enum addon_type type ){
+   if( addon_system.registry_count == ADDON_MOUNTED_MAX ){
+      vg_error( "You have too many addons installed!\n" );
+      return NULL;
+   }
+
+   addon_reg *reg = &addon_system.registry[ addon_system.registry_count ];
+   reg->flags = 0;
+   reg->metadata_len = 0;
+   reg->cache_id = 0;
+   reg->state = k_addon_state_indexed;
+   reg->alias.workshop_id = workshop_id;
+   reg->alias.foldername[0] = '\0';
+   reg->alias.type = type;
+
+   if( workshop_id ){
+      char foldername[64];
+      snprintf( foldername, 64, PRINTF_U64, workshop_id );
+      addon_set_foldername( reg, foldername );
+   }
+   return reg;
+}
+
+/*
+ * If the addon.inf exists int the folder, load into the reg
+ */
+static int addon_try_load_metadata( addon_reg *reg, vg_str folder_path ){
+   vg_str meta_path = folder_path;
+   vg_strcat( &meta_path, "/addon.inf" );
+   if( !vg_strgood( &meta_path ) ){
+      vg_error( "The metadata path is too long\n" );
+      return 0;
+   }
+
+   FILE *fp = fopen( meta_path.buffer, "rb" );
+   if( !fp ){
+      vg_error( "Could not open the '%s'\n", meta_path.buffer );
+      return 0;
+   }
+
+   reg->metadata_len = fread( reg->metadata, 1, 512, fp );
+   if( reg->metadata_len != 512 ){
+      if( !feof(fp) ){
+         fclose(fp);
+         vg_error( "unknown error codition" );
+         reg->metadata_len = 0;
+         return 0;
+      }
+   }
+   fclose(fp);
+   return 1;
+}
+
+static void addon_print_info( addon_reg *reg ){
+   vg_info( "addon_reg #%u{\n", addon_system.registry_count );
+   vg_info( "  type: %d\n", reg->alias.type );
+   vg_info( "  workshop_id: " PRINTF_U64 "\n", reg->alias.workshop_id );
+   vg_info( "  folder: [%u]%s\n", reg->foldername_hash, reg->alias.foldername );
+   vg_info( "  metadata_len: %u\n", reg->metadata_len );
+   vg_info( "  cache_id: %hu\n", reg->cache_id );
+   vg_info( "}\n" );
+}
+
+static void addon_mount_finish( addon_reg *reg ){
+#if 0
+   addon_print_info( reg );
+#endif
+   addon_system.registry_count ++;
+}
+
+/*
+ * Mount a fully packaged addon, one that certainly has a addon.inf
+ */
+static addon_reg *addon_mount_workshop_folder( PublishedFileId_t workshop_id,
+                                                  vg_str folder_path )
+{
+   addon_reg *reg = addon_alloc_reg( workshop_id, k_addon_type_none );
+   if( !reg ) return NULL;
+
+   if( !addon_try_load_metadata( reg, folder_path ) ){
+      return NULL;
+   }
+
+   enum addon_type type = k_addon_type_none;
+   vg_msg msg;
+   vg_msg_init( &msg, reg->metadata, reg->metadata_len );
+
+   if( vg_msg_seekframe( &msg, "workshop" ))
+   {
+      vg_msg_getkvintg( &msg, "type", k_vg_msg_u32, &type, NULL );
+   }
+
+   if( type == k_addon_type_none )
+   {
+      vg_error( "Cannot determine addon type\n" );
+      return NULL;
+   }
+
+   reg->alias.type = type;
+   addon_mount_finish( reg );
+   return reg;
+}
+
+/*
+ * Mount a local folder. may or may not have addon.inf
+ */
+addon_reg *addon_mount_local_addon( const char *folder,
+                                    enum addon_type type,
+                                    const char *content_ext )
+{
+   char folder_path_buf[4096];
+   vg_str folder_path;
+   vg_strnull( &folder_path, folder_path_buf, 4096 );
+   vg_strcat( &folder_path, folder );
+
+   const char *folder_name = vg_strch( &folder_path, '/' )+1;
+   u32 folder_hash = vg_strdjb2(folder_name);
+   for( u32 i=0; i<addon_system.registry_count; i++ ){
+      addon_reg *reg = &addon_system.registry[i];
+
+      if( (reg->alias.type == type) && (reg->foldername_hash == folder_hash) ){
+         if( !strcmp( reg->alias.foldername, folder_name ) ){
+            reg->state = k_addon_state_indexed;
+            return reg;
+         }
+      }
+   }
+
+   addon_reg *reg = addon_alloc_reg( 0, type );
+   if( !reg ) return NULL;
+   addon_set_foldername( reg, folder_name );
+   addon_try_load_metadata( reg, folder_path );
+
+   if( reg->metadata_len == 0 ){
+      /* create our own content commands */
+      vg_msg msg;
+      vg_msg_init( &msg, reg->metadata, sizeof(reg->metadata) );
+
+      u32 content_count = 0;
+
+      vg_strcat( &folder_path, "" );
+      vg_warn( "Creating own metadata for: %s\n", folder_path.buffer );
+
+      vg_dir subdir;
+      if( !vg_dir_open(&subdir, folder_path.buffer) ){
+         vg_error( "Failed to open '%s'\n", folder_path.buffer );
+         return NULL;
+      }
+
+      while( vg_dir_next_entry(&subdir) ){
+         if( vg_dir_entry_type(&subdir) == k_vg_entry_type_file ){
+            const char *fname = vg_dir_entry_name(&subdir);
+            vg_str file = folder_path;
+            vg_strcat( &file, "/" );
+            vg_strcat( &file, fname );
+            if( !vg_strgood( &file ) ) continue;
+
+            char *ext = vg_strch( &file, '.' );
+            if( !ext ) continue;
+            if( strcmp(ext,content_ext) ) continue;
+            
+            vg_msg_wkvstr( &msg, "content", fname );
+            content_count ++;
+         }
+      }
+      vg_dir_close(&subdir);
+
+      if( !content_count ) return NULL;
+      if( msg.error == k_vg_msg_error_OK )
+         reg->metadata_len = msg.cur.co;
+      else{
+         vg_error( "Error creating metadata: %d\n", msg.error );
+         return NULL;
+      }
+   }
+
+   addon_mount_finish( reg );
+   return reg;
+}
+
+/*
+ * Check all subscribed items
+ */
+void addon_mount_workshop_items(void)
+{
+   if( skaterift.demo_mode ){
+      vg_info( "Won't load workshop items in demo mode\n" );
+      return;
+   }
+   if( !steam_ready ) return;
+
+   /*
+    * Steam workshop scan
+    */
+   vg_info( "Mounting steam workshop subscriptions\n" );
+   PublishedFileId_t workshop_ids[ ADDON_MOUNTED_MAX ];
+   u32 workshop_count = ADDON_MOUNTED_MAX;
+
+   vg_async_item *call = vg_async_alloc(
+                           sizeof(struct async_workshop_installed_files_info));
+   struct async_workshop_installed_files_info *info = call->payload;
+   info->buffer = workshop_ids;
+   info->len = &workshop_count;
+   vg_async_dispatch( call, async_workshop_get_installed_files );
+   vg_async_stall();
+
+   for( u32 j=0; j<workshop_count; j++ ){
+      /* check for existance in both our caches
+       * ----------------------------------------------------------*/
+      PublishedFileId_t id = workshop_ids[j];
+      for( u32 i=0; i<addon_system.registry_count; i++ ){
+         addon_reg *reg = &addon_system.registry[i];
+
+         if( reg->alias.workshop_id == id ){
+            reg->state = k_addon_state_indexed;
+            goto next_file_workshop;
+         }
+      }
+
+      vg_async_item *call1 = 
+         vg_async_alloc( sizeof(struct async_workshop_filepath_info) );
+
+      char path[ 4096 ];
+
+      struct async_workshop_filepath_info *info = call1->payload;
+      info->buf = path;
+      info->id = id;
+      info->len = VG_ARRAY_LEN(path);
+      vg_async_dispatch( call1, async_workshop_get_filepath );
+      vg_async_stall(); /* too bad! */
+
+      vg_str folder = {.buffer = path, .i=strlen(path), .len=4096};
+      addon_mount_workshop_folder( id, folder );
+next_file_workshop:;
+   }
+}
+
+/*
+ * Scan a local content folder for addons. It must find at least one file with 
+ * the specified content_ext to be considered.
+ */
+void addon_mount_content_folder( enum addon_type type,
+                                 const char *base_folder, 
+                                 const char *content_ext )
+{
+   vg_info( "Mounting addons(type:%d) matching skaterift/%s/*/*%s\n", 
+                  type, base_folder, content_ext );
+
+   char path_buf[4096];
+   vg_str path;
+   vg_strnull( &path, path_buf, 4096 );
+   vg_strcat( &path, base_folder );
+
+   vg_dir dir;
+   if( !vg_dir_open(&dir,path.buffer) ){
+      vg_error( "vg_dir_open('%s') failed\n", path.buffer );
+      return;
+   }
+
+   vg_strcat(&path,"/");
+
+   while( vg_dir_next_entry(&dir) ){
+      if( vg_dir_entry_type(&dir) == k_vg_entry_type_dir ){
+         const char *d_name = vg_dir_entry_name(&dir);
+
+         vg_str folder = path;
+         if( strlen( d_name ) > ADDON_FOLDERNAME_MAX ){
+            vg_warn( "folder too long: %s\n", d_name );
+            continue;
+         }
+
+         vg_strcat( &folder, d_name );
+         if( !vg_strgood( &folder ) ) continue;
+
+         addon_mount_local_addon( folder.buffer, type, content_ext );
+      }
+   }
+   vg_dir_close(&dir);
+}
+
+/*
+ * write the full path of the addon's folder into the vg_str
+ */
+int addon_get_content_folder( addon_reg *reg, vg_str *folder, int async)
+{
+   if( reg->alias.workshop_id ){
+      struct async_workshop_filepath_info *info = NULL;
+      vg_async_item *call = NULL;
+
+      if( async ){
+         call = vg_async_alloc( sizeof(struct async_workshop_filepath_info) );
+         info = call->payload;
+      }
+      else 
+         info = alloca( sizeof(struct async_workshop_filepath_info) );
+
+      info->buf = folder->buffer;
+      info->id = reg->alias.workshop_id;
+      info->len = folder->len;
+
+      if( async ){
+         vg_async_dispatch( call, async_workshop_get_filepath );
+         vg_async_stall(); /* too bad! */
+      }
+      else {
+         async_workshop_get_filepath( info, 0 );
+      }
+
+      if( info->buf[0] == '\0' ){
+         vg_error( "Failed SteamAPI_GetItemInstallInfo(" PRINTF_U64 ")\n",
+                     reg->alias.workshop_id );
+         return 0;
+      }
+      folder->i = strlen( folder->buffer );
+      return 1;
+   }
+   else{
+      folder->i = 0;
+      
+      const char *local_folder = 
+         addon_type_infos[reg->alias.type].local_content_folder;
+
+      if( !local_folder ) return 0;
+      vg_strcat( folder, local_folder );
+      vg_strcat( folder, reg->alias.foldername );
+      return 1;
+   }
+}
+
+/*
+ * Return existing cache id if reg_index points to a registry with its cache
+ * already set.
+ */
+u16 addon_cache_fetch( enum addon_type type, u32 reg_index )
+{
+   addon_reg *reg = NULL;
+
+   if( reg_index < addon_count( type, 0 ) ){
+      reg = get_addon_from_index( type, reg_index, 0 );
+      if( reg->cache_id ) 
+         return reg->cache_id;
+   }
+
+   return 0;
+}
+
+/*
+ * Allocate a new cache item from the pool
+ */
+u16 addon_cache_alloc( enum addon_type type, u32 reg_index )
+{
+   struct addon_cache *cache = &addon_system.cache[ type ];
+
+   u16 new_id = vg_pool_lru( &cache->pool );
+   struct addon_cache_entry *new_entry = vg_pool_item( &cache->pool, new_id );
+
+   addon_reg *reg = NULL;
+   if( reg_index < addon_count( type, 0 ) )
+      reg = get_addon_from_index( type, reg_index, 0 );
+
+   if( new_entry ){
+      if( new_entry->reg_ptr )
+         new_entry->reg_ptr->cache_id = 0;
+
+      if( reg )
+         reg->cache_id = new_id;
+
+      new_entry->reg_ptr = reg;
+      new_entry->reg_index = reg_index;
+      return new_id;
+   }
+   else{
+      vg_error( "cache full (type: %u)!\n", type );
+      return 0;
+   }
+}
+
+/*
+ * Get the real item data for cache id 
+ */
+void *addon_cache_item( enum addon_type type, u16 id )
+{
+   if( !id ) return NULL;
+
+   struct addon_cache *cache = &addon_system.cache[type];
+   return cache->items + ((size_t)(id-1) * cache->stride);
+}
+
+/*
+ * Get the real item data for cache id ONLY if the item is completely loaded.
+ */
+void *addon_cache_item_if_loaded( enum addon_type type, u16 id )
+{
+   if( !id ) return NULL;
+
+   struct addon_cache *cache = &addon_system.cache[type];
+   struct addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
+
+   if( entry->state == k_addon_cache_state_loaded )
+      return addon_cache_item( type, id );
+   else return NULL;
+}
+
+/* 
+ * Updates the item state from the main thread
+ */
+void async_addon_setstate( void *_entry, u32 _state )
+{
+   addon_cache_entry *entry = _entry;
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+   entry->state = _state;
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+   vg_success( "   loaded (%s)\n", entry->reg_ptr->alias.foldername );
+}
+
+/*
+ * Handles the loading of an individual item
+ */
+static int addon_cache_load_request( enum addon_type type, u16 id,
+                                     addon_reg *reg, vg_str folder ){
+   
+   /* load content files
+    * --------------------------------- */
+   vg_str content_path = folder;
+
+   vg_msg msg;
+   vg_msg_init( &msg, reg->metadata, reg->metadata_len );
+
+   const char *kv_content = vg_msg_getkvstr( &msg, "content" );
+   if( kv_content ){
+      vg_strcat( &content_path, "/" );
+      vg_strcat( &content_path, kv_content );
+   }
+   else{
+      vg_error( "   No content paths in metadata\n" );
+      return 0;
+   }
+
+   if( !vg_strgood( &content_path ) ) {
+      vg_error( "   Metadata path too long\n" );
+      return 0;
+   }
+   
+   if( type == k_addon_type_board ){
+      struct player_board *board = addon_cache_item( type, id );
+      player_board_load( board, content_path.buffer );
+      return 1;
+   }
+   else if( type == k_addon_type_player ){
+      struct player_model *model = addon_cache_item( type, id );
+      player_model_load( model, content_path.buffer );
+      return 1;
+   }
+   else {
+      return 0;
+   }
+
+   return 0;
+}
+
+static void addon_cache_free_item( enum addon_type type, u16 id ){
+   if( type == k_addon_type_board ){
+      struct player_board *board = addon_cache_item( type, id );
+      player_board_unload( board );
+   }
+   else if( type == k_addon_type_player ){
+      struct player_model *model = addon_cache_item( type, id );
+      player_model_unload( model );
+   }
+}
+
+/*
+ * Goes over cache item load requests and calls the above ^
+ */
+static void T1_addon_cache_load_loop(void *_)
+{
+   vg_info( "Running load loop\n" );
+   char path_buf[4096];
+
+   for( u32 type=0; type<k_addon_type_max; type++ )
+   {
+      struct addon_cache *cache = &addon_system.cache[type];
+
+      for( u32 id=1; id<=cache->pool.count; id++ )
+      {
+         addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
+
+         SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+         if( entry->state == k_addon_cache_state_load_request )
+         {
+            vg_info( "process cache load request (%u#%u, reg:%u)\n",
+                        type, id, entry->reg_index );
+
+            if( entry->reg_index >= addon_count(type,0) )
+            {
+               /* should maybe have a different value for this case */
+               entry->state = k_addon_cache_state_none;
+               SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+               continue;
+            }
+
+            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+
+            /* continue with the request */
+            addon_reg *reg = get_addon_from_index( type, entry->reg_index, 0 );
+            entry->reg_ptr = reg;
+
+            vg_str folder;
+            vg_strnull( &folder, path_buf, 4096 );
+            if( addon_get_content_folder( reg, &folder, 1 ) )
+            {
+               if( addon_cache_load_request( type, id, reg, folder ) )
+               {
+                  vg_async_call( async_addon_setstate, 
+                                 entry, k_addon_cache_state_loaded );
+                  continue;
+               }
+            }
+            
+            vg_warn( "cache item did not load (%u#%u)\n", type, id );
+            SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+            entry->state = k_addon_cache_state_none;
+            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+         }
+         else
+            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+      }
+   }
+}
+
+void addon_system_pre_update(void)
+{
+   if( !vg_loader_availible() ) return;
+
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+   for( u32 type=0; type<k_addon_type_max; type++ )
+   {
+      struct addon_cache *cache = &addon_system.cache[type];
+
+      for( u32 id=1; id<=cache->pool.count; id++ )
+      {
+         addon_cache_entry *entry = vg_pool_item( &cache->pool, id );
+         if( entry->state == k_addon_cache_state_load_request )
+         {
+            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+            vg_loader_start( T1_addon_cache_load_loop, NULL );
+            return;
+         }
+      }
+   }
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+}
+
+/*
+ * Perform the cache interactions required to create a viewslot which will
+ * eventually be loaded by other parts of the system.
+ */
+u16 addon_cache_create_viewer( enum addon_type type, u16 reg_id )
+{
+   struct addon_cache *cache = &addon_system.cache[type];
+   vg_pool *pool = &cache->pool;
+
+   u16 cache_id = addon_cache_fetch( type, reg_id );
+   if( !cache_id ){
+      cache_id = addon_cache_alloc( type, reg_id );
+
+      if( cache_id ){
+         SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+         addon_cache_entry *entry = vg_pool_item( pool, cache_id );
+
+         if( entry->state == k_addon_cache_state_loaded ){
+            addon_cache_free_item( type, cache_id );
+         }
+
+         entry->state = k_addon_cache_state_load_request;
+         SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+      }
+   }
+
+   if( cache_id )
+      vg_pool_watch( pool, cache_id );
+
+   return cache_id;
+}
+
+u16 addon_cache_create_viewer_from_uid( enum addon_type type,
+                                        char uid[ADDON_UID_MAX] )
+{
+   addon_alias q;
+   if( !addon_uid_to_alias( uid, &q ) ) return 0;
+   if( q.type != type ) return 0;
+
+   u32 reg_id = addon_match( &q );
+
+   if( reg_id == 0xffffffff ){
+      vg_warn( "We dont have the addon '%s' installed.\n", uid );
+      return 0;
+   }
+   else {
+      return addon_cache_create_viewer( type, reg_id );
+   }
+}
+
+void addon_cache_watch( enum addon_type type, u16 cache_id )
+{
+   if( !cache_id ) return;
+
+   struct addon_cache *cache = &addon_system.cache[type];
+   vg_pool *pool = &cache->pool;
+   vg_pool_watch( pool, cache_id );
+}
+
+void addon_cache_unwatch( enum addon_type type, u16 cache_id )
+{
+   if( !cache_id ) return;
+
+   struct addon_cache *cache = &addon_system.cache[type];
+   vg_pool *pool = &cache->pool;
+   vg_pool_unwatch( pool, cache_id );
+}
diff --git a/src/addon.h b/src/addon.h
new file mode 100644 (file)
index 0000000..c43277c
--- /dev/null
@@ -0,0 +1,108 @@
+#pragma once
+#include "vg/vg_steam_ugc.h"
+#include "vg/vg_mem_pool.h"
+#include "vg/vg_string.h"
+#include "addon_types.h"
+
+typedef struct addon_reg addon_reg;
+typedef struct addon_cache_entry addon_cache_entry;
+typedef struct addon_alias addon_alias;
+
+struct addon_alias 
+{
+   enum addon_type type;
+   PublishedFileId_t workshop_id;
+   char foldername[ ADDON_FOLDERNAME_MAX ];
+};
+
+#define ADDON_REG_HIDDEN   0x1
+#define ADDON_REG_MTZERO   0x2
+#define ADDON_REG_CITY     0x4
+#define ADDON_REG_PREMIUM  0x8
+
+struct addon_system
+{
+   struct addon_reg
+   {
+      addon_alias alias;
+      u32 foldername_hash;
+      u8 metadata[512];  /* vg_msg buffer */
+      u32 metadata_len;
+      u32 flags;
+
+      u16 cache_id;
+
+      enum addon_state{
+         k_addon_state_none,
+         k_addon_state_indexed,
+         k_addon_state_indexed_absent /* gone but not forgotten */
+      }
+      state;
+   }
+   *registry;
+   u32 registry_count;
+
+   /* deffered: updates in main thread */
+   u32 registry_type_counts[k_addon_type_max];
+
+   struct addon_cache
+   {
+      struct addon_cache_entry
+      {
+         u32 reg_index;
+         addon_reg *reg_ptr;     /* TODO: only use reg_index? */
+
+         vg_pool_node poolnode;
+
+         enum addon_cache_state{
+            k_addon_cache_state_none,
+            k_addon_cache_state_loaded,
+            k_addon_cache_state_load_request
+         }
+         state;
+      }
+      *allocs;
+      vg_pool pool;
+
+      void *items;  /* the real data */
+      size_t stride;
+   }
+   cache[k_addon_type_max];
+   SDL_SpinLock sl_cache_using_resources;
+}
+extern addon_system;
+
+void addon_system_init( void );
+u32 addon_count( enum addon_type type, u32 ignoreflags );
+addon_reg *get_addon_from_index( enum addon_type type, u32 index, 
+                                 u32 ignoreflags );
+u32 get_index_from_addon( enum addon_type type, addon_reg *a );
+int addon_get_content_folder( addon_reg *reg, vg_str *folder, int async);
+
+/* scanning routines */
+u32 addon_match( addon_alias *alias );
+int addon_alias_eq( addon_alias *a, addon_alias *b );
+void addon_alias_uid( addon_alias *alias, char buf[ADDON_UID_MAX] );
+int addon_uid_to_alias( const char *uid, addon_alias *alias );
+void invalidate_addon_alias( addon_alias *alias );
+void addon_mount_content_folder( enum addon_type type,
+                                    const char *base_folder, 
+                                    const char *content_ext );
+void addon_mount_workshop_items(void);
+void async_addon_reg_update( void *data, u32 size );
+addon_reg *addon_mount_local_addon( const char *folder,
+                                       enum addon_type type,
+                                       const char *content_ext );
+u16 addon_cache_fetch( enum addon_type type, u32 reg_index );
+u16 addon_cache_alloc( enum addon_type type, u32 reg_index );
+void *addon_cache_item( enum addon_type type, u16 id );
+void *addon_cache_item_if_loaded( enum addon_type type, u16 id );
+void async_addon_setstate( void *data, u32 size );
+
+void addon_system_pre_update(void);
+u16 addon_cache_create_viewer( enum addon_type type, u16 reg_id);
+
+void addon_cache_watch( enum addon_type type, u16 cache_id );
+void addon_cache_unwatch( enum addon_type type, u16 cache_id );
+u16 addon_cache_create_viewer_from_uid( enum addon_type type,
+                                        char uid[ADDON_UID_MAX] );
diff --git a/src/addon_types.c b/src/addon_types.c
new file mode 100644 (file)
index 0000000..b10a23c
--- /dev/null
@@ -0,0 +1,20 @@
+#include "player.h"
+#include "player_render.h"
+#include "player_api.h"
+
+struct addon_type_info addon_type_infos[] = 
+{
+   [k_addon_type_board] = { 
+      .local_content_folder = "boards/",
+      .cache_stride = sizeof(struct player_board),
+      .cache_count  = 20
+   },
+   [k_addon_type_player] = {
+      .local_content_folder = "playermodels/",
+      .cache_stride = sizeof(struct player_model),
+      .cache_count  = 20
+   },
+   [k_addon_type_world] = {
+      .local_content_folder = "maps/"
+   }
+};
diff --git a/src/addon_types.h b/src/addon_types.h
new file mode 100644 (file)
index 0000000..a244fc0
--- /dev/null
@@ -0,0 +1,24 @@
+#pragma once
+
+enum addon_type{
+   k_addon_type_none   = 0,
+   k_addon_type_board  = 1,
+   k_addon_type_world  = 2,
+   k_addon_type_player = 3,
+   k_addon_type_max
+};
+
+#define ADDON_FOLDERNAME_MAX 64
+#define ADDON_MOUNTED_MAX 128 /* total count that we have knowledge of */
+#define ADDON_UID_MAX 76
+
+#ifdef VG_ENGINE
+
+struct addon_type_info {
+   size_t cache_stride;
+   u16 cache_count;
+   const char *local_content_folder;
+}
+extern addon_type_infos[];
+
+#endif
diff --git a/src/audio.c b/src/audio.c
new file mode 100644 (file)
index 0000000..dda8a2d
--- /dev/null
@@ -0,0 +1,259 @@
+#include "world.h"
+#include "audio.h"
+#include "vg/vg_audio_dsp.h"
+
+audio_clip audio_board[] =
+{
+   { .path="sound/skate_hpf.ogg" },
+   { .path="sound/wheel.ogg" },
+   { .path="sound/slide.ogg" },
+   { .path="sound/grind_enter.ogg" },
+   { .path="sound/grind_exit.ogg" },
+   { .path="sound/grind_loop.ogg" },
+   { .path="sound/woodslide.ogg" },
+   { .path="sound/metalscrape.ogg" },
+   { .path="sound/slidetap.ogg" }
+};
+
+audio_clip audio_taps[] =
+{
+   { .path="sound/tap0.ogg" },
+   { .path="sound/tap1.ogg" },
+   { .path="sound/tap2.ogg" },
+   { .path="sound/tap3.ogg" }
+};
+
+audio_clip audio_flips[] =
+{
+   { .path="sound/lf0.ogg" },
+   { .path="sound/lf1.ogg" },
+   { .path="sound/lf2.ogg" },
+   { .path="sound/lf3.ogg" },
+};
+
+audio_clip audio_hits[] =
+{
+   { .path="sound/hit0.ogg" },
+   { .path="sound/hit1.ogg" },
+   { .path="sound/hit2.ogg" },
+   { .path="sound/hit3.ogg" },
+   { .path="sound/hit4.ogg" }
+};
+
+audio_clip audio_splash =
+{ .path = "sound/splash.ogg" };
+
+audio_clip audio_jumps[] = {
+   { .path = "sound/jump0.ogg" },
+   { .path = "sound/jump1.ogg" },
+};
+
+audio_clip audio_footsteps[] = {
+ {.path = "sound/step_concrete0.ogg" },
+ {.path = "sound/step_concrete1.ogg" },
+ {.path = "sound/step_concrete2.ogg" },
+ {.path = "sound/step_concrete3.ogg" }
+};
+
+audio_clip audio_footsteps_grass[] = {
+ {.path = "sound/step_bush0.ogg" },
+ {.path = "sound/step_bush1.ogg" },
+ {.path = "sound/step_bush2.ogg" },
+ {.path = "sound/step_bush3.ogg" },
+ {.path = "sound/step_bush4.ogg" },
+ {.path = "sound/step_bush5.ogg" }
+};
+
+audio_clip audio_footsteps_wood[] = {
+ {.path = "sound/step_wood0.ogg" },
+ {.path = "sound/step_wood1.ogg" },
+ {.path = "sound/step_wood2.ogg" },
+ {.path = "sound/step_wood3.ogg" },
+ {.path = "sound/step_wood4.ogg" },
+ {.path = "sound/step_wood5.ogg" }
+};
+
+audio_clip audio_lands[] = {
+   { .path = "sound/land0.ogg" },
+   { .path = "sound/land1.ogg" },
+   { .path = "sound/land2.ogg" },
+   { .path = "sound/landsk0.ogg" },
+   { .path = "sound/landsk1.ogg" },
+   { .path = "sound/onto.ogg" },
+   { .path = "sound/outo.ogg" },
+};
+
+audio_clip audio_water[] = {
+   { .path = "sound/wave0.ogg" },
+   { .path = "sound/wave1.ogg" },
+   { .path = "sound/wave2.ogg" },
+   { .path = "sound/wave3.ogg" },
+   { .path = "sound/wave4.ogg" },
+   { .path = "sound/wave5.ogg" }
+};
+
+audio_clip audio_grass[] = {
+   { .path = "sound/grass0.ogg" },
+   { .path = "sound/grass1.ogg" },
+   { .path = "sound/grass2.ogg" },
+   { .path = "sound/grass3.ogg" },
+};
+
+audio_clip audio_ambience[] =
+{
+   { .path="sound/town_generic.ogg" }
+};
+
+audio_clip audio_gate_pass = {
+   .path = "sound/gate_pass.ogg"
+};
+
+audio_clip audio_gate_lap = {
+   .path = "sound/gate_lap.ogg"
+};
+
+audio_clip audio_gate_ambient = {
+.path = "sound/gate_ambient.ogg"
+};
+
+audio_clip audio_rewind[] = {
+{ .path = "sound/rewind_start.ogg" },
+{ .path = "sound/rewind_end_1.5.ogg" },
+{ .path = "sound/rewind_end_2.5.ogg" },
+{ .path = "sound/rewind_end_6.5.ogg" },
+{ .path = "sound/rewind_clack.ogg" },
+};
+
+audio_clip audio_ui[] = {
+   { .path = "sound/ui_click.ogg" },
+   { .path = "sound/ui_ding.ogg" },
+   { .path = "sound/teleport.ogg" },
+   { .path = "sound/ui_move.ogg" }
+};
+
+audio_clip audio_challenge[] = {
+   { .path = "sound/objective0.ogg" },
+   { .path = "sound/objective1.ogg" },
+   { .path = "sound/objective_win.ogg" },
+   { .path = "sound/ui_good.ogg" },
+   { .path = "sound/ui_inf.ogg" },
+   { .path = "sound/ui_ok.ogg" },
+   { .path = "sound/objective_fail.ogg" }
+};
+
+struct air_synth_data air_audio_data;
+
+static void audio_air_synth_get_samples( void *_data, f32 *buf, u32 count ){
+   struct air_synth_data *data = _data;
+
+   SDL_AtomicLock( &data->sl );
+   f32 spd = data->speed;
+   SDL_AtomicUnlock( &data->sl );
+
+   f32 s0  = sinf(data->t*2.0f),
+       s1  = sinf(data->t*0.43f),
+       s2  = sinf(data->t*1.333f),
+       sm  = vg_clampf( data->speed / 45.0f, 0, 1 ),
+       ft  = (s0*s1*s2)*0.5f+0.5f,
+       f   = vg_lerpf( 200.0f, 1200.0f, sm*0.7f + ft*0.3f ),
+       vol = 0.25f * sm;
+
+   dsp_init_biquad_butterworth_lpf( &data->lpf, f );
+
+   for( u32 i=0; i<count; i ++ ){
+      f32 v = (vg_randf64(&vg_dsp.rand) * 2.0f - 1.0f) * vol;
+      v = dsp_biquad_process( &data->lpf, v );
+
+      buf[i*2+0] = v;
+      buf[i*2+1] = v;
+   }
+
+   data->t += (f32)(count)/44100.0f;
+};
+
+static audio_clip air_synth = {
+   .flags = k_audio_format_gen,
+   .size = 0,
+   .func = audio_air_synth_get_samples,
+   .data = &air_audio_data
+};
+
+void audio_init(void)
+{
+   audio_clip_loadn( audio_board, VG_ARRAY_LEN(audio_board), NULL );
+   audio_clip_loadn( audio_taps, VG_ARRAY_LEN(audio_taps), NULL );
+   audio_clip_loadn( audio_flips, VG_ARRAY_LEN(audio_flips), NULL );
+   audio_clip_loadn( audio_hits, VG_ARRAY_LEN(audio_hits), NULL );
+   audio_clip_loadn( audio_ambience, VG_ARRAY_LEN(audio_ambience), NULL );
+   audio_clip_loadn( &audio_splash, 1, NULL );
+   audio_clip_loadn( &audio_gate_pass, 1, NULL );
+   audio_clip_loadn( &audio_gate_lap, 1, NULL );
+   audio_clip_loadn( &audio_gate_ambient, 1, NULL );
+
+   audio_clip_loadn( audio_jumps, VG_ARRAY_LEN(audio_jumps), NULL );
+   audio_clip_loadn( audio_lands, VG_ARRAY_LEN(audio_lands), NULL );
+   audio_clip_loadn( audio_water, VG_ARRAY_LEN(audio_water), NULL );
+   audio_clip_loadn( audio_grass, VG_ARRAY_LEN(audio_grass), NULL );
+   audio_clip_loadn( audio_footsteps, VG_ARRAY_LEN(audio_footsteps), NULL );
+   audio_clip_loadn( audio_footsteps_grass, 
+                     VG_ARRAY_LEN(audio_footsteps_grass), NULL );
+   audio_clip_loadn( audio_footsteps_wood, 
+                     VG_ARRAY_LEN(audio_footsteps_wood), NULL );
+   audio_clip_loadn( audio_rewind, VG_ARRAY_LEN(audio_rewind), NULL );
+   audio_clip_loadn( audio_ui, VG_ARRAY_LEN(audio_ui), NULL );
+   audio_clip_loadn( audio_challenge, VG_ARRAY_LEN(audio_challenge), NULL );
+
+   audio_lock();
+   audio_set_lfo_wave( 0, k_lfo_polynomial_bipolar, 80.0f );
+   audio_set_lfo_frequency( 0, 20.0f );
+
+   air_audio_data.channel = audio_get_first_idle_channel();
+   if( air_audio_data.channel )
+      audio_channel_init( air_audio_data.channel, &air_synth, 0 );
+
+   audio_unlock();
+}
+
+void audio_ambient_sprite_play( v3f co, audio_clip *clip )
+{
+   audio_lock();
+   u16 group_id = 0xfff0;
+   audio_channel *ch = audio_get_group_idle_channel( group_id, 4 );
+
+   if( ch ){
+      audio_channel_init( ch, clip, AUDIO_FLAG_SPACIAL_3D );
+      audio_channel_group( ch, group_id );
+      audio_channel_set_spacial( ch, co, 80.0f );
+      audio_channel_edit_volume( ch, 1.0f, 1 );
+      ch = audio_relinquish_channel( ch );
+   }
+   audio_unlock();
+}
+
+enum audio_sprite_type world_audio_sample_sprite_random(v3f origin, v3f output);
+void audio_ambient_sprites_update( world_instance *world, v3f co )
+{
+   static float accum = 0.0f;
+   accum += vg.time_delta;
+
+   if( accum > 0.1f )
+      accum -= 0.1f;
+   else return;
+
+   v3f sprite_pos;
+   enum audio_sprite_type sprite_type = 
+      world_audio_sample_sprite_random( co, sprite_pos );
+   
+   if( sprite_type != k_audio_sprite_type_none ){
+      if( sprite_type == k_audio_sprite_type_grass ){
+         audio_ambient_sprite_play( sprite_pos, 
+                                    &audio_grass[vg_randu32(&vg.rand)%4] );
+      }
+      else if( sprite_type == k_audio_sprite_type_water ){
+         if( world->water.enabled ){
+            audio_ambient_sprite_play( sprite_pos, 
+                                       &audio_water[vg_randu32(&vg.rand)%6] );
+         }
+      }
+   }
+}
diff --git a/src/audio.h b/src/audio.h
new file mode 100644 (file)
index 0000000..95894f1
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+
+#include "vg/vg_engine.h"
+#include "vg/vg_audio.h"
+#include "vg/vg_audio_dsp.h"
+#include "world.h"
+
+struct air_synth_data {
+   f32 speed;
+
+   /* internal */
+   f32 t;
+   struct dsp_biquad lpf;
+   SDL_SpinLock sl;
+
+   /* not used in locking */
+   audio_channel *channel;
+}
+extern air_audio_data;
+
+void audio_init(void);
+void audio_ambient_sprite_play( v3f co, audio_clip *clip );
+void audio_ambient_sprites_update( world_instance *world, v3f co );
+
+/* TODO(ASSETS): 
+ * Have these as asignable ID's and not a bunch of different arrays. 
+ */
+extern audio_clip audio_board[];
+extern audio_clip audio_taps[];
+extern audio_clip audio_flips[];
+extern audio_clip audio_hits[];
+extern audio_clip audio_splash;
+extern audio_clip audio_jumps[];
+extern audio_clip audio_footsteps[];
+extern audio_clip audio_footsteps_grass[];
+extern audio_clip audio_footsteps_wood[];
+extern audio_clip audio_lands[];
+extern audio_clip audio_water[];
+extern audio_clip audio_grass[];
+extern audio_clip audio_ambience[];
+extern audio_clip audio_gate_pass;
+extern audio_clip audio_gate_lap;
+extern audio_clip audio_gate_ambient;
+extern audio_clip audio_rewind[];
+extern audio_clip audio_ui[];
+extern audio_clip audio_challenge[];
+
+enum audio_sprite_type 
+{
+   k_audio_sprite_type_none,
+   k_audio_sprite_type_grass,
+   k_audio_sprite_type_water
+};
diff --git a/src/build_control_overlay.c b/src/build_control_overlay.c
new file mode 100644 (file)
index 0000000..3205955
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Script to load the overlay model and generate an enum referencing each 
+ * submesh by its name.
+ */
+void build_control_overlay(void)
+{
+   FILE *hdr = fopen( "src/control_overlay.h.generated", "w" );
+   mdl_context ctx;
+   mdl_open( &ctx, "content_skaterift/models/rs_overlay.mdl", NULL );
+   mdl_load_metadata_block( &ctx, NULL );
+   mdl_close( &ctx );
+
+   for( u32 i=0; i<mdl_arrcount( &ctx.meshs ); i ++ )
+   {
+      mdl_mesh *mesh = mdl_arritm( &ctx.meshs, i );
+      fprintf( hdr, "   %s = %u,\n", 
+                  mdl_pstr( &ctx, mesh->pstr_name ), mesh->submesh_start );
+   }
+
+   fclose( hdr );
+}
diff --git a/src/client.c b/src/client.c
new file mode 100644 (file)
index 0000000..e06c253
--- /dev/null
@@ -0,0 +1,88 @@
+#include "vg/vg_opt.h"
+#include "vg/vg_loader.h"
+#include "vg/vg_io.h"
+#include "vg/vg_audio.h"
+#include "vg/vg_async.h"
+
+#include "client.h"
+#include "render.h"
+#include "network.h"
+#include "player_remote.h"
+#include "menu.h"
+
+const char* __asan_default_options() { return "detect_leaks=0"; }
+
+struct game_client g_client = 
+{
+   .demo_mode = 1
+};
+
+static void async_client_ready( void *payload, u32 size )
+{
+   g_client.loaded = 1;
+
+   if( network_client.auto_connect )
+      network_client.user_intent = k_server_intent_online;
+
+   menu_at_begin();
+}
+
+void vg_load(void)
+{
+   vg_audio.always_keep_compressed = 1;
+   vg_loader_step( render_init, NULL );
+
+   game_load();
+
+   vg_async_call( async_client_ready, NULL, 0 );
+}
+
+void vg_preload(void)
+{
+vg_info(" Copyright  .        . .       -----, ,----- ,---.   .---.  \n" );
+vg_info(" 2021-2024  |\\      /| |           /  |      |    | |    /| \n" );
+vg_info("            | \\    / | +--        /   +----- +---'  |   / | \n" );
+vg_info("            |  \\  /  | |         /    |      |   \\  |  /  | \n" );
+vg_info("            |   \\/   | |        /     |      |    \\ | /   | \n" );
+vg_info("            '        ' '--' [] '----- '----- '     ' '---'  " 
+        "SOFTWARE\n" );
+
+   /* please forgive me! */
+   u32 sz; char *drm;
+   if( (drm = vg_file_read_text( vg_mem.scratch, "DRM", &sz )) )
+      if( !strcmp(drm, "blibby!") )
+         g_client.demo_mode = 0;
+
+   vg_loader_step( remote_players_init, NULL );
+
+   steam_init();
+   vg_loader_step( NULL, steam_end );
+   vg_loader_step( network_init, network_end );
+}
+
+void vg_launch_opt(void)
+{
+   const char *arg;
+
+   if( vg_long_opt( "noauth" ) )
+      network_client.auth_mode = eServerModeNoAuthentication;
+
+   if( (arg = vg_long_opt_arg( "server" )) )
+      network_set_host( arg, NULL );
+
+   if( vg_long_opt( "demo" ) )
+      g_client.demo_mode = 1;
+
+   game_launch_opt();
+}
+
+int main( int argc, char *argv[] )
+{
+   network_set_host( "skaterift.com", NULL );
+   vg_mem.use_libc_malloc = 1;
+   vg_set_mem_quota( 160*1024*1024 );
+   vg_enter( argc, argv, "Voyager Game Engine" ); 
+   return 0;
+}
+
+#include "skaterift.c"
diff --git a/src/client.h b/src/client.h
new file mode 100644 (file)
index 0000000..e3435b9
--- /dev/null
@@ -0,0 +1,18 @@
+#pragma once
+#include "vg/vg_platform.h"
+
+/*
+ *   client     - entry point. window, common things like render init.. etc
+ *      vg      - backend code
+ *         game - top layer: game content, state
+ */
+
+struct game_client
+{
+   bool loaded, demo_mode;
+}
+extern g_client;
+
+/* game defined */
+void game_launch_opt( void );
+void game_load( void );
diff --git a/src/common.h b/src/common.h
new file mode 100644 (file)
index 0000000..6f0c2f9
--- /dev/null
@@ -0,0 +1,8 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#ifndef COMMON_H
+#define COMMON_H
+
+#endif /* COMMON_H */
diff --git a/src/control_overlay.c b/src/control_overlay.c
new file mode 100644 (file)
index 0000000..a39d77b
--- /dev/null
@@ -0,0 +1,793 @@
+#include "control_overlay.h"
+#include "model.h"
+#include "input.h"
+#include "player.h"
+#include "player_skate.h"
+#include "player_walk.h"
+#include "shaders/model_menu.h"
+#include "vg/vg_engine.h"
+#include "vg/vg_mem.h"
+#include "vg/vg_m.h"
+
+struct control_overlay control_overlay = { .enabled = 1 };
+
+static void render_overlay_mesh( enum control_overlay_mesh index )
+{
+   mdl_draw_submesh( mdl_arritm( &control_overlay.mdl.submeshs, index ) );
+}
+
+void control_overlay_init(void)
+{
+   void *alloc = vg_mem.rtmemory;
+   mdl_context *mdl = &control_overlay.mdl;
+
+   mdl_open( mdl, "models/rs_overlay.mdl", alloc );
+   mdl_load_metadata_block( mdl, alloc );
+   mdl_async_full_load_std( mdl );
+   mdl_close( mdl );
+
+   vg_async_stall();
+
+   if( mdl_arrcount( &mdl->textures ) )
+   {
+      mdl_texture *tex = mdl_arritm( &mdl->textures, 0 );
+      control_overlay.tex = tex->glname;
+   }
+   else
+   {
+      control_overlay.tex = vg.tex_missing;
+      vg_error( "No texture in control overlay\n" );
+   }
+
+   vg_console_reg_var( "control_overlay", &control_overlay.enabled, 
+                       k_var_dtype_i32, VG_VAR_PERSISTENT );
+}
+
+static void draw_key( bool press, bool wide )
+{
+   if( wide ) render_overlay_mesh( press? ov_shift_down: ov_shift );
+   else render_overlay_mesh( press? ov_key_down: ov_key );
+}
+
+static void colorize( bool press, bool condition )
+{
+   v4f cnorm = { 1,1,1,0.76f },
+       cdis  = { 1,1,1,0.35f },
+       chit  = { 1,0.5f,0.2f,0.8f };
+
+   if( condition )
+      if( press )
+         shader_model_menu_uColour( chit );
+      else
+         shader_model_menu_uColour( cnorm );
+   else
+      shader_model_menu_uColour( cdis );
+}
+
+void control_overlay_render(void)
+{
+   if( !control_overlay.enabled ) return;
+   if( skaterift.activity != k_skaterift_default ) return;
+
+   glEnable(GL_BLEND);
+   glDisable(GL_DEPTH_TEST);
+   glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
+   glBlendEquation(GL_FUNC_ADD);
+
+   m4x4f ortho;
+   f32  r = (f32)vg.window_x / (f32)vg.window_y,
+       fl = -r,
+       fr =  r,
+       fb =  1.0f,
+       ft = -1.0f,
+       rl = 1.0f / (fr-fl),
+       tb = 1.0f / (ft-fb);
+
+   m4x4_zero( ortho );
+   ortho[0][0] = 2.0f * rl;
+   ortho[2][1] = 2.0f * tb;
+   ortho[3][0] = (fr + fl) * -rl;
+   ortho[3][1] = (ft + fb) * -tb;
+   ortho[3][3] = 1.0f;
+
+   v4f cnorm = { 1,1,1,0.76f },
+       cdis  = { 1,1,1,0.35f },
+       chit  = { 1,0.5f,0.2f,0.8f };
+
+   shader_model_menu_use();
+   shader_model_menu_uTexMain( 1 );
+   shader_model_menu_uPv( ortho );
+   shader_model_menu_uColour( cnorm );
+
+   mdl_context *mdl = &control_overlay.mdl;
+   mesh_bind( &mdl->mesh );
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, control_overlay.tex );
+
+   enum player_subsystem subsytem = localplayer.subsystem;
+
+   m4x3f mmdl;
+   m4x3_identity( mmdl );
+
+   bool in_air = 0, grinding = 0;
+
+   if( subsytem == k_player_subsystem_walk )
+      in_air = player_walk.state.activity == k_walk_activity_air;
+   else if( subsytem == k_player_subsystem_skate )
+      in_air = player_skate.state.activity < k_skate_activity_ground;
+
+   grinding = (subsytem == k_player_subsystem_skate) &&
+              (player_skate.state.activity >= k_skate_activity_grind_any);
+
+   if( vg_input.display_input_method == k_input_method_controller )
+   {
+      bool press_jump = player_skate.state.jump_charge > 0.2f;
+      u8 lb_down = 0, rb_down = 0;
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, vg_end 
+            }, &rb_down );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, vg_end 
+            }, &lb_down );
+      f32 lt_amt = 0.0f, rt_amt = 0.0f;
+      vg_exec_input_program( k_vg_input_type_axis_f32,
+            (vg_input_op[]){ vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT, vg_end },
+            &lt_amt );
+      vg_exec_input_program( k_vg_input_type_axis_f32,
+            (vg_input_op[]){ vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, vg_end },
+            &rt_amt );
+
+      /* joystick L */
+      v2f steer;
+      joystick_state( k_srjoystick_steer, steer );
+
+      mmdl[3][0] =   -r + 0.375f;
+      mmdl[3][2] = 1.0f - 0.375f;
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( 0, 1 );
+         render_overlay_mesh( ov_ls_circ_skate );
+
+         colorize( steer[1]>=0.5f, press_jump );
+         render_overlay_mesh( ov_ls_circ_backflip );
+         colorize( steer[1]<=-0.5f, press_jump );
+         render_overlay_mesh( ov_ls_circ_frontflip );
+
+         colorize( steer[1] > 0.7f, !press_jump && !in_air );
+         render_overlay_mesh( ov_ls_circ_manual );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( 0, 1 );
+         render_overlay_mesh( ov_ls_circ_walk );
+      }
+
+      mmdl[3][0] += steer[0]*0.125f*0.75f;
+      mmdl[3][2] += steer[1]*0.125f*0.75f;
+
+      colorize( 0, 1 );
+      shader_model_menu_uMdl( mmdl );
+      render_overlay_mesh( ov_ls );
+
+      /* joystick R */
+      mmdl[3][0] =    r - 0.375f;
+      mmdl[3][2] = 1.0f - 0.375f;
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( rt_amt > 0.5f, in_air );
+         render_overlay_mesh( ov_rs_circ_grab );
+         colorize( 0, in_air );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( 0, 1 );
+         render_overlay_mesh( ov_rs_circ_look );
+      }
+
+      v2f jlook;
+      joystick_state( k_srjoystick_look, jlook );
+
+      mmdl[3][0] += jlook[0]*0.125f*0.75f;
+      mmdl[3][2] += jlook[1]*0.125f*0.75f;
+      shader_model_menu_uMdl( mmdl );
+      render_overlay_mesh( ov_rs );
+
+
+
+      /* LEFT UPPERS */
+      mmdl[3][0] =    -r;
+      mmdl[3][2] = -1.0f;
+      shader_model_menu_uMdl( mmdl );
+
+      /* LB -------------------------------------------------------------- */
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( lb_down, !in_air );
+         render_overlay_mesh( ov_carve_l );
+      }
+      else
+         colorize( 0, 0 );
+
+      render_overlay_mesh( lb_down? ov_lb_down: ov_lb );
+
+      /* LT ---------------------------------------------------------------- */
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( 0, 0 );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( lt_amt>0.2f, 1 );
+         render_overlay_mesh( ov_lt_run );
+      }
+
+      render_overlay_mesh( ov_lt );
+
+      mmdl[3][2] += lt_amt*0.125f*0.5f;
+      shader_model_menu_uMdl( mmdl );
+      render_overlay_mesh( ov_lt_act );
+
+      /* RIGHT UPPERS */
+      mmdl[3][0] =     r;
+      mmdl[3][2] = -1.0f;
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( rb_down, !in_air );
+         render_overlay_mesh( ov_carve_r );
+      }
+      else
+         colorize( 0, 0 );
+
+      render_overlay_mesh( rb_down? ov_rb_down: ov_rb );
+
+      /* RT ---------------------------------------------------------------- */
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( rt_amt>0.2f, in_air );
+         render_overlay_mesh( ov_rt_grab );
+         colorize( rt_amt>0.2f, !in_air );
+         render_overlay_mesh( ov_rt_crouch );
+         colorize( rt_amt>0.2f, 1 );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( 0, 0 );
+      }
+
+      render_overlay_mesh( ov_rt );
+
+      mmdl[3][2] += rt_amt*0.125f*0.5f;
+      shader_model_menu_uMdl( mmdl );
+      render_overlay_mesh( ov_rt_act );
+
+      /* RIGHT SIDE BUTTONS */
+      bool press_a = 0, press_b = 0, press_x = 0, press_y = 0,
+           press_dpad_w = 0, press_dpad_e = 0, press_dpad_n = 0, press_dpad_s = 0,
+           press_menu = 0, press_back = 0;
+
+      bool is_ps = 0;
+      if( (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS3) ||
+          (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS4) ||
+          (vg_input.display_input_type == SDL_CONTROLLER_TYPE_PS5) )
+      {
+         is_ps = 1;
+      }
+
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end }, &press_a );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_B, vg_end }, &press_b );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end }, &press_x );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_Y, vg_end }, &press_y );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_LEFT, vg_end }, &press_dpad_w );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, vg_end }, &press_dpad_e );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_UP, vg_end }, &press_dpad_n );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_DPAD_DOWN, vg_end }, &press_dpad_s );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_BACK, vg_end }, &press_back );
+      vg_exec_input_program( k_vg_input_type_button_u8, 
+            (vg_input_op[]){
+               vg_joy_button, SDL_CONTROLLER_BUTTON_START, vg_end }, &press_menu );
+
+      mmdl[3][0] =    r;
+      mmdl[3][2] = 0.0f;
+      shader_model_menu_uMdl( mmdl );
+
+      /* B / KICKFLIP / PUSH */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( press_b, !in_air );
+         render_overlay_mesh( ov_text_b_push );
+         colorize( press_b,  in_air );
+         render_overlay_mesh( ov_text_b_kickflip );
+         colorize( press_b, 1 );
+      }
+      else
+      {
+         colorize( 0, 0 );
+      }
+
+      if( is_ps ) render_overlay_mesh( press_b? ov_b_down_ps: ov_b_ps );
+      else        render_overlay_mesh( press_b? ov_b_down: ov_b );
+
+      /* Y / SKATE / WALK / GLIDE */
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         if( localplayer.have_glider )
+         {
+            colorize( press_y, !in_air );
+            render_overlay_mesh( ov_text_y_walk_lwr );
+            colorize( press_y, in_air );
+            render_overlay_mesh( ov_text_y_glide );
+         }
+         else
+         {
+            colorize( press_y, 1 );
+            render_overlay_mesh( ov_text_y_walk );
+         }
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( press_y, player_walk.state.activity < k_walk_activity_inone );
+         render_overlay_mesh( ov_text_y_skate );
+      }
+      else if( subsytem == k_player_subsystem_glide )
+      {
+         colorize( press_y, 1 );
+         render_overlay_mesh( ov_text_y_skate );
+      }
+      else
+         colorize( 0, 0 );
+
+      if( is_ps ) render_overlay_mesh( press_y? ov_y_down_ps: ov_y_ps );
+      else        render_overlay_mesh( press_y? ov_y_down: ov_y );
+
+      /* X / TREFLIP */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( press_x, in_air );
+         render_overlay_mesh( ov_text_x_treflip );
+      }
+      else
+         colorize( press_x, 0 );
+
+      if( is_ps ) render_overlay_mesh( press_x? ov_x_down_ps: ov_x_ps );
+      else        render_overlay_mesh( press_x? ov_x_down: ov_x );
+
+      /* A / JUMP / SHUVIT */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( press_a, !in_air );
+         render_overlay_mesh( ov_text_a_jump );
+         colorize( press_a,  in_air );
+         render_overlay_mesh( ov_text_a_shuvit );
+         colorize( press_a, 1 );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( press_a, !in_air );
+         render_overlay_mesh( ov_text_a_jump_mid );
+      }
+
+      if( is_ps ) render_overlay_mesh( press_a? ov_a_down_ps: ov_a_ps );
+      else        render_overlay_mesh( press_a? ov_a_down: ov_a );
+
+      /* JUMP CHARGE */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         if( player_skate.state.jump_charge > 0.01f )
+         {
+            mmdl[0][0] = player_skate.state.jump_charge * 0.465193f;
+            mmdl[3][0] += -0.4375f;
+            mmdl[3][2] +=  0.09375f;
+            shader_model_menu_uMdl( mmdl );
+            render_overlay_mesh( ov_jump_ind );
+            mmdl[0][0] = 1.0f;
+         }
+      }
+
+
+      /* DPAD --------------------------------------------------- */
+      
+      mmdl[3][0] =   -r;
+      mmdl[3][2] = 0.0f;
+      shader_model_menu_uMdl( mmdl );
+      colorize( 0, 1 );
+      render_overlay_mesh( ov_dpad );
+      
+      colorize( press_dpad_e, 1 );
+      render_overlay_mesh( ov_text_de_camera );
+      if( press_dpad_e )
+         render_overlay_mesh( ov_dpad_e );
+
+      colorize( press_dpad_w, 1 );
+      render_overlay_mesh( ov_text_dw_rewind );
+      if( press_dpad_w )
+         render_overlay_mesh( ov_dpad_w );
+
+      if( subsytem == k_player_subsystem_dead )
+      {
+         colorize( press_dpad_n, 1 );
+         render_overlay_mesh( ov_text_dn_respawn );
+      }
+      else colorize( press_dpad_n, 0 );
+      if( press_dpad_n )
+         render_overlay_mesh( ov_dpad_n );
+
+      colorize( press_dpad_s, 0 );
+      if( press_dpad_s )
+         render_overlay_mesh( ov_dpad_s );
+
+      
+      /* WEIGHT */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         /* stored indicator text */
+         mmdl[3][0] = r -0.842671f;
+         mmdl[3][2] = -1.0f + 0.435484f;
+         colorize( 0, !in_air );
+         shader_model_menu_uMdl( mmdl );
+         render_overlay_mesh( ov_text_stored );
+      
+         mmdl[0][0] = v3_length( player_skate.state.throw_v ) / k_mmthrow_scale;
+         shader_model_menu_uMdl( mmdl );
+         colorize( 0, !in_air );
+         render_overlay_mesh( ov_stored_ind );
+
+         static f32 collect = 0.0f;
+         collect = vg_lerpf( collect, player_skate.collect_feedback, 
+                             vg.time_frame_delta * 15.0f );
+         collect = vg_clampf( collect, 0.0f, 1.0f );
+
+         mmdl[0][0] = collect;
+         mmdl[3][2] += 0.015625f;
+         shader_model_menu_uMdl( mmdl );
+         render_overlay_mesh( ov_stored_ind );
+      }
+
+      mmdl[0][0] =  1.0f;
+      mmdl[3][0] =  0.0f;
+      mmdl[3][2] = -1.0f;
+      shader_model_menu_uMdl( mmdl );
+      colorize( press_menu, 1 );
+      render_overlay_mesh( press_menu? ov_met_r_down: ov_met_r );
+      render_overlay_mesh( ov_text_met_menu );
+
+      colorize( press_back, 0 );
+      render_overlay_mesh( press_back? ov_met_l_down: ov_met_l );
+
+      colorize( 0, 0 );
+      render_overlay_mesh( ov_met );
+   }
+   else 
+   {
+      static v2f gd;
+      v2_lerp( gd, player_skate.state.grab_mouse_delta, vg.time_frame_delta*20.0f, gd );
+
+      /* CTRL  ||  CARVE */
+      if( subsytem == k_player_subsystem_skate )
+      {
+         bool press_ctrl = vg_getkey(SDLK_LCTRL);
+         mmdl[3][0] = -r + 0.25f;
+         mmdl[3][2] = 1.0f - 0.125f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_ctrl, !in_air && !grinding );
+         draw_key( press_ctrl, 1 );
+         render_overlay_mesh( ov_text_carve );
+      }
+
+      /* SHIFT  ||  CROUCH / GRAB / RUN */
+      bool press_shift = vg_getkey( SDLK_LSHIFT );
+      if( subsytem == k_player_subsystem_skate ||
+          subsytem == k_player_subsystem_walk )
+      {
+         mmdl[3][0] = -r + 0.25f;
+         mmdl[3][2] = 1.0f - 0.125f - 0.25f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_shift, !grinding );
+         draw_key( press_shift, 1 );
+         render_overlay_mesh( ov_text_shift );
+
+         if( subsytem == k_player_subsystem_skate )
+         {
+            colorize( press_shift, !in_air && !grinding );
+            render_overlay_mesh( ov_text_crouch );
+            colorize( press_shift,  in_air && !grinding );
+            render_overlay_mesh( ov_text_grab );
+         }
+         else if( subsytem == k_player_subsystem_walk )
+         {
+            render_overlay_mesh( ov_text_run );
+         }
+      }
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         /* stored indicator text */
+         mmdl[3][0] = -r + 0.25f + 0.203125f + 0.007812f;
+         colorize( 0, !in_air );
+         shader_model_menu_uMdl( mmdl );
+         render_overlay_mesh( ov_text_stored );
+      
+         mmdl[0][0] = v3_length( player_skate.state.throw_v ) / k_mmthrow_scale;
+         shader_model_menu_uMdl( mmdl );
+         colorize( 0, !in_air );
+         render_overlay_mesh( ov_stored_ind );
+
+         static f32 collect = 0.0f;
+         collect = vg_lerpf( collect, player_skate.collect_feedback, 
+                             vg.time_frame_delta * 15.0f );
+         collect = vg_clampf( collect, 0.0f, 1.0f );
+
+         mmdl[0][0] = collect;
+         mmdl[3][2] += 0.015625f;
+         shader_model_menu_uMdl( mmdl );
+         render_overlay_mesh( ov_stored_ind );
+      }
+
+      /* -1 */
+      if( subsytem != k_player_subsystem_dead )
+      {
+         bool press_c =  vg_getkey(SDLK_c);
+         mmdl[0][0] = 1.0f;
+         mmdl[3][0] = -r + 0.125f + 1.0f;
+         mmdl[3][2] = 1.0f - 0.125f - 0.25f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_c, 1 );
+         draw_key( press_c, 0 );
+         render_overlay_mesh( ov_text_camera );
+      }
+
+      /* +0 */
+      mmdl[0][0] = 1.0f;
+      mmdl[3][2] = 1.0f - 0.125f - 0.25f - 0.25f;
+
+      /* A  ||  LEFT */
+      if( subsytem != k_player_subsystem_dead )
+      {
+         bool press_a =  vg_getkey(SDLK_a);
+         mmdl[3][0] = -r + 0.125f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_a, 1 );
+         draw_key( press_a, 0 );
+         render_overlay_mesh( ov_text_left );
+      }
+
+      bool press_jump = player_skate.state.jump_charge < 0.2f;
+
+      /* S  ||  MANUAL / BACKFLIP */
+      bool press_s =  vg_getkey(SDLK_s);
+      mmdl[3][0] = -r + 0.125f + 0.25f;
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( press_s, !in_air );
+         draw_key( press_s, 0 );
+         render_overlay_mesh( ov_text_s );
+         /* backflip/manual */
+         colorize( press_s, !in_air && !press_jump );
+         render_overlay_mesh( ov_text_back_flip );
+         colorize( press_s, !in_air &&  press_jump );
+         render_overlay_mesh( ov_text_manual );
+      }
+      else if( subsytem != k_player_subsystem_dead )
+      {
+         colorize( press_s, 1 );
+         draw_key( press_s, 0 );
+         render_overlay_mesh( ov_text_s );
+         render_overlay_mesh( ov_text_back );
+      }
+
+      /* D  ||  RIGHT */
+      if( subsytem != k_player_subsystem_dead )
+      {
+         bool press_d = vg_getkey(SDLK_d);
+         mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_d, 1 );
+         draw_key( press_d, 0 );
+         render_overlay_mesh( ov_text_right );
+      }
+
+      /* +1 */
+      mmdl[3][2] = 1.0f - 0.125f - 0.25f - 0.25f - 0.25f;
+
+      /* Q */
+      if( subsytem == k_player_subsystem_dead )
+      {
+         bool press_q = vg_getkey(SDLK_q);
+         mmdl[3][0] = -r + 0.125f;
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_q, 1 );
+         draw_key( press_q, 0 );
+         render_overlay_mesh( ov_text_respawn );
+      }
+
+      /* W  ||  PUSH / FRONTFLIP */
+      bool press_w = vg_getkey(SDLK_w);
+      mmdl[3][0] = -r + 0.125f + 0.25f;
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( press_w, !in_air );
+         draw_key( press_w, 0 );
+         render_overlay_mesh( ov_text_w );
+         /* frontflip/push */
+         colorize( press_w, !in_air && !press_jump );
+         render_overlay_mesh( ov_text_front_flip );
+         colorize( press_w, !in_air &&  press_jump );
+         render_overlay_mesh( ov_text_push );
+      }
+      else if( subsytem != k_player_subsystem_dead )
+      {
+         colorize( press_w, 1 );
+         draw_key( press_w, 0 );
+         render_overlay_mesh( ov_text_w );
+         render_overlay_mesh( ov_text_forward );
+      }
+
+      /* E */
+      bool press_e = vg_getkey(SDLK_e);
+      mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f;
+
+      shader_model_menu_uMdl( mmdl );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         if( localplayer.have_glider )
+         {
+            colorize( press_e, !in_air );
+            render_overlay_mesh( ov_text_walk_lwr );
+            colorize( press_e, in_air );
+            render_overlay_mesh( ov_text_glide );
+         }
+         else
+         {
+            colorize( press_e, 1 );
+            render_overlay_mesh( ov_text_walk );
+         }
+      }
+      else if( subsytem == k_player_subsystem_glide )
+      {
+         colorize( press_e, 1 );
+         render_overlay_mesh( ov_text_skate );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( press_e, player_walk.state.activity < k_walk_activity_inone );
+         render_overlay_mesh( ov_text_skate );
+      }
+
+      if( subsytem != k_player_subsystem_dead )
+      {
+         draw_key( press_e, 0 );
+         render_overlay_mesh( ov_text_e );
+      }
+
+      /* R */
+      bool press_r = vg_getkey(SDLK_r);
+      mmdl[3][0] = -r + 0.125f + 0.25f + 0.25f + 0.25f;
+      shader_model_menu_uColour( cnorm );
+      shader_model_menu_uMdl( mmdl );
+
+      colorize( press_r, 1 );
+      draw_key( press_r, 0 );
+      render_overlay_mesh( ov_text_rewind );
+
+      /* space */
+      bool press_space = vg_getkey(SDLK_SPACE);
+      mmdl[3][0] = 0.0f;
+      mmdl[3][2] = 1.0f - 0.125f;
+
+
+      if( subsytem == k_player_subsystem_skate ||
+          subsytem == k_player_subsystem_walk )
+      {
+         shader_model_menu_uMdl( mmdl );
+         colorize( press_space, !in_air );
+
+         render_overlay_mesh( press_space?
+               ov_space_down: ov_space );
+         render_overlay_mesh( ov_text_jump );
+      }
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         if( player_skate.state.jump_charge > 0.01f )
+         {
+            mmdl[0][0] = player_skate.state.jump_charge;
+            mmdl[3][0] = -0.4375f;
+            shader_model_menu_uMdl( mmdl );
+            render_overlay_mesh( ov_jump_ind );
+         }
+      }
+
+      bool press_esc = vg_getkey(SDLK_ESCAPE);
+      mmdl[0][0] = 1.0f;
+      mmdl[3][0] = -r + 0.125f;;
+      mmdl[3][2] = -1.0f + 0.125f;
+      shader_model_menu_uMdl( mmdl );
+      colorize( press_esc, 1 );
+      render_overlay_mesh( ov_text_menu );
+      render_overlay_mesh( press_esc? ov_key_menu_down: ov_key_menu );
+      mmdl[3][0] = r - 0.38f;
+      mmdl[3][2] = 0.0f;
+      shader_model_menu_uMdl( mmdl );
+      colorize( press_shift, in_air );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         render_overlay_mesh( ov_mouse_grabs );
+
+         if( in_air && press_shift )
+         {
+            mmdl[3][0] += gd[0]*0.125f;
+            mmdl[3][2] += gd[1]*0.125f;
+         }
+      }
+
+      shader_model_menu_uMdl( mmdl );
+
+      bool lmb = button_press( k_srbind_trick0 ),
+           rmb = button_press( k_srbind_trick1 );
+
+      if( subsytem == k_player_subsystem_skate )
+      {
+         colorize( 0, press_space || in_air );
+         render_overlay_mesh( ov_mouse );
+
+         colorize( lmb&&!rmb, press_space || in_air );
+         render_overlay_mesh( ov_text_shuvit );
+
+         colorize( lmb, press_space || in_air );
+         render_overlay_mesh( lmb? ov_lmb_down: ov_lmb );
+
+         colorize( rmb&&!lmb, press_space || in_air );
+         render_overlay_mesh( ov_text_kickflip );
+
+         colorize( rmb, press_space || in_air );
+         render_overlay_mesh( rmb? ov_rmb_down: ov_rmb );
+
+         colorize( rmb&&lmb, press_space || in_air );
+         render_overlay_mesh( ov_text_treflip );
+      }
+      else if( subsytem == k_player_subsystem_walk )
+      {
+         colorize( 0, 1 );
+         render_overlay_mesh( ov_mouse );
+         render_overlay_mesh( ov_text_look );
+
+         render_overlay_mesh( lmb? ov_lmb_down: ov_lmb );
+         render_overlay_mesh( rmb? ov_rmb_down: ov_rmb );
+      }
+   }
+}
diff --git a/src/control_overlay.h b/src/control_overlay.h
new file mode 100644 (file)
index 0000000..089136d
--- /dev/null
@@ -0,0 +1,17 @@
+#pragma once
+
+enum control_overlay_mesh
+{
+   #include "control_overlay.h.generated"
+};
+
+struct control_overlay
+{
+   mdl_context mdl;
+   GLuint tex;
+   i32 enabled;
+}
+extern control_overlay;
+
+void control_overlay_render(void);
+void control_overlay_init(void);
diff --git a/src/depth_compare.h b/src/depth_compare.h
new file mode 100644 (file)
index 0000000..1db5665
--- /dev/null
@@ -0,0 +1,23 @@
+#pragma once
+#include "vg/vg_m.h"
+#include "vg/vg_framebuffer.h"
+#include "skaterift.h"
+#include "render.h"
+
+static inline void depth_compare_bind( 
+      void (*uTexSceneDepth)(int),
+      void (*uInverseRatioDepth)(v3f),
+      void (*uInverseRatioMain)(v3f),
+      vg_camera *cam )
+{
+   uTexSceneDepth( 5 );
+   vg_framebuffer_bind_texture( g_render.fb_main, 2, 5 );
+   v3f inverse;
+   vg_framebuffer_inverse_ratio( g_render.fb_main, inverse );
+   inverse[2] = g_render.cam.farz-g_render.cam.nearz;
+
+   uInverseRatioDepth( inverse );
+   vg_framebuffer_inverse_ratio( NULL, inverse );
+   inverse[2] = cam->farz-cam->nearz;
+   uInverseRatioMain( inverse );
+}
diff --git a/src/ent_camera.c b/src/ent_camera.c
new file mode 100644 (file)
index 0000000..d23d8d8
--- /dev/null
@@ -0,0 +1,10 @@
+#include "entity.h"
+
+void ent_camera_unpack( ent_camera *ent, vg_camera *cam )
+{
+   v3f dir = {0.0f,-1.0f,0.0f};
+   mdl_transform_vector( &ent->transform, dir, dir );
+   v3_angles( dir, cam->angles );
+   v3_copy( ent->transform.co, cam->pos );
+   cam->fov = ent->fov;
+}
diff --git a/src/ent_camera.h b/src/ent_camera.h
new file mode 100644 (file)
index 0000000..bf4ce14
--- /dev/null
@@ -0,0 +1,3 @@
+#include "entity.h"
+
+void ent_camera_unpack( ent_camera *ent, vg_camera *cam );
diff --git a/src/ent_challenge.c b/src/ent_challenge.c
new file mode 100644 (file)
index 0000000..d30a5dc
--- /dev/null
@@ -0,0 +1,127 @@
+#include "vg/vg_engine.h"
+#include "entity.h"
+#include "input.h"
+#include "gui.h"
+#include "audio.h"
+
+entity_call_result ent_challenge_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+
+   if( call->function == 0 ) /* unlock() */
+   {
+      if( !challenge->status )
+      {
+         vg_info( "challenge( '%s' )\n", 
+                  mdl_pstr( &world->meta, challenge->pstr_alias) );
+         ent_call call;
+         call.data = NULL;
+         call.function = challenge->target_event;
+         call.id = challenge->target;
+         entity_call( world, &call );
+      }
+      challenge->status = 1;
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 1 ) /* view() */
+   {
+      if( (localplayer.subsystem == k_player_subsystem_walk) &&
+          (world_static.challenge_target == NULL) )
+      {
+         world_static.challenge_target = NULL;
+         world_entity_set_focus( call->id );
+         world_entity_focus_modal();
+
+         gui_helper_clear();
+         vg_str text;
+         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
+            vg_strcat( &text, "Start" );
+         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
+            vg_strcat( &text, "Exit" );
+      }
+      return k_entity_call_result_OK;
+   }
+   else 
+      return k_entity_call_result_unhandled;
+}
+
+void ent_challenge_preupdate( ent_focus_context *ctx )
+{
+   world_instance *world = ctx->world;
+   ent_challenge *challenge = mdl_arritm( &world->ent_challenge, ctx->index );
+
+   /* maximum distance from active challenge */
+   if( !ctx->active )
+   {
+      f32 min_dist2 = 999999.9f;
+
+      if( mdl_entity_id_type( challenge->first ) == k_ent_objective )
+      {
+         u32 next = challenge->first;
+         while( mdl_entity_id_type(next) == k_ent_objective ){
+            u32 index = mdl_entity_id_id( next );
+            ent_objective *objective = mdl_arritm(&world->ent_objective,index);
+            next = objective->id_next;
+
+            f32 d2 = v3_dist2( localplayer.rb.co, objective->transform.co );
+            if( d2 < min_dist2 ) 
+               min_dist2 = d2;
+         }
+      }
+
+      f32 max_dist = 100.0f;
+      if( min_dist2 > max_dist*max_dist ){
+         world_static.challenge_target = NULL;
+         world_static.challenge_timer = 0.0f;
+         world_entity_clear_focus();
+         audio_lock();
+         audio_oneshot_3d( &audio_challenge[6], localplayer.rb.co,
+                           30.0f, 1.0f );
+         audio_unlock();
+      }
+      return;
+   }
+
+   world_entity_focus_camera( world, challenge->camera );
+
+   if( mdl_entity_id_type( challenge->first ) == k_ent_objective ){
+      if( button_down( k_srbind_maccept ) ){
+         u32 index = mdl_entity_id_id( challenge->first );
+         world_static.challenge_target = mdl_arritm( &world->ent_objective, 
+                                                     index );
+         world_static.challenge_timer = 0.0f;
+         world_entity_exit_modal();
+         gui_helper_clear();
+
+         u32 next = challenge->first;
+         while( mdl_entity_id_type(next) == k_ent_objective ){
+            u32 index = mdl_entity_id_id( next );
+            ent_objective *objective = mdl_arritm(&world->ent_objective,index);
+            objective->flags &= ~k_ent_objective_passed;
+            next = objective->id_next;
+            v3_fill( objective->transform.s, 1.0f );
+         }
+         audio_lock();
+         audio_oneshot( &audio_challenge[5], 1.0f, 0.0f );
+         audio_unlock();
+         return;
+      }
+   }
+
+   if( button_down( k_srbind_mback ) )
+   {
+      world_static.challenge_target = NULL;
+      world_entity_exit_modal();
+      world_entity_clear_focus();
+      gui_helper_clear();
+      audio_lock();
+      audio_oneshot( &audio_challenge[4], 1.0f, 0.0f );
+      audio_unlock();
+      return;
+   }
+}
+
+static void ent_challenge_render( ent_challenge *challenge ){
+   
+}
diff --git a/src/ent_challenge.h b/src/ent_challenge.h
new file mode 100644 (file)
index 0000000..f53c956
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+#include "entity.h"
+
+void ent_challenge_preupdate( ent_focus_context *ctx );
+entity_call_result ent_challenge_call( world_instance *world, ent_call *call );
diff --git a/src/ent_glider.c b/src/ent_glider.c
new file mode 100644 (file)
index 0000000..12b0575
--- /dev/null
@@ -0,0 +1,25 @@
+#pragma once
+#include "entity.h"
+#include "player_glide.h"
+
+entity_call_result ent_glider_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_glider *glider = mdl_arritm( &world->ent_glider, index );
+
+   if( call->function == 0 )
+   {
+      glider->flags |= 0x1;
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 1 )
+   {
+      if( glider->flags & 0x1 )
+      {
+         player_glide_equip_glider();
+      }
+      return k_entity_call_result_OK;
+   }
+   else 
+      return k_entity_call_result_unhandled;
+}
diff --git a/src/ent_glider.h b/src/ent_glider.h
new file mode 100644 (file)
index 0000000..5815dda
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+#include "entity.h"
+
+entity_call_result ent_glider_call( world_instance *world, ent_call *call );
diff --git a/src/ent_miniworld.c b/src/ent_miniworld.c
new file mode 100644 (file)
index 0000000..19c88d0
--- /dev/null
@@ -0,0 +1,211 @@
+#include "entity.h"
+#include "ent_miniworld.h"
+#include "world_render.h"
+#include "world_load.h"
+#include "input.h"
+#include "gui.h"
+#include "menu.h"
+#include "audio.h"
+
+struct global_miniworld global_miniworld;
+
+entity_call_result ent_miniworld_call( world_instance *world, ent_call *call )
+{
+   ent_miniworld *miniworld = mdl_arritm( &world->ent_miniworld, 
+                                          mdl_entity_id_id(call->id) );
+
+   int world_id = world - world_static.instances;
+
+   if( call->function == 0 ) /* zone() */
+   {
+      const char *uid = mdl_pstr( &world->meta, miniworld->pstr_world );
+      skaterift_load_world_command( 1, (const char *[]){ uid } );
+
+      mdl_transform_m4x3( &miniworld->transform, global_miniworld.mmdl );
+      global_miniworld.active = miniworld;
+
+      gui_helper_clear();
+      vg_str text;
+
+      if( gui_new_helper( input_button_list[k_srbind_miniworld_resume], &text ))
+         vg_strcat( &text, "Enter World" );
+
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 1 )
+   {
+      global_miniworld.active = NULL;
+      gui_helper_clear();
+
+      if( miniworld->proxy )
+      {
+         ent_prop *prop = mdl_arritm( &world->ent_prop, 
+                                      mdl_entity_id_id(miniworld->proxy) );
+         prop->flags &= ~0x1;
+      }
+
+      return k_entity_call_result_OK;
+   }
+   else
+      return k_entity_call_result_unhandled;
+}
+
+static void miniworld_icon( vg_camera *cam, enum gui_icon icon, 
+                            v3f pos, f32 size)
+{
+   m4x3f mmdl;
+   v3_copy( cam->transform[2], mmdl[2] );
+   mmdl[2][1] = 0.0f;
+   v3_normalize( mmdl[2] );
+   v3_copy( (v3f){0,1,0}, mmdl[1] );
+   v3_cross( mmdl[1], mmdl[2], mmdl[0] );
+   m4x3_mulv( global_miniworld.mmdl, pos, mmdl[3] );
+
+   shader_model_font_uMdl( mmdl );
+   shader_model_font_uOffset( (v4f){0,0,0,20.0f*size} );
+
+   m4x4f m4mdl;
+   m4x3_expand( mmdl, m4mdl );
+   m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
+   shader_model_font_uPvmPrev( m4mdl );
+
+   mdl_submesh *sm = gui.icons[ icon ];
+   if( sm )
+      mdl_draw_submesh( sm );
+}
+
+void ent_miniworld_render( world_instance *host_world, vg_camera *cam )
+{
+   if( host_world != &world_static.instances[k_world_purpose_hub] )
+      return;
+
+   ent_miniworld *miniworld = global_miniworld.active;
+
+   if( !miniworld )
+      return;
+
+   world_instance *dest_world = &world_static.instances[k_world_purpose_client];
+
+   int rendering = 1;
+   if( dest_world->status != k_world_status_loaded )
+      rendering = 0;
+
+   if( miniworld->proxy ){
+      ent_prop *prop = mdl_arritm( &host_world->ent_prop, 
+                                   mdl_entity_id_id(miniworld->proxy) );
+      if( !rendering )
+         prop->flags &= ~0x1;
+      else
+         prop->flags |= 0x1;
+   }
+
+
+   if( !rendering )
+      return;
+
+   render_world_override( dest_world, host_world, global_miniworld.mmdl, cam,
+                          NULL, (v4f){dest_world->tar_min,10000.0f,0.0f,0.0f} );
+   render_world_routes( dest_world, host_world, 
+                        global_miniworld.mmdl, cam, 0, 1 );
+
+   /* icons
+    * ---------------------*/
+   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, cam );
+   mesh_bind( &gui.icons_mesh );
+
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, gui.icons_texture );
+   shader_model_font_uTexMain( 0 );
+   shader_model_font_uColour( (v4f){1,1,1,1} );
+
+   miniworld_icon( cam, k_gui_icon_player, dest_world->player_co, 
+                  1.0f + sinf(vg.time)*0.2f );
+
+   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_challenge); i++ ){
+      ent_challenge *challenge = mdl_arritm( &dest_world->ent_challenge, i );
+
+      enum gui_icon icon = k_gui_icon_exclaim;
+      if( challenge->status )
+         icon = k_gui_icon_tick;
+
+      miniworld_icon( cam, icon, challenge->transform.co, 1.0f );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &dest_world->ent_route, i );
+
+      if( route->flags & k_ent_route_flag_achieve_gold ){
+         miniworld_icon( cam, k_gui_icon_rift_run_gold, 
+                         route->board_transform[3],1.0f);
+      }
+      else if( route->flags & k_ent_route_flag_achieve_silver ){
+         miniworld_icon( cam, k_gui_icon_rift_run_silver, 
+                         route->board_transform[3],1.0f);
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&dest_world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &dest_world->ent_route, i );
+
+      v4f colour;
+      v4_copy( route->colour, colour );
+      v3_muls( colour, 1.6666f, colour );
+      shader_model_font_uColour( colour );
+      miniworld_icon( cam, k_gui_icon_rift_run, route->board_transform[3],1.0f);
+   }
+}
+
+void ent_miniworld_preupdate(void)
+{
+   world_instance *hub = world_current_instance(),
+                  *dest = &world_static.instances[k_world_purpose_client];
+
+   ent_miniworld *miniworld = global_miniworld.active;
+
+   if( (localplayer.subsystem != k_player_subsystem_walk) ||
+       (global_miniworld.transition) ||
+       (world_static.active_instance != k_world_purpose_hub) ||
+       (!miniworld) ||
+       (dest->status != k_world_status_loaded) ||
+       (skaterift.activity != k_skaterift_default)) {
+      return;
+   }
+
+   if( button_down( k_srbind_miniworld_resume ) )
+   {
+      if( skaterift.demo_mode )
+      {
+         if( world_static.instance_addons[1]->flags & ADDON_REG_PREMIUM )
+         {
+            menu_open( k_menu_page_premium );
+            return;
+         }
+      }
+
+      global_miniworld.transition = 1;
+      global_miniworld.t = 0.0f;
+      global_miniworld.cam = g_render.cam;
+
+      world_switch_instance(1);
+      srinput.state = k_input_state_resume;
+      menu.disable_open = 0;
+      gui_helper_clear();
+      audio_lock();
+      audio_oneshot( &audio_ui[2], 1.0f, 0.0f );
+      audio_unlock();
+   }
+}
+
+void ent_miniworld_goback(void)
+{
+   audio_lock();
+   audio_oneshot( &audio_ui[2], 1.0f, 0.0f );
+   audio_unlock();
+
+   global_miniworld.transition = -1;
+   global_miniworld.t = 1.0f;
+
+   global_miniworld.cam = g_render.cam;
+   vg_m4x3_transform_camera( global_miniworld.mmdl, &global_miniworld.cam );
+   world_switch_instance(0);
+}
diff --git a/src/ent_miniworld.h b/src/ent_miniworld.h
new file mode 100644 (file)
index 0000000..278cf75
--- /dev/null
@@ -0,0 +1,18 @@
+#pragma once
+#include "entity.h"
+
+struct global_miniworld
+{
+   ent_miniworld *active;
+   int transition;
+   f32 t;
+
+   m4x3f mmdl;
+   vg_camera cam;
+}
+extern global_miniworld;
+
+entity_call_result ent_miniworld_call( world_instance *world, ent_call *call );
+void ent_miniworld_render( world_instance *host_world, vg_camera *cam );
+void ent_miniworld_goback(void);
+void ent_miniworld_preupdate(void);
diff --git a/src/ent_npc.c b/src/ent_npc.c
new file mode 100644 (file)
index 0000000..7a4b484
--- /dev/null
@@ -0,0 +1,304 @@
+#include "vg/vg_mem.h"
+#include "ent_npc.h"
+#include "shaders/model_character_view.h"
+#include "input.h"
+#include "player.h"
+#include "gui.h"
+
+struct npc npc_gumpa, npc_slowmo, npc_volc_flight;
+static struct skeleton_anim *gumpa_idle;
+static struct skeleton_anim *slowmo_momentum, *slowmo_slide, *slowmo_rewind,
+                            *anim_tutorial_cam;
+static float slowmo_opacity = 0.0f;
+static f64 volc_start_preview = 0.0;
+
+void npc_load_model( struct npc *npc, const char *path )
+{
+   vg_linear_clear( vg_mem.scratch );
+   
+   mdl_context *meta = &npc->meta;
+   mdl_open( meta, path, vg_mem.rtmemory );
+   mdl_load_metadata_block( meta, vg_mem.rtmemory );
+   mdl_load_animation_block( meta, vg_mem.rtmemory );
+
+   struct skeleton *sk = &npc->skeleton;
+   skeleton_setup( sk, vg_mem.rtmemory, meta );
+
+   u32 mtx_size = sizeof(m4x3f)*sk->bone_count;
+   npc->final_mtx = vg_linear_alloc( vg_mem.rtmemory, mtx_size );
+
+   if( mdl_arrcount( &meta->textures ) )
+   {
+      mdl_texture *tex0 = mdl_arritm( &meta->textures, 0 );
+      void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
+      mdl_fread_pack_file( meta, &tex0->file, data );
+
+      vg_tex2d_load_qoi_async( data, tex0->file.pack_size,
+                               VG_TEX2D_NEAREST|VG_TEX2D_CLAMP,
+                               &npc->texture );
+   }
+   else
+   {
+      npc->texture = vg.tex_missing;
+   }
+
+   mdl_async_load_glmesh( meta, &npc->mesh, NULL );
+   mdl_close( meta );
+}
+
+void npc_init(void)
+{
+   npc_load_model( &npc_gumpa, "models/gumpa.mdl" );
+   gumpa_idle = skeleton_get_anim( &npc_gumpa.skeleton, "gumpa_idle" );
+
+   npc_load_model( &npc_slowmo, "models/slowmos.mdl" );
+   slowmo_momentum = 
+      skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_momentum" );
+   slowmo_slide = skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_slide" );
+   slowmo_rewind = skeleton_get_anim( &npc_slowmo.skeleton, "slowmo_rewind" );
+
+   npc_load_model( &npc_volc_flight, "models/volc_flight.mdl" );
+   anim_tutorial_cam = 
+      skeleton_get_anim( &npc_volc_flight.skeleton, "tutorial" );
+}
+
+static struct npc *npc_resolve( u32 id )
+{
+   if( id == 1 ) return &npc_gumpa;
+   else if( id == 2 ) return &npc_slowmo;
+   else if( id == 3 ) return &npc_volc_flight;
+   else return NULL;
+}
+
+static entity_call_result npc_slowmo_call( ent_npc *npc, ent_call *call )
+{
+   if( call->function == 0 )
+   {
+      gui_helper_clear();
+      vg_str text;
+
+      if( npc->context == 2 )
+      {
+         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ))
+            vg_strcat( &text, "Crouch (store energy)" );
+         if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ))
+            vg_strcat( &text, "Slide" );
+      }
+      else if( npc->context == 1 )
+      {
+         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ))
+            vg_strcat( &text, "Crouch (store energy)" );
+      }
+      else if( npc->context == 3 )
+      {
+         if( gui_new_helper( input_button_list[k_srbind_reset], &text ))
+            vg_strcat( &text, "Rewind time" );
+         if( gui_new_helper( input_button_list[k_srbind_replay_resume], &text ))
+            vg_strcat( &text, "Resume" );
+      }
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == -1 )
+   {
+      world_entity_clear_focus();
+      gui_helper_clear();
+      return k_entity_call_result_OK;
+   }
+   else
+      return k_entity_call_result_unhandled;
+}
+
+entity_call_result ent_npc_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_npc *npc = mdl_arritm( &world->ent_npc, index );
+
+   if( npc->id == 2 )
+   {
+      return npc_slowmo_call( npc, call );
+   }
+   else if( npc->id == 3 )
+   {
+      if( call->function == 0 )
+      {
+         world_entity_set_focus( call->id );
+         gui_helper_clear();
+         vg_str text;
+         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
+            vg_strcat( &text, "Preview course" );
+         return k_entity_call_result_OK;
+      }
+      else if( call->function == -1 )
+      {
+         world_entity_clear_focus();
+         gui_helper_clear();
+         return k_entity_call_result_OK;
+      }
+      else
+         return k_entity_call_result_unhandled;
+   }
+   else if( npc->id == 4 )
+   {
+      if( call->function == 0 )
+      {
+         gui_helper_clear();
+         vg_str text;
+         if( gui_new_helper( input_button_list[k_srbind_camera], &text ))
+            vg_strcat( &text, "First/Thirdperson" );
+         return k_entity_call_result_OK;
+      }
+      else if( call->function == -1 )
+      {
+         gui_helper_clear();
+         return k_entity_call_result_OK;
+      }
+      else
+         return k_entity_call_result_unhandled;
+   }
+   else
+   {
+      if( call->function == 0 )
+      {
+         world_entity_set_focus( call->id );
+         gui_helper_clear();
+         vg_str text;
+         if( gui_new_helper( input_button_list[k_srbind_maccept], &text ))
+            vg_strcat( &text, "Talk to ???" );
+         return k_entity_call_result_OK;
+      }
+      else if( call->function == -1 )
+      {
+         world_entity_clear_focus();
+         gui_helper_clear();
+         return k_entity_call_result_OK;
+      }
+      else 
+         return k_entity_call_result_unhandled;
+   }
+}
+
+void ent_npc_preupdate( ent_focus_context *ctx )
+{
+   world_instance *world = ctx->world;
+   ent_npc *ent = mdl_arritm( &world->ent_npc, ctx->index );
+
+   if( !ctx->active )
+   {
+      if( button_down(k_srbind_maccept) )
+      {
+         world_entity_focus_modal();
+         gui_helper_clear();
+         vg_str text;
+         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
+            vg_strcat( &text, "leave" );
+
+         volc_start_preview = vg.time;
+      }
+
+      return;
+   }
+
+   if( ent->id == 3 )
+   {
+      player_pose pose;
+      struct skeleton *sk = &npc_volc_flight.skeleton;
+
+      f64 t = (vg.time - volc_start_preview) * 0.5;
+      skeleton_sample_anim_clamped( sk, anim_tutorial_cam, t, pose.keyframes );
+      
+      ent_camera *cam = mdl_arritm( &world->ent_camera, 
+                                     mdl_entity_id_id(ent->camera) );
+      v3_copy( pose.keyframes[0].co, cam->transform.co );
+
+      v4f qp;
+      q_axis_angle( qp, (v3f){1,0,0}, VG_TAUf*0.25f );
+      q_mul( pose.keyframes[0].q, qp, cam->transform.q );
+      q_normalize( cam->transform.q );
+
+      v3_add( ent->transform.co, cam->transform.co, cam->transform.co );
+   }
+
+   world_entity_focus_camera( world, ent->camera );
+
+   if( button_down( k_srbind_mback ) )
+   {
+      world_entity_exit_modal();
+      world_entity_clear_focus();
+      gui_helper_clear();
+   }
+}
+
+void npc_update( ent_npc *ent )
+{
+   if( ent->id == 3 ) return;
+   if( ent->id == 4 ) return;
+
+   struct npc *npc_def = npc_resolve( ent->id );
+   VG_ASSERT( npc_def );
+
+   player_pose pose;
+   struct skeleton *sk = &npc_def->skeleton;
+   pose.type = k_player_pose_type_ik;
+   pose.board.lean = 0.0f;
+
+   if( ent->id == 1 )
+   {
+      skeleton_sample_anim( sk, gumpa_idle, vg.time, pose.keyframes );
+   }
+   else if( ent->id == 2 )
+   {
+      struct skeleton_anim *anim = NULL;
+      if( ent->context == 1 ) anim = slowmo_momentum;
+      else if( ent->context == 2 ) anim = slowmo_slide;
+      else if( ent->context == 3 ) anim = slowmo_rewind;
+
+      VG_ASSERT( anim );
+
+      f32 t        = vg.time*0.5f,
+          animtime = fmodf( t*anim->rate, anim->length ),
+          lt       = animtime / (f32)anim->length;
+      skeleton_sample_anim( sk, anim, t, pose.keyframes );
+      slowmo_opacity = vg_clampf(fabsf(lt-0.5f)*9.0f-3.0f,0,1);
+   }
+
+   v3_copy( ent->transform.co, pose.root_co );
+   v4_copy( ent->transform.q, pose.root_q );
+   apply_full_skeleton_pose( &npc_def->skeleton, &pose, npc_def->final_mtx );
+}
+
+void npc_render( ent_npc *ent, world_instance *world, vg_camera *cam )
+{
+   if( ent->id == 3 ) return;
+   if( ent->id == 4 ) return;
+
+   struct npc *npc_def = npc_resolve( ent->id );
+   VG_ASSERT( npc_def );
+
+   shader_model_character_view_use();
+
+       glActiveTexture( GL_TEXTURE0 );
+       glBindTexture( GL_TEXTURE_2D, npc_def->texture );
+   shader_model_character_view_uTexMain( 0 );
+   shader_model_character_view_uCamera( cam->transform[3] );
+   shader_model_character_view_uPv( cam->mtx.pv );
+
+   if( ent->id == 2 )
+   {
+      shader_model_character_view_uDepthMode( 2 );
+      shader_model_character_view_uDitherCutoff( slowmo_opacity );
+   }
+   else 
+   {
+      shader_model_character_view_uDepthMode( 0 );
+   }
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_character_view );
+
+   glUniformMatrix4x3fv( _uniform_model_character_view_uTransforms,
+                         npc_def->skeleton.bone_count,
+                         0,
+                         (const GLfloat *)npc_def->final_mtx );
+   
+   mesh_bind( &npc_def->mesh );
+   mesh_draw( &npc_def->mesh );
+}
diff --git a/src/ent_npc.h b/src/ent_npc.h
new file mode 100644 (file)
index 0000000..c20cc97
--- /dev/null
@@ -0,0 +1,28 @@
+#pragma once
+#include "player_render.h"
+#include "entity.h"
+
+struct npc
+{
+   glmesh mesh;
+   GLuint texture;
+
+   mdl_context meta;
+   struct skeleton skeleton;
+
+   m4x3f *final_mtx;
+}
+extern npc_gumpa;
+
+enum npc_id
+{
+   k_npc_id_none = 0,
+   k_npc_id_gumpa = 1
+};
+
+void npc_load_model( struct npc *npc, const char *path );
+void ent_npc_preupdate( ent_focus_context *context );
+entity_call_result ent_npc_call( world_instance *world, ent_call *call );
+void npc_update( ent_npc *ent );
+void npc_render( ent_npc *ent, world_instance *world, vg_camera *cam );
+void npc_init(void);
diff --git a/src/ent_objective.c b/src/ent_objective.c
new file mode 100644 (file)
index 0000000..6a8bbe5
--- /dev/null
@@ -0,0 +1,139 @@
+#include "world.h"
+#include "world_load.h"
+#include "entity.h"
+#include "audio.h"
+#include "steam.h"
+#include "ent_region.h"
+#include "player.h"
+#include "player_skate.h"
+
+static void ent_objective_pass( world_instance *world, 
+                                   ent_objective *objective ){
+   if( objective->id_next ){
+      world_static.challenge_timer += objective->filter;
+
+      u32 index = mdl_entity_id_id( objective->id_next );
+      ent_objective *next = mdl_arritm( &world->ent_objective, index );
+      world_static.challenge_target = next;
+      objective->flags |= k_ent_objective_passed;
+
+      if( next->filter & k_ent_objective_filter_passthrough )
+         ent_objective_pass( world, next );
+      else{
+         vg_info( "pass challenge point\n" );
+         audio_lock();
+         audio_oneshot_3d( &audio_challenge[0], localplayer.rb.co,
+                           30.0f, 1.0f );
+         audio_unlock();
+      }
+   }
+   else {
+      vg_success( "challenge win\n" );
+      audio_lock();
+      audio_oneshot( &audio_challenge[2], 1.0f, 0.0f );
+      audio_unlock();
+      world_static.challenge_target = NULL;
+      world_static.challenge_timer = 0.0f;
+      world_static.focused_entity = 0;
+
+      if( objective->id_win ){
+         ent_call call;
+         call.data = NULL;
+         call.function = objective->win_event;
+         call.id = objective->id_win;
+         entity_call( world, &call );
+      }
+
+      ent_region_re_eval( world );
+   }
+}
+
+static int ent_objective_check_filter( ent_objective *objective ){
+   if( objective->filter ){
+      struct player_skate_state *s = &player_skate.state;
+      enum trick_type trick = s->trick_type;
+
+      u32 state = 0x00;
+
+      if( trick == k_trick_type_shuvit ) 
+         state |= k_ent_objective_filter_trick_shuvit;
+      if( trick == k_trick_type_treflip )
+         state |= k_ent_objective_filter_trick_treflip;
+      if( trick == k_trick_type_kickflip )
+         state |= k_ent_objective_filter_trick_kickflip;
+      
+      if( s->flip_rate < -0.0001f ) state |= k_ent_objective_filter_flip_back;
+      if( s->flip_rate >  0.0001f ) state |= k_ent_objective_filter_flip_front;
+
+      if( s->activity == k_skate_activity_grind_5050 ||
+          s->activity == k_skate_activity_grind_back50 ||
+          s->activity == k_skate_activity_grind_front50 ) 
+         state |= k_ent_objective_filter_grind_truck_any;
+
+      if( s->activity == k_skate_activity_grind_boardslide )
+         state |= k_ent_objective_filter_grind_board_any;
+
+      return ((objective->filter  & state) || !objective->filter) && 
+             ((objective->filter2 & state) || !objective->filter2);
+   }
+   else {
+      return 1;
+   }
+}
+
+entity_call_result ent_objective_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+
+   if( call->function == 0 )
+   {
+      if( objective->flags & (k_ent_objective_hidden|k_ent_objective_passed))
+      {
+         return k_entity_call_result_OK;
+      }
+
+      if( world_static.challenge_target )
+      {
+         if( (world_static.challenge_target == objective) && 
+              ent_objective_check_filter( objective )){
+            ent_objective_pass( world, objective );
+         }
+         else 
+         {
+            audio_lock();
+            audio_oneshot_3d( &audio_challenge[6], localplayer.rb.co,
+                              30.0f, 1.0f );
+            audio_unlock();
+            vg_error( "challenge failed\n" );
+            world_static.challenge_target = NULL;
+            world_static.challenge_timer = 0.0f;
+            world_static.focused_entity = 0;
+         }
+      }
+
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 2 )
+   {
+      objective->flags &= ~k_ent_objective_hidden;
+
+      if( mdl_entity_id_type( objective->id_next ) == k_ent_objective ){
+         call->id = objective->id_next;
+         entity_call( world, call );
+      }
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 3 )
+   {
+      objective->flags |=  k_ent_objective_hidden;
+
+      if( mdl_entity_id_type( objective->id_next ) == k_ent_objective ){
+         call->id = objective->id_next;
+         entity_call( world, call );
+      }
+      return k_entity_call_result_OK;
+   }
+   else 
+      return k_entity_call_result_unhandled;
+}
diff --git a/src/ent_objective.h b/src/ent_objective.h
new file mode 100644 (file)
index 0000000..9d94772
--- /dev/null
@@ -0,0 +1,4 @@
+#pragma once
+#include "entity.h"
+#include "world.h"
+entity_call_result ent_objective_call( world_instance *world, ent_call *call );
diff --git a/src/ent_region.c b/src/ent_region.c
new file mode 100644 (file)
index 0000000..5e0ec03
--- /dev/null
@@ -0,0 +1,165 @@
+#include "ent_region.h"
+#include "gui.h"
+#include "network_common.h"
+#include "network.h"
+
+struct global_ent_region global_ent_region;
+
+u32 region_spark_colour( u32 flags )
+{
+   if( flags & k_ent_route_flag_achieve_gold )
+      return 0xff8ce0fa;
+   else if( flags & k_ent_route_flag_achieve_silver )
+      return 0xffc2c2c2;
+   else 
+      return 0x00;
+}
+
+entity_call_result ent_region_call( world_instance *world, ent_call *call )
+{
+   ent_region *region = 
+      mdl_arritm( &world->ent_region, mdl_entity_id_id(call->id) );
+
+   if( !region->zone_volume )
+      return k_entity_call_result_invalid;
+
+   ent_volume *volume = 
+      mdl_arritm( &world->ent_volume, mdl_entity_id_id(region->zone_volume) );
+
+   if( call->function == 0 ) /* enter */
+   {
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i ++ )
+      {
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+
+         v3f local;
+         m4x3_mulv( volume->to_local, route->board_transform[3], local );
+         if( (fabsf(local[0]) <= 1.0f) &&
+             (fabsf(local[1]) <= 1.0f) &&
+             (fabsf(local[2]) <= 1.0f) )
+         {
+            route->flags &= ~k_ent_route_flag_out_of_zone;
+         }
+         else 
+         {
+            route->flags |= k_ent_route_flag_out_of_zone;
+         }
+      }
+
+      gui_location_print_ccmd( 1, (const char *[]){
+            mdl_pstr(&world->meta,region->pstr_title)} );
+
+      vg_strncpy( mdl_pstr(&world->meta,region->pstr_title), 
+                  global_ent_region.location, NETWORK_REGION_MAX,
+                  k_strncpy_always_add_null );
+      global_ent_region.flags = region->flags;
+      network_send_region();
+
+      localplayer.effect_data.spark.colour = region_spark_colour(region->flags);
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == 1 ) /* leave */
+   {
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i ++ )
+      {
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+         route->flags |= k_ent_route_flag_out_of_zone;
+      }
+      localplayer.effect_data.spark.colour = 0x00;
+      return k_entity_call_result_OK;
+   }
+   else
+      return k_entity_call_result_unhandled;
+}
+
+/* 
+ * reevaluate all achievements to calculate the compiled achievement
+ */
+void ent_region_re_eval( world_instance *world )
+{
+   u32 world_total = k_ent_route_flag_achieve_gold | 
+                     k_ent_route_flag_achieve_silver;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_region); i ++ ){
+      ent_region *region = mdl_arritm(&world->ent_region, i);
+
+      if( !region->zone_volume )
+         continue;
+
+      ent_volume *volume = mdl_arritm(&world->ent_volume,
+            mdl_entity_id_id(region->zone_volume));
+
+      u32 combined = k_ent_route_flag_achieve_gold | 
+                     k_ent_route_flag_achieve_silver;
+
+      for( u32 j=0; j<mdl_arrcount(&world->ent_route); j ++ ){
+         ent_route *route = mdl_arritm(&world->ent_route, j );
+
+         v3f local;
+         m4x3_mulv( volume->to_local, route->board_transform[3], local );
+         if( !((fabsf(local[0]) <= 1.0f) &&
+               (fabsf(local[1]) <= 1.0f) &&
+               (fabsf(local[2]) <= 1.0f)) ){
+            continue;
+         }
+
+         combined &= route->flags;
+      }
+
+      for( u32 j=0; j<mdl_arrcount(&world->ent_challenge); j ++ ){
+         ent_challenge *challenge = mdl_arritm( &world->ent_challenge, j );
+
+         v3f local;
+         m4x3_mulv( volume->to_local, challenge->transform.co, local );
+         if( !((fabsf(local[0]) <= 1.0f) &&
+               (fabsf(local[1]) <= 1.0f) &&
+               (fabsf(local[2]) <= 1.0f)) ){
+            continue;
+         }
+
+         u32 flags = 0x00;
+         if( challenge->status ){
+            flags |= k_ent_route_flag_achieve_gold;
+            flags |= k_ent_route_flag_achieve_silver;
+         }
+
+         combined &= flags;
+      }
+
+      region->flags = combined;
+      world_total &= combined;
+
+      /* run unlock triggers. v105+ */
+      if( world->meta.info.version >= 105 ){
+         if( region->flags & (k_ent_route_flag_achieve_gold|
+                              k_ent_route_flag_achieve_silver) ){
+            if( region->target0[0] ){
+               ent_call call;
+               call.data = NULL;
+               call.id = region->target0[0];
+               call.function = region->target0[1];
+               entity_call( world, &call );
+            }
+         }
+      }
+   }
+
+   u32 instance_id = world - world_static.instances;
+
+   if( world_static.instance_addons[instance_id]->flags & ADDON_REG_MTZERO ){
+      if( world_total & k_ent_route_flag_achieve_gold ){
+         steam_set_achievement( "MTZERO_GOLD" );
+         steam_store_achievements();
+      }
+
+      if( world_total & k_ent_route_flag_achieve_silver ){
+         steam_set_achievement( "MTZERO_SILVER" );
+         steam_store_achievements();
+      }
+   }
+
+   if( world_static.instance_addons[instance_id]->flags & ADDON_REG_CITY ){
+      steam_set_achievement( "CITY_COMPLETE" );
+      steam_store_achievements();
+   }
+}
diff --git a/src/ent_region.h b/src/ent_region.h
new file mode 100644 (file)
index 0000000..6a86a32
--- /dev/null
@@ -0,0 +1,14 @@
+#pragma once
+#include "world_entity.h"
+#include "network_common.h"
+
+struct global_ent_region
+{
+   char location[ NETWORK_REGION_MAX ];
+   u32 flags;
+}
+extern global_ent_region;
+
+u32 region_spark_colour( u32 flags );
+void ent_region_re_eval( world_instance *world );
+entity_call_result ent_region_call( world_instance *world, ent_call *call );
diff --git a/src/ent_relay.c b/src/ent_relay.c
new file mode 100644 (file)
index 0000000..b22b0b5
--- /dev/null
@@ -0,0 +1,25 @@
+#include "ent_relay.h"
+
+entity_call_result ent_relay_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_relay *relay = mdl_arritm( &world->ent_relay, index );
+
+   if( call->function == 0 )
+   {
+      for( u32 i=0; i<VG_ARRAY_LEN(relay->targets); i++ )
+      {
+         if( relay->targets[i][0] )
+         {
+            ent_call call;
+            call.data = NULL;
+            call.function = relay->targets[i][1];
+            call.id = relay->targets[i][0];
+            entity_call( world, &call );
+         }
+      }
+      return k_entity_call_result_OK;
+   }
+   else 
+      return k_entity_call_result_unhandled;
+}
diff --git a/src/ent_relay.h b/src/ent_relay.h
new file mode 100644 (file)
index 0000000..054570c
--- /dev/null
@@ -0,0 +1,3 @@
+#pragma once
+#include "entity.h"
+entity_call_result ent_relay_call( world_instance *world, ent_call *call );
diff --git a/src/ent_route.c b/src/ent_route.c
new file mode 100644 (file)
index 0000000..8564eed
--- /dev/null
@@ -0,0 +1,75 @@
+#include "ent_route.h"
+#include "input.h"
+#include "gui.h"
+
+struct global_ent_route global_ent_route;
+
+entity_call_result ent_route_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_route *route = mdl_arritm( &world->ent_route, index );
+
+   if( call->function == 0 )
+   { /* view() */
+      if( localplayer.subsystem == k_player_subsystem_walk )
+      {
+         world_entity_set_focus( call->id );
+         world_entity_focus_modal();
+
+         gui_helper_clear();
+         vg_str text;
+
+         if( (global_ent_route.helper_weekly = 
+                  gui_new_helper( input_button_list[k_srbind_mleft], &text )))
+            vg_strcat( &text, "Weekly" );
+
+         if( (global_ent_route.helper_alltime = 
+                  gui_new_helper( input_button_list[k_srbind_mright], &text )))
+            vg_strcat( &text, "All time" );
+
+         if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
+            vg_strcat( &text, "Exit" );
+      }
+
+      return k_entity_call_result_OK;
+   }
+
+   return k_entity_call_result_unhandled;
+}
+
+void ent_route_preupdate( ent_focus_context *ctx )
+{
+   if( !ctx->active ) 
+      return;
+
+   world_instance *world = ctx->world;
+   ent_route *route = mdl_arritm( &world->ent_route, ctx->index );
+
+   u32 cam_id = 0;
+   
+   if( __builtin_expect( world->meta.info.version >= 103, 1 ) )
+      cam_id = route->id_camera;
+
+   world_entity_focus_camera( world, cam_id );
+
+   if( button_down( k_srbind_mleft ) ){
+      world_sfd.view_weekly = 1;
+      world_sfd_compile_active_scores();
+   }
+
+   if( button_down( k_srbind_mright ) ){
+      world_sfd.view_weekly = 0;
+      world_sfd_compile_active_scores();
+   }
+
+   global_ent_route.helper_alltime->greyed =!world_sfd.view_weekly;
+   global_ent_route.helper_weekly->greyed =  world_sfd.view_weekly;
+
+   if( button_down( k_srbind_mback ) )
+   {
+      world_entity_exit_modal();
+      world_entity_clear_focus();
+      gui_helper_clear();
+      return;
+   }
+}
diff --git a/src/ent_route.h b/src/ent_route.h
new file mode 100644 (file)
index 0000000..cbf61b2
--- /dev/null
@@ -0,0 +1,11 @@
+#pragma once
+#include "entity.h"
+
+struct global_ent_route
+{
+   struct gui_helper *helper_weekly, *helper_alltime;
+}
+extern global_ent_route;
+
+entity_call_result ent_route_call( world_instance *world, ent_call *call );
+void ent_route_preupdate( ent_focus_context *ctx );
diff --git a/src/ent_skateshop.c b/src/ent_skateshop.c
new file mode 100644 (file)
index 0000000..ded707c
--- /dev/null
@@ -0,0 +1,850 @@
+#include "vg/vg_steam_ugc.h"
+#include "vg/vg_msg.h"
+#include "vg/vg_tex.h"
+#include "vg/vg_image.h"
+#include "vg/vg_loader.h"
+#include "ent_skateshop.h"
+#include "world.h"
+#include "player.h"
+#include "gui.h"
+#include "menu.h"
+#include "steam.h"
+#include "addon.h"
+#include "save.h"
+#include "network.h"
+
+struct global_skateshop global_skateshop = 
+{
+   .render={.reg_id=0xffffffff,.world_reg=0xffffffff}
+};
+
+/*
+ * Checks string equality but does a hash check first
+ */
+static inline int const_str_eq( u32 hash, const char *str, const char *cmp )
+{
+   if( hash == vg_strdjb2(cmp) )
+      if( !strcmp( str, cmp ) )
+         return 1;
+   return 0;
+}
+
+static void skateshop_update_viewpage(void){
+   u32 page = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
+
+   for( u32 i=0; i<SKATESHOP_VIEW_SLOT_MAX; i++ ){
+      u32 j = SKATESHOP_VIEW_SLOT_MAX-1-i;
+      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[j];
+      addon_cache_unwatch( k_addon_type_board, slot->cache_id );
+   }
+   
+   for( u32 i=0; i<SKATESHOP_VIEW_SLOT_MAX; i++ ){
+      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[i];
+      u32 request_id = page*SKATESHOP_VIEW_SLOT_MAX + i;
+      slot->cache_id = addon_cache_create_viewer( k_addon_type_board,
+                                                  request_id );
+   }
+}
+
+struct async_preview_load_thread_data{
+   void *data;
+   addon_reg *reg;
+};
+
+static void skateshop_async_preview_imageload( void *data, u32 len ){
+   struct async_preview_load_thread_data *inf = data;
+
+   if( inf->data ){
+      glBindTexture( GL_TEXTURE_2D, global_skateshop.tex_preview );
+      glTexSubImage2D( GL_TEXTURE_2D, 0,0,0,
+                        WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT, 
+                        GL_RGB, GL_UNSIGNED_BYTE, inf->data );
+      glGenerateMipmap( GL_TEXTURE_2D );
+      stbi_image_free( inf->data );
+
+      skaterift.rt_textures[k_skaterift_rt_workshop_preview] =
+         global_skateshop.tex_preview;
+   }
+   else {
+      skaterift.rt_textures[k_skaterift_rt_workshop_preview] = vg.tex_missing;
+   }
+
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+   global_skateshop.reg_loaded_preview = inf->reg;
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+}
+
+static void skateshop_update_preview_image_thread(void *_args)
+{
+   char path_buf[4096];
+   vg_str folder;
+   vg_strnull( &folder, path_buf, sizeof(path_buf) );
+
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+   addon_reg *reg_preview = global_skateshop.reg_preview;
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+
+   if( !addon_get_content_folder( reg_preview, &folder, 1 ) )
+   {
+      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+      global_skateshop.reg_loaded_preview = reg_preview;
+      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+      return;
+   }
+
+   vg_strcat( &folder, "/preview.jpg" );
+   vg_async_item *call = 
+      vg_async_alloc( sizeof(struct async_preview_load_thread_data) );
+   struct async_preview_load_thread_data *inf = call->payload;
+
+   inf->reg = reg_preview;
+
+   if( vg_strgood( &folder ) )
+   {
+      stbi_set_flip_vertically_on_load(1);
+      int x, y, nc;
+      inf->data = stbi_load( folder.buffer, &x, &y, &nc, 3 );
+
+      if( inf->data )
+      {
+         if( (x != WORKSHOP_PREVIEW_WIDTH) || (y != WORKSHOP_PREVIEW_HEIGHT) )
+         {
+            vg_error( "Resolution does not match framebuffer, so we can't"
+                      " show it\n" );
+            stbi_image_free( inf->data );
+            inf->data = NULL;
+         }
+      }
+
+      vg_async_dispatch( call, skateshop_async_preview_imageload );
+   }
+   else
+   {
+      vg_error( "Path too long to workshop preview image.\n" );
+
+      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+      global_skateshop.reg_loaded_preview = reg_preview;
+      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+   }
+}
+
+void skateshop_world_preview_preupdate(void)
+{
+   /* try to load preview image if we availible to do. */
+   if( vg_loader_availible() )
+   {
+      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+      if( global_skateshop.reg_preview != global_skateshop.reg_loaded_preview )
+      {
+         SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+         vg_loader_start( skateshop_update_preview_image_thread, NULL );
+      }
+      else SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+   }
+}
+
+/*
+ * op/subroutine: k_workshop_op_item_load
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * Regular stuff
+ * -----------------------------------------------------------------------------
+ */
+
+static void skateshop_init_async(void *_data,u32 size){
+       glGenTextures( 1, &global_skateshop.tex_preview );
+       glBindTexture( GL_TEXTURE_2D, global_skateshop.tex_preview );
+   glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, 
+                  WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT,
+                  0, GL_RGB, GL_UNSIGNED_BYTE, NULL );
+
+   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
+                    GL_LINEAR_MIPMAP_LINEAR );
+   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
+   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
+
+   skaterift.rt_textures[ k_skaterift_rt_workshop_preview ] = vg.tex_missing;
+   skaterift.rt_textures[ k_skaterift_rt_server_status ] = vg.tex_missing;
+   render_server_status_gui();
+}
+
+/*
+ * VG event init
+ */
+void skateshop_init(void)
+{
+   vg_async_call( skateshop_init_async, NULL, 0 );
+}
+
+static u16 skateshop_selected_cache_id(void){
+   if( addon_count(k_addon_type_board, ADDON_REG_HIDDEN) ){
+      addon_reg *reg = get_addon_from_index( 
+            k_addon_type_board, global_skateshop.selected_board_id,
+            ADDON_REG_HIDDEN );
+      return reg->cache_id;
+   }
+   else return 0;
+}
+
+static void skateshop_server_helper_update(void){
+   vg_str text;
+   vg_strnull( &text, global_skateshop.helper_toggle->text, 
+               sizeof(global_skateshop.helper_toggle->text) );
+
+   if( skaterift.demo_mode ){
+      vg_strcat( &text, "Not availible in demo" );
+   }
+   else {
+      if( network_client.user_intent == k_server_intent_online )
+         vg_strcat( &text, "Disconnect" );
+      else
+         vg_strcat( &text, "Go Online" );
+   }
+}
+
+/*
+ * VG event preupdate 
+ */
+void temp_update_playermodel(void);
+void ent_skateshop_preupdate( ent_focus_context *ctx )
+{
+   if( !ctx->active ) 
+      return;
+
+   world_instance *world = ctx->world;
+   ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, ctx->index );
+
+   /* camera positioning */
+   ent_camera *ref = mdl_arritm( &world->ent_camera, 
+                                 mdl_entity_id_id(shop->id_camera) );
+      
+   v3f dir = {0.0f,-1.0f,0.0f};
+   mdl_transform_vector( &ref->transform, dir, dir );
+   v3_angles( dir, world_static.focus_cam.angles );
+
+   v3f lookat;
+   if( shop->type == k_skateshop_type_boardshop ||
+       shop->type == k_skateshop_type_worldshop ){
+      ent_marker *display = mdl_arritm( &world->ent_marker,
+                                    mdl_entity_id_id(shop->boards.id_display) );
+      v3_sub( display->transform.co, localplayer.rb.co, lookat );
+   }
+   else if( shop->type == k_skateshop_type_charshop ){
+      v3_sub( ref->transform.co, localplayer.rb.co, lookat );
+   }
+   else if( shop->type == k_skateshop_type_server ){
+      ent_prop *prop = mdl_arritm( &world->ent_prop,
+            mdl_entity_id_id(shop->server.id_lever) );
+      v3_sub( prop->transform.co, localplayer.rb.co, lookat );
+   }
+   else
+      vg_fatal_error( "Unknown store (%u)\n", shop->type );
+
+   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
+                 atan2f(lookat[0],lookat[2]) );
+
+   v3_copy( ref->transform.co, world_static.focus_cam.pos );
+   world_static.focus_cam.fov = ref->fov;
+
+   /* input */
+   if( shop->type == k_skateshop_type_boardshop ){
+      if( !vg_loader_availible() ) return;
+
+      u16 cache_id = skateshop_selected_cache_id();
+      global_skateshop.helper_pick->greyed = !cache_id;
+
+      /*
+       * Controls
+       * ----------------------
+       */
+      u32 opage = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
+
+      if( button_down( k_srbind_mleft ) ){
+         if( global_skateshop.selected_board_id > 0 ){
+            global_skateshop.selected_board_id --;
+         }
+      }
+
+      u32 valid_count = addon_count( k_addon_type_board, 0 );
+      if( button_down( k_srbind_mright ) ){
+         if( global_skateshop.selected_board_id+1 < valid_count ){
+            global_skateshop.selected_board_id ++;
+         }
+      }
+
+      u32 npage = global_skateshop.selected_board_id/SKATESHOP_VIEW_SLOT_MAX;
+
+      if( opage != npage ){
+         skateshop_update_viewpage();
+      }
+      else if( cache_id && button_down( k_srbind_maccept )){
+         vg_info( "chose board from skateshop (%u)\n", 
+                     global_skateshop.selected_board_id );
+
+         addon_cache_unwatch( k_addon_type_board, localplayer.board_view_slot );
+         addon_cache_watch( k_addon_type_board, cache_id );
+         localplayer.board_view_slot = cache_id;
+         network_send_item( k_netmsg_playeritem_board );
+
+         world_entity_exit_modal();
+         world_entity_clear_focus();
+         gui_helper_clear();
+         skaterift_autosave(1);
+         return;
+      }
+   }
+   else if( shop->type == k_skateshop_type_charshop ){
+      if( !vg_loader_availible() ) return;
+
+      int changed = 0;
+      u32 valid_count = addon_count( k_addon_type_player, ADDON_REG_HIDDEN );
+
+      if( button_down( k_srbind_mleft ) ){
+         if( global_skateshop.selected_player_id > 0 ){
+            global_skateshop.selected_player_id --;
+         }
+         else{
+            global_skateshop.selected_player_id = valid_count-1;
+         }
+
+         changed = 1;
+      }
+
+      if( button_down( k_srbind_mright ) ){
+         if( global_skateshop.selected_player_id+1 < valid_count ){
+            global_skateshop.selected_player_id ++;
+         }
+         else{
+            global_skateshop.selected_player_id = 0;
+         }
+
+         changed = 1;
+      }
+
+      if( changed ){
+         addon_reg *addon = get_addon_from_index( 
+               k_addon_type_player, global_skateshop.selected_player_id,
+               ADDON_REG_HIDDEN );
+
+         u32 real_id = get_index_from_addon( 
+               k_addon_type_player, addon );
+
+         player__use_model( real_id );
+      }
+
+      if( button_down( k_srbind_maccept ) ){
+         network_send_item( k_netmsg_playeritem_player );
+         world_entity_exit_modal();
+         world_entity_clear_focus();
+         gui_helper_clear();
+      }
+   }
+   else if( shop->type == k_skateshop_type_worldshop ){
+      int browseable = 0,
+          loadable = 0;
+
+      u32 valid_count = addon_count( k_addon_type_world, ADDON_REG_HIDDEN );
+
+      if( valid_count && vg_loader_availible() )
+         browseable = 1;
+
+      if( valid_count && vg_loader_availible() )
+         loadable = 1;
+
+      global_skateshop.helper_browse->greyed = !browseable;
+      global_skateshop.helper_pick->greyed = !loadable;
+      
+      addon_reg *selected_world = NULL;
+
+      int change = 0;
+      if( browseable ){
+         if( button_down( k_srbind_mleft ) ){
+            if( global_skateshop.selected_world_id > 0 ){
+               global_skateshop.selected_world_id --;
+               change = 1;
+            }
+         }
+
+         if( button_down( k_srbind_mright ) ){
+            if( global_skateshop.selected_world_id+1 < valid_count ){
+               global_skateshop.selected_world_id ++;
+               change = 1;
+            }
+         }
+
+         selected_world = get_addon_from_index( k_addon_type_world, 
+                     global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
+
+         if( change || (global_skateshop.reg_preview == NULL) ){
+            SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+            global_skateshop.reg_preview = selected_world;
+            SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+         }
+      }
+
+      if( loadable ){
+         if( button_down( k_srbind_maccept ) ){
+            skaterift_change_world_start( selected_world );
+         }
+      }
+   }
+   else if( shop->type == k_skateshop_type_server ){
+      f64 delta = vg.time_real - network_client.last_intent_change;
+
+      if( (delta > 5.0) && (!skaterift.demo_mode) ){
+         global_skateshop.helper_pick->greyed = 0;
+         if( button_down( k_srbind_maccept ) ){
+            network_client.user_intent = !network_client.user_intent;
+            network_client.last_intent_change = vg.time_real;
+            skateshop_server_helper_update();
+            render_server_status_gui();
+         }
+      }
+      else {
+         global_skateshop.helper_pick->greyed = 1;
+      }
+   }
+   else{
+      vg_fatal_error( "Unknown store (%u)\n", shop->type );
+   }
+
+   if( button_down( k_srbind_mback ) )
+   {
+      if( shop->type == k_skateshop_type_charshop )
+         network_send_item( k_netmsg_playeritem_player );
+
+      world_entity_exit_modal();
+      world_entity_clear_focus();
+      gui_helper_clear();
+      return;
+   }
+}
+
+void skateshop_world_preupdate( world_instance *world )
+{
+   for( u32 i=0; i<mdl_arrcount(&world->ent_skateshop); i++ ){
+      ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, i );
+
+      if( shop->type == k_skateshop_type_server ){
+         f32 a = network_client.user_intent;
+
+         vg_slewf( &network_client.fintent, a, vg.time_frame_delta );
+         a = (vg_smoothstepf( network_client.fintent ) - 0.5f) * (VG_PIf/2.0f);
+
+         ent_prop *lever = mdl_arritm( &world->ent_prop,
+               mdl_entity_id_id(shop->server.id_lever) );
+
+         /* we need parent transforms now? */
+         q_axis_angle( lever->transform.q, (v3f){0,0,1}, a );
+      }
+   }
+}
+
+static void skateshop_render_boardshop( ent_skateshop *shop ){
+   world_instance *world = world_current_instance();
+   u32 slot_count = VG_ARRAY_LEN(global_skateshop.shop_view_slots);
+
+   ent_marker *mark_rack = mdl_arritm( &world->ent_marker, 
+                                  mdl_entity_id_id(shop->boards.id_rack)),
+              *mark_display = mdl_arritm( &world->ent_marker,
+                                  mdl_entity_id_id(shop->boards.id_display));
+
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+   struct addon_cache *cache = &addon_system.cache[k_addon_type_board];
+   
+   /* Render loaded boards in the view slots */
+   for( u32 i=0; i<slot_count; i++ ){
+      struct shop_view_slot *slot = &global_skateshop.shop_view_slots[i];
+      float selected = 0.0f;
+
+      if( !slot->cache_id ) 
+         goto fade_out;
+
+      addon_cache_entry *entry = vg_pool_item( &cache->pool, slot->cache_id );
+
+      if( entry->state != k_addon_cache_state_loaded )
+         goto fade_out;
+
+      struct player_board *board = 
+         addon_cache_item( k_addon_type_board, slot->cache_id );
+
+      mdl_transform xform;
+      transform_identity( &xform );
+
+      xform.co[0] = -((float)i - ((float)slot_count)*0.5f)*0.45f;
+      mdl_transform_mul( &mark_rack->transform, &xform, &xform );
+
+
+      if( entry->reg_index == global_skateshop.selected_board_id ){
+         selected = 1.0f;
+      }
+
+      float t = slot->view_blend;
+      v3_lerp( xform.co, mark_display->transform.co, t, xform.co );
+      q_nlerp( xform.q, mark_display->transform.q, t, xform.q );
+      v3_lerp( xform.s, mark_display->transform.s, t, xform.s );
+
+      struct player_board_pose pose = {0};
+      m4x3f mmdl;
+      mdl_transform_m4x3( &xform, mmdl );
+      render_board( &g_render.cam, world, board, mmdl, 
+                    &pose, k_board_shader_entity );
+
+fade_out:;
+      float rate = 5.0f*vg.time_delta;
+      slot->view_blend = vg_lerpf( slot->view_blend, selected, rate );
+   }
+
+   ent_marker *mark_info = mdl_arritm( &world->ent_marker, 
+                                        mdl_entity_id_id(shop->boards.id_info));
+   m4x3f mtext, mrack;
+   mdl_transform_m4x3( &mark_info->transform, mtext );
+   mdl_transform_m4x3( &mark_rack->transform, mrack );
+
+   m4x3f mlocal, mmdl;
+   m4x3_identity( mlocal );
+
+   float scale = 0.2f,
+         thickness = 0.03f;
+
+   font3d_bind( &gui.font, k_font_shader_default, 0, world, &g_render.cam );
+   shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
+
+   /* Selection counter
+    * ------------------------------------------------------------------ */
+   m3x3_zero( mlocal );
+   v3_zero( mlocal[3] );
+   mlocal[0][0] = -scale*2.0f;
+   mlocal[1][2] = -scale*2.0f;
+   mlocal[2][1] = -thickness;
+   mlocal[3][2] = -0.7f;
+   m4x3_mul( mrack, mlocal, mmdl );
+
+   u32 valid_count = addon_count(k_addon_type_board,0);
+   if( valid_count ){
+      char buf[16];
+      vg_str str;
+      vg_strnull( &str, buf, sizeof(buf) );
+      vg_strcati32( &str, global_skateshop.selected_board_id+1 );
+      vg_strcatch( &str, '/' );
+      vg_strcati32( &str, valid_count );
+      font3d_simple_draw( 0, buf, &g_render.cam, mmdl );
+   }
+   else{
+      font3d_simple_draw( 0, "Nothing installed", &g_render.cam, mmdl );
+   }
+
+   u16 cache_id = skateshop_selected_cache_id();
+   struct addon_cache_entry *entry = vg_pool_item( &cache->pool, cache_id );
+   addon_reg *reg = NULL;
+
+   if( entry ) reg = entry->reg_ptr;
+
+   if( !reg ){
+      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+      global_skateshop.render.item_title = "";
+      global_skateshop.render.item_desc = "";
+      return;
+   }
+
+   if( global_skateshop.render.reg_id != global_skateshop.selected_board_id ){
+      global_skateshop.render.item_title = "";
+      global_skateshop.render.item_desc = "";
+      vg_msg msg;
+      vg_msg_init( &msg, reg->metadata, reg->metadata_len );
+
+      if( vg_msg_seekframe( &msg, "workshop" ) ){
+         const char *title = vg_msg_getkvstr( &msg, "title" );
+         if( title ) global_skateshop.render.item_title = title;
+
+         const char *dsc = vg_msg_getkvstr( &msg, "author" );
+         if( dsc ) global_skateshop.render.item_desc = dsc;
+         vg_msg_skip_frame( &msg );
+      }
+
+      global_skateshop.render.reg_id = global_skateshop.selected_board_id;
+   }
+
+   /* Skin title
+    * ----------------------------------------------------------------- */
+   m3x3_zero( mlocal );
+   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
+   mlocal[3][0] = -font3d_string_width( 0, global_skateshop.render.item_title );
+   mlocal[3][0] *= scale*0.5f;
+   mlocal[3][1] = 0.1f;
+   mlocal[3][2] = 0.0f;
+   m4x3_mul( mtext, mlocal, mmdl );
+   font3d_simple_draw( 0, global_skateshop.render.item_title, 
+                       &g_render.cam, mmdl );
+
+   /* Author name
+    * ----------------------------------------------------------------- */
+   scale *= 0.4f;
+   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
+   mlocal[3][0] = -font3d_string_width( 0, global_skateshop.render.item_desc );
+   mlocal[3][0] *= scale*0.5f;
+   mlocal[3][1] = 0.0f;
+   mlocal[3][2] = 0.0f;
+   m4x3_mul( mtext, mlocal, mmdl );
+   font3d_simple_draw( 0, global_skateshop.render.item_desc, 
+                       &g_render.cam, mmdl );
+
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+}
+
+static void skateshop_render_charshop( ent_skateshop *shop ){
+}
+
+static void skateshop_render_worldshop( ent_skateshop *shop ){
+   world_instance *world = world_current_instance();
+
+   ent_marker *mark_display = mdl_arritm( &world->ent_marker,
+                                  mdl_entity_id_id(shop->worlds.id_display)),
+              *mark_info = mdl_arritm( &world->ent_marker, 
+                                  mdl_entity_id_id(shop->boards.id_info));
+
+   if( global_skateshop.render.world_reg != global_skateshop.selected_world_id){
+      global_skateshop.render.world_title = "missing: workshop.title";
+
+      addon_reg *reg = get_addon_from_index( k_addon_type_world,
+            global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
+
+      if( !reg ) 
+         goto none;
+
+      if( reg->alias.workshop_id )
+      {
+         vg_msg msg;
+         vg_msg_init( &msg, reg->metadata, reg->metadata_len );
+
+         global_skateshop.render.world_loc = vg_msg_getkvstr(&msg,"location");
+         global_skateshop.render.world_reg = global_skateshop.selected_world_id;
+
+         if( vg_msg_seekframe( &msg, "workshop" ) )
+         {
+            global_skateshop.render.world_title = vg_msg_getkvstr(&msg,"title");
+            vg_msg_skip_frame( &msg );
+         }
+         else {
+            vg_warn( "No workshop body\n" );
+         }
+      }
+      else {
+         global_skateshop.render.world_title = reg->alias.foldername;
+      }
+   }
+
+none:;
+
+   /* Text */
+   char buftext[128], bufsubtext[128];
+   vg_str info, subtext;
+   vg_strnull( &info, buftext, 128 );
+   vg_strnull( &subtext, bufsubtext, 128 );
+   
+   u32 valid_count = addon_count(k_addon_type_world,ADDON_REG_HIDDEN);
+   if( valid_count )
+   {
+      vg_strcati32( &info, global_skateshop.selected_world_id+1 );
+      vg_strcatch( &info, '/' );
+      vg_strcati32( &info, valid_count );
+      vg_strcatch( &info, ' ' );
+      vg_strcat( &info, global_skateshop.render.world_title );
+
+      if( !vg_loader_availible() )
+      {
+         vg_strcat( &subtext, "Loading..." );
+      }
+      else
+      {
+         addon_reg *reg = get_addon_from_index( k_addon_type_world,
+                     global_skateshop.selected_world_id, ADDON_REG_HIDDEN );
+
+         if( reg->alias.workshop_id )
+            vg_strcat( &subtext, "(Workshop) " );
+
+         vg_strcat( &subtext, global_skateshop.render.world_loc );
+      }
+   }
+   else
+   {
+      vg_strcat( &info, "No workshop worlds installed" );
+   }
+
+   m4x3f mtext,mlocal,mtextmdl;
+   mdl_transform_m4x3( &mark_info->transform, mtext );
+
+   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, &g_render.cam );
+   shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
+
+   float scale = 0.2f, thickness = 0.015f, scale1 = 0.08f;
+   m3x3_zero( mlocal );
+   m3x3_setdiagonalv3( mlocal, (v3f){ scale, scale, thickness } );
+   mlocal[3][0] = -font3d_string_width( 0, buftext );
+   mlocal[3][0] *= scale*0.5f;
+   mlocal[3][1] = 0.1f;
+   mlocal[3][2] = 0.0f;
+   m4x3_mul( mtext, mlocal, mtextmdl );
+   font3d_simple_draw( 0, buftext, &g_render.cam, mtextmdl );
+
+   m3x3_setdiagonalv3( mlocal, (v3f){ scale1, scale1, thickness } );
+   mlocal[3][0] = -font3d_string_width( 0, bufsubtext );
+   mlocal[3][0] *= scale1*0.5f;
+   mlocal[3][1] = -scale1*0.3f;
+   m4x3_mul( mtext, mlocal, mtextmdl );
+   font3d_simple_draw( 0, bufsubtext, &g_render.cam, mtextmdl );
+}
+
+/*
+ * World: render event
+ */
+void skateshop_render( ent_skateshop *shop )
+{
+   if( shop->type == k_skateshop_type_boardshop )
+      skateshop_render_boardshop( shop );
+   else if( shop->type == k_skateshop_type_charshop )
+      skateshop_render_charshop( shop );
+   else if( shop->type == k_skateshop_type_worldshop )
+      skateshop_render_worldshop( shop );
+   else if( shop->type == k_skateshop_type_server ){
+   }
+   else
+      vg_fatal_error( "Unknown store (%u)\n", shop->type );
+}
+
+void skateshop_render_nonfocused( world_instance *world, vg_camera *cam )
+{
+   for( u32 j=0; j<mdl_arrcount( &world->ent_skateshop ); j ++ )
+   {
+      ent_skateshop *shop = mdl_arritm(&world->ent_skateshop, j );
+
+      if( shop->type != k_skateshop_type_boardshop ) continue;
+
+      f32 dist2 = v3_dist2( cam->pos, shop->transform.co ),
+          maxdist = 50.0f;
+
+      if( dist2 > maxdist*maxdist ) continue;
+      ent_marker *mark_rack = mdl_arritm( &world->ent_marker, 
+                                     mdl_entity_id_id(shop->boards.id_rack));
+
+      if( !mark_rack ) 
+         continue;
+
+      u32 slot_count = VG_ARRAY_LEN(global_skateshop.shop_view_slots);
+      for( u32 i=0; i<slot_count; i++ )
+      {
+         struct player_board *board = &localplayer.fallback_board;
+
+         mdl_transform xform;
+         transform_identity( &xform );
+
+         xform.co[0] = -((float)i - ((float)slot_count)*0.5f)*0.45f;
+         mdl_transform_mul( &mark_rack->transform, &xform, &xform );
+
+         struct player_board_pose pose = {0};
+         m4x3f mmdl;
+         mdl_transform_m4x3( &xform, mmdl );
+         render_board( cam, world, board, mmdl, &pose, k_board_shader_entity );
+      }
+   }
+}
+
+static void ent_skateshop_helpers_pickable( const char *acceptance )
+{
+   vg_str text;
+
+   if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
+      vg_strcat( &text, "Exit" );
+
+   if( (global_skateshop.helper_pick = gui_new_helper(
+               input_button_list[k_srbind_maccept], &text))){
+      vg_strcat( &text, acceptance );
+   }
+
+   if( (global_skateshop.helper_browse = gui_new_helper( 
+               input_axis_list[k_sraxis_mbrowse_h], &text ))){
+      vg_strcat( &text, "Browse" );
+   }
+}
+
+static void board_scan_thread( void *_args )
+{
+   addon_mount_content_folder( k_addon_type_board, "boards", ".mdl" );
+   addon_mount_workshop_items();
+   vg_async_call( async_addon_reg_update, NULL, 0 );
+   vg_async_stall();
+
+   /* 04.03.24
+    * REVIEW: This is removed as it *should* be done on the preupdate of the 
+    *         addon system.
+    *
+    *         Verify that it works the same.
+    */
+#if 0
+   board_processview_thread(NULL);
+#endif
+}
+
+static void world_scan_thread( void *_args )
+{
+   addon_mount_content_folder( k_addon_type_world, "maps", ".mdl" );
+   addon_mount_workshop_items();
+   vg_async_call( async_addon_reg_update, NULL, 0 );
+}
+
+/*
+ * Entity logic: entrance event
+ */
+entity_call_result ent_skateshop_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, index );
+   vg_info( "skateshop_call\n" );
+
+   if( (skaterift.activity != k_skaterift_default) || 
+       !vg_loader_availible() ) 
+      return k_entity_call_result_invalid;
+
+   if( call->function == k_ent_function_trigger )
+   {
+      if( localplayer.subsystem != k_player_subsystem_walk )
+         return k_entity_call_result_OK;
+      
+      vg_info( "Entering skateshop\n" );
+
+      world_entity_set_focus( call->id );
+      world_entity_focus_modal();
+      gui_helper_clear();
+      
+      if( shop->type == k_skateshop_type_boardshop )
+      {
+         skateshop_update_viewpage();
+         vg_loader_start( board_scan_thread, NULL );
+         ent_skateshop_helpers_pickable( "Pick" );
+      }
+      else if( shop->type == k_skateshop_type_charshop )
+      {
+         ent_skateshop_helpers_pickable( "Pick" );
+      }
+      else if( shop->type == k_skateshop_type_worldshop )
+      {
+         ent_skateshop_helpers_pickable( "Open rift" );
+         vg_loader_start( world_scan_thread, NULL );
+      }
+      else if( shop->type == k_skateshop_type_server )
+      {
+         vg_str text;
+         global_skateshop.helper_pick = gui_new_helper(
+                     input_button_list[k_srbind_maccept], &text);
+         if( gui_new_helper( input_button_list[k_srbind_mback], &text ))
+            vg_strcat( &text, "exit" );
+         skateshop_server_helper_update();
+      }
+      return k_entity_call_result_OK;
+   }
+   else
+      return k_entity_call_result_unhandled;
+}
diff --git a/src/ent_skateshop.h b/src/ent_skateshop.h
new file mode 100644 (file)
index 0000000..2f8e3a6
--- /dev/null
@@ -0,0 +1,54 @@
+#pragma once
+#include "world.h"
+#include "world_load.h"
+#include "player.h"
+#include "vg/vg_steam_remote_storage.h"
+#include "workshop.h"
+#include "addon.h"
+
+#define SKATESHOP_VIEW_SLOT_MAX    5
+
+struct global_skateshop
+{
+   v3f look_target;
+
+   struct shop_view_slot{
+      u16 cache_id;
+      float view_blend;
+   }
+   shop_view_slots[ SKATESHOP_VIEW_SLOT_MAX ];
+
+   u32 selected_world_id,
+       selected_board_id,
+       selected_player_id,
+       pointcloud_world_id;
+
+   struct {
+      const char *item_title, *item_desc;
+      u32 reg_id;
+
+      const char *world_title, *world_loc;
+      u32 world_reg;
+   }
+   render;
+
+   union {
+      struct gui_helper *helper_pick, *helper_toggle;
+   };
+
+   struct gui_helper *helper_browse;
+
+
+   addon_reg *reg_preview, *reg_loaded_preview;
+   GLuint tex_preview;
+}
+extern global_skateshop;
+
+void skateshop_init(void);
+void ent_skateshop_preupdate( ent_focus_context *ctx );
+void skateshop_render( ent_skateshop *shop );
+void skateshop_render_nonfocused( world_instance *world, vg_camera *cam );
+void skateshop_autostart_loading(void);
+void skateshop_world_preupdate( world_instance *world );
+entity_call_result ent_skateshop_call( world_instance *world, ent_call *call );
+void skateshop_world_preview_preupdate(void);
diff --git a/src/ent_tornado.c b/src/ent_tornado.c
new file mode 100644 (file)
index 0000000..f9fca03
--- /dev/null
@@ -0,0 +1,87 @@
+#include "world.h"
+#include "particle.h"
+
+static f32 k_tornado_strength = 0.0f,
+           k_tornado_ratio    = 0.5f,
+           k_tornado_range    = 10.f;
+
+void ent_tornado_init(void)
+{
+   vg_console_reg_var( "k_tonado_strength", &k_tornado_strength,
+                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
+   vg_console_reg_var( "k_tonado_ratio", &k_tornado_ratio,
+                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
+   vg_console_reg_var( "k_tonado_range", &k_tornado_range,
+                        k_var_dtype_f32, VG_VAR_PERSISTENT|VG_VAR_CHEAT );
+}
+
+void ent_tornado_debug(void) 
+{
+   world_instance *world = world_current_instance();
+   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
+      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
+
+      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
+         v3f p1;
+         v3_add( marker->transform.co, (v3f){0,20,0}, p1 );
+         vg_line( marker->transform.co, p1, VG__RED );
+
+         m4x3f mmdl;
+         m4x3_identity( mmdl );
+         v3_copy( marker->transform.co, mmdl[3] );
+         vg_line_sphere( mmdl, k_tornado_range, 0 );
+      }
+   }
+}
+
+void ent_tornado_forces( v3f co, v3f cv, v3f out_a )
+{
+   world_instance *world = world_current_instance();
+   v3_zero( out_a );
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
+      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
+
+      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
+         v3f d, dir;
+         v3_sub( co, marker->transform.co, d );
+         d[1] = 0.0f;
+
+         f32 dist = v3_length( d );
+         v3_normalize( d );
+
+         v3_cross( d, (v3f){0,1,0}, dir );
+         if( v3_dot( dir, cv ) < 0.0f )
+            v3_negate( dir, dir );
+
+         f32  s = vg_maxf(0.0f, 1.0f-dist/k_tornado_range),
+              F0 = s*k_tornado_strength,
+              F1 = s*s*k_tornado_strength;
+
+         v3_muladds( out_a, dir, F0 * k_tornado_ratio, out_a );
+         v3_muladds( out_a, d,   F1 * -(1.0f-k_tornado_ratio), out_a );
+      }
+   }
+}
+
+void ent_tornado_pre_update(void)
+{
+   world_instance *world = world_current_instance();
+   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i ++ ){
+      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
+
+      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tornado" ) ){
+         v3f co;
+         vg_rand_sphere( &vg.rand, co );
+
+         v3f tangent = { co[2], 0, co[0] };
+
+         f32 s = vg_signf( co[1] );
+         v3_muls( tangent, s*10.0f, tangent );
+         co[1] *= s;
+
+         v3_muladds( marker->transform.co, co, k_tornado_range, co );
+         particle_spawn( &particles_env, co, tangent, 2.0f, 0xffffffff );
+      }
+   }
+}
diff --git a/src/ent_tornado.h b/src/ent_tornado.h
new file mode 100644 (file)
index 0000000..f89c070
--- /dev/null
@@ -0,0 +1,6 @@
+#pragma once
+
+void ent_tornado_init(void);
+void ent_tornado_debug(void);
+void ent_tornado_forces( v3f co, v3f cv, v3f out_a );
+void ent_tornado_pre_update(void);
diff --git a/src/ent_traffic.c b/src/ent_traffic.c
new file mode 100644 (file)
index 0000000..8bb19b9
--- /dev/null
@@ -0,0 +1,63 @@
+#include "world.h"
+
+void ent_traffic_update( world_instance *world, v3f pos )
+{
+   for( u32 i=0; i<mdl_arrcount( &world->ent_traffic ); i++ ){
+      ent_traffic *traffic = mdl_arritm( &world->ent_traffic, i );
+      
+      u32 i1 = traffic->index,
+          i0,
+          i2 = i1+1;
+
+      if( i1 == 0 ) i0 = traffic->node_count-1;
+      else i0 = i1-1;
+
+      if( i2 >= traffic->node_count ) i2 = 0;
+
+      i0 += traffic->start_node;
+      i1 += traffic->start_node;
+      i2 += traffic->start_node;
+      
+      v3f h[3];
+
+      ent_route_node *rn0 = mdl_arritm( &world->ent_route_node, i0 ),
+                     *rn1 = mdl_arritm( &world->ent_route_node, i1 ),
+                     *rn2 = mdl_arritm( &world->ent_route_node, i2 );
+
+      v3_copy( rn1->co, h[1] );
+      v3_lerp( rn0->co, rn1->co, 0.5f, h[0] );
+      v3_lerp( rn1->co, rn2->co, 0.5f, h[2] );
+
+      float const k_sample_dist = 0.0025f;
+      v3f pc, pd;
+      eval_bezier3( h[0], h[1], h[2], traffic->t, pc );
+      eval_bezier3( h[0], h[1], h[2], traffic->t+k_sample_dist, pd );
+
+      v3f v0;
+      v3_sub( pd, pc, v0 );
+      float length = vg_maxf( 0.0001f, v3_length( v0 ) );
+      v3_muls( v0, 1.0f/length, v0 );
+
+      float mod = k_sample_dist / length;
+
+      traffic->t += traffic->speed * vg.time_delta * mod;
+
+      if( traffic->t > 1.0f ){
+         traffic->t -= 1.0f;
+
+         if( traffic->t > 1.0f ) traffic->t = 0.0f;
+
+         traffic->index ++;
+
+         if( traffic->index >= traffic->node_count ) 
+            traffic->index = 0;
+      }
+
+      v3_copy( pc, traffic->transform.co );
+
+      float a = atan2f( -v0[0], v0[2] );
+      q_axis_angle( traffic->transform.q, (v3f){0.0f,1.0f,0.0f}, -a );
+
+      vg_line_point( traffic->transform.co, 0.3f, VG__BLUE );
+   }
+}
diff --git a/src/ent_traffic.h b/src/ent_traffic.h
new file mode 100644 (file)
index 0000000..18d8d1e
--- /dev/null
@@ -0,0 +1,3 @@
+#pragma once
+#include "world.h"
+void ent_traffic_update( world_instance *world, v3f pos );
diff --git a/src/entity.c b/src/entity.c
new file mode 100644 (file)
index 0000000..797e29d
--- /dev/null
@@ -0,0 +1,77 @@
+#include "world.h"
+#include "entity.h"
+#include "world_entity.h"
+
+#include "ent_objective.h"
+#include "ent_skateshop.h"
+#include "ent_relay.h"
+#include "ent_challenge.h"
+#include "ent_route.h"
+#include "ent_miniworld.h"
+#include "ent_region.h"
+#include "ent_glider.h"
+#include "ent_npc.h"
+#include "world_water.h"
+
+#include <string.h>
+
+void entity_call( world_instance *world, ent_call *call )
+{
+   u32 type = mdl_entity_id_type( call->id ),
+       index = mdl_entity_id_id( call->id );
+
+   fn_entity_call_handler table[] = {
+      [k_ent_volume]    = ent_volume_call,
+      [k_ent_audio]     = ent_audio_call,
+      [k_ent_skateshop] = ent_skateshop_call,
+      [k_ent_objective] = ent_objective_call,
+      [k_ent_ccmd]      = ent_ccmd_call,
+      [k_ent_gate]      = ent_gate_call,
+      [k_ent_relay]     = ent_relay_call,
+      [k_ent_challenge] = ent_challenge_call,
+      [k_ent_route]     = ent_route_call,
+      [k_ent_miniworld] = ent_miniworld_call,
+      [k_ent_region]    = ent_region_call,
+      [k_ent_glider]    = ent_glider_call,
+      [k_ent_npc]       = ent_npc_call,
+      [k_ent_water]     = ent_water_call,
+   };
+
+   if( type >= VG_ARRAY_LEN(table) ){
+      vg_error( "call to entity type: %u is out of range\n", type );
+      return;
+   }
+
+   fn_entity_call_handler fn = table[ type ];
+
+   if( !fn )
+   {
+      vg_error( "Entity type %u does not have a call handler, "
+                "but was called anyway\n", type );
+      return;
+   }
+
+   enum entity_call_result res = fn( world, call );
+
+   if( res == k_entity_call_result_unhandled )
+   {
+      vg_warn( "Call to entity %u#%u was unhandled.\n", type, index );
+   }
+}
+
+ent_marker *ent_find_marker( mdl_context *mdl, mdl_array_ptr *arr, 
+                             const char *alias )
+{
+   for( u32 i=0; i<mdl_arrcount(arr); i++ )
+   {
+      ent_marker *marker = mdl_arritm( arr, i );
+
+      if( !strcmp( mdl_pstr( mdl, marker->pstr_alias ), alias ) )
+      {
+         return marker;
+      }
+   }
+
+   return NULL;
+}
+
diff --git a/src/entity.h b/src/entity.h
new file mode 100644 (file)
index 0000000..6d503f9
--- /dev/null
@@ -0,0 +1,582 @@
+#pragma once
+
+#include "vg/vg_audio.h"
+#include "vg/vg_ui/imgui.h"
+#include "model.h"
+
+typedef struct ent_spawn ent_spawn;
+typedef struct ent_light ent_light;
+typedef struct ent_gate ent_gate;
+typedef struct ent_route_node ent_route_node;
+typedef struct ent_path_index ent_path_index;
+typedef struct ent_checkpoint ent_checkpoint;
+typedef struct ent_route ent_route;
+typedef struct ent_water ent_water;
+typedef struct ent_audio_clip ent_audio_clip;
+typedef struct volume_particles volume_particles;
+typedef struct volume_trigger volume_trigger;
+typedef struct ent_volume ent_volume;
+typedef struct ent_audio ent_audio;
+typedef struct ent_marker ent_marker;
+typedef struct ent_traffic ent_traffic;
+typedef struct ent_font ent_font;
+typedef struct ent_font_variant ent_font_variant;
+typedef struct ent_glyph ent_glyph;
+typedef struct ent_skateshop ent_skateshop;
+typedef struct ent_camera ent_camera;
+typedef struct ent_swspreview ent_swspreview;
+typedef struct ent_worldinfo ent_worldinfo;
+typedef struct ent_ccmd ent_ccmd;
+typedef struct ent_objective ent_objective;
+typedef struct ent_challenge ent_challenge;
+typedef struct ent_relay ent_relay;
+typedef struct ent_cubemap ent_cubemap;
+typedef struct ent_miniworld ent_miniworld;
+typedef struct ent_prop ent_prop;
+typedef struct ent_region ent_region;
+typedef struct ent_list ent_list;
+typedef struct ent_glider ent_glider;
+typedef struct ent_npc ent_npc;
+
+enum entity_alias{
+   k_ent_none        = 0,
+   k_ent_gate        = 1,
+   k_ent_spawn       = 2,
+   k_ent_route_node  = 3,
+   k_ent_route       = 4,
+   k_ent_water       = 5,
+   k_ent_volume      = 6,
+   k_ent_audio       = 7,
+   k_ent_marker      = 8,
+   k_ent_font        = 9,
+   k_ent_font_variant= 10,
+   k_ent_traffic     = 11,
+   k_ent_skateshop   = 12,
+   k_ent_camera      = 13,
+   k_ent_swspreview  = 14,
+   k_ent_menuitem    = 15,
+   k_ent_worldinfo   = 16,
+   k_ent_ccmd        = 17,
+   k_ent_objective   = 18,
+   k_ent_challenge   = 19,
+   k_ent_relay       = 20,
+   k_ent_cubemap     = 21,
+   k_ent_miniworld   = 22,
+   k_ent_prop        = 23,
+   k_ent_list        = 24,
+   k_ent_region      = 25,
+   k_ent_glider      = 26,
+   k_ent_npc         = 27
+};
+
+typedef struct ent_call ent_call;
+typedef enum entity_call_result entity_call_result;
+enum entity_call_result 
+{
+   k_entity_call_result_OK,
+   k_entity_call_result_unhandled,
+   k_entity_call_result_invalid
+};
+
+static inline u32 mdl_entity_id_type( u32 entity_id )
+{
+   return (entity_id & 0x0fff0000) >> 16;
+}
+
+static inline u32 mdl_entity_id_id( u32 entity_id )
+{
+   return entity_id & 0x0000ffff;
+}
+
+static inline u32 mdl_entity_id( u32 type, u32 index )
+{
+   return (type & 0xfffff)<<16 | (index & 0xfffff);
+}
+
+enum entity_function
+{
+   k_ent_function_trigger,
+   k_ent_function_particle_spawn,
+   k_ent_function_trigger_leave
+};
+
+struct ent_spawn{
+   mdl_transform transform;
+   u32 pstr_name;
+};
+
+enum light_type{
+   k_light_type_point = 0,
+   k_light_type_spot = 1
+};
+
+struct ent_light{
+   mdl_transform transform;
+   u32 daytime,
+       type;
+
+   v4f colour;
+   float angle,
+         range;
+
+   m4x3f inverse_world;
+   v2f angle_sin_cos;
+};
+
+/* v101 */
+#if 0
+enum gate_type{
+   k_gate_type_unlinked = 0,
+   k_gate_type_teleport = 1,
+   k_gate_type_nonlocal_unlinked = 2,
+   k_gate_type_nonlocel = 3
+};
+#endif
+
+/* v102+ */
+enum ent_gate_flag{
+   k_ent_gate_linked      = 0x1, /* this is a working portal */
+   k_ent_gate_nonlocal    = 0x2, /* use the key string to link this portal.
+                                       NOTE: if set, it adds the flip flag. */
+   k_ent_gate_flip        = 0x4, /* flip direction 180* for exiting portal */
+   k_ent_gate_custom_mesh = 0x8, /* use a custom submesh instead of default */
+   k_ent_gate_locked      = 0x10,/* has to be unlocked to be useful */
+
+   k_ent_gate_clean_pass  = 0x20,/* player didn't rewind while getting here */
+};
+
+struct ent_gate{
+   u32 flags,
+       target, 
+       key;
+
+   v3f dimensions,
+       co[2];
+
+   v4f q[2];
+
+   /* runtime */
+   m4x3f to_world, transport;
+
+   union{
+      u32 timing_version;
+
+      struct{
+         u8 ref_count;
+      };
+   };
+
+   double timing_time;
+   u16 routes[4];       /* routes that pass through this gate */
+   u8 route_count;
+
+   /* v102+ */
+   u32 submesh_start, submesh_count;
+};
+
+struct ent_route_node{
+   v3f co;
+   u8 ref_count, ref_total;
+};
+
+struct ent_path_index{
+   u16 index;
+};
+
+struct ent_checkpoint{
+   u16 gate_index,
+       path_start,
+       path_count;
+
+   /* EXTENSION */
+   f32 best_time;
+};
+
+enum ent_route_flag {
+   k_ent_route_flag_achieve_silver = 0x1,
+   k_ent_route_flag_achieve_gold   = 0x2,
+
+   k_ent_route_flag_out_of_zone    = 0x10,
+   k_ent_region_flag_hasname       = 0x20
+};
+
+struct ent_route{
+   union{
+      mdl_transform transform;
+      u32 official_track_id;   /* TODO: remove this */
+   }
+   anon;
+
+   u32 pstr_name;
+   u16 checkpoints_start,
+       checkpoints_count;
+
+   v4f colour;
+
+   /* runtime */
+   u16 active_checkpoint, 
+       valid_checkpoints;
+
+   f32 factive;
+   m4x3f board_transform;
+   mdl_submesh sm;
+   f64 timing_base;
+
+   u32 id_camera; /* v103+ */
+
+   /* v104+, but always accessible */
+   u32 flags;
+   f64 best_laptime;
+   f32 ui_stopper, ui_residual;
+
+   ui_px ui_first_block_width, ui_residual_block_w;
+};
+
+struct ent_water{
+   mdl_transform transform;
+   float max_dist;
+   u32 reserved0, reserved1;
+};
+
+struct ent_audio_clip{
+   union{
+      mdl_file file;
+      audio_clip clip;
+   }_;
+
+   float probability;
+};
+
+struct volume_particles{
+   u32 blank, blank2;
+};
+
+struct volume_trigger{
+   i32 event, event_leave;
+};
+
+enum ent_volume_flag {
+   k_ent_volume_flag_particles = 0x1,
+   k_ent_volume_flag_disabled  = 0x2
+};
+
+struct ent_volume{
+   mdl_transform transform;
+   m4x3f to_world, to_local;
+   u32 flags;
+
+   u32 target;
+   union{
+      volume_trigger trigger;
+      volume_particles particles;
+   };
+};
+
+struct ent_audio{
+   mdl_transform transform;
+   u32 flags,
+       clip_start,
+       clip_count;
+   float volume, crossfade;
+   u32 behaviour,
+       group,
+       probability_curve,
+       max_channels;
+};
+
+struct ent_marker{
+   mdl_transform transform;
+   u32 pstr_alias;
+};
+
+enum skateshop_type{
+   k_skateshop_type_boardshop = 0,
+   k_skateshop_type_charshop = 1,
+   k_skateshop_type_worldshop = 2,
+   k_skateshop_type_DELETED = 3,
+   k_skateshop_type_server = 4
+};
+
+struct ent_skateshop{
+   mdl_transform transform;
+   u32 type, id_camera;
+
+   union{
+      struct{
+         u32 id_display,
+             id_info,
+             id_rack;
+      }
+      boards;
+
+      struct{
+         u32 id_display,
+             id_info;
+      }
+      character;
+
+      struct{
+         u32 id_display,
+             id_info;
+      }
+      worlds;
+
+      struct{
+         u32 id_lever;
+      }
+      server;
+   };
+};
+
+struct ent_swspreview{
+   u32 id_camera, id_display, id_display1;
+};
+
+struct ent_traffic{
+   mdl_transform transform;
+   u32 submesh_start,
+       submesh_count,
+       start_node,
+       node_count;
+   float speed,
+         t;
+   u32 index;     /* into the path */
+};
+
+struct ent_camera{
+   mdl_transform transform;
+   float fov;
+};
+
+enum ent_menuitem_type{
+   k_ent_menuitem_type_visual       = 0,
+   k_ent_menuitem_type_event_button = 1,
+   k_ent_menuitem_type_page_button  = 2,
+   k_ent_menuitem_type_toggle       = 3,
+   k_ent_menuitem_type_slider       = 4,
+   k_ent_menuitem_type_page         = 5,
+   k_ent_menuitem_type_binding      = 6,
+   k_ent_menuitem_type_visual_nocol = 7,
+   k_ent_menuitem_type_disabled     = 90
+};
+
+enum ent_menuitem_stack_behaviour{
+   k_ent_menuitem_stack_append  = 0,
+   k_ent_menuitem_stack_replace = 1
+};
+
+typedef struct ent_menuitem ent_menuitem;
+struct ent_menuitem{
+   u32 type, groups, 
+       id_links[4];  /* ent_menuitem */
+   f32 factive, fvisible;
+
+   mdl_transform transform;
+   u32 submesh_start, submesh_count;
+
+   union{ u64 _u64;  /* force storage for 64bit pointers */
+          i32 *pi32;
+          f32 *pf32;
+          void *pvoid;
+   };
+
+   union{
+      struct{
+         u32 pstr_name;
+      }
+      visual;
+
+      struct{
+         u32 id_min,    /* ent_marker */
+             id_max,    /* . */
+             id_handle, /* ent_menuitem */
+             pstr_data;
+      }
+      slider;
+
+      struct{
+         u32 pstr,
+             stack_behaviour;
+      }
+      button;
+
+      struct{
+         u32 id_check, /* ent_menuitem */
+             pstr_data;
+         v3f offset; /* relative to parent */
+      }
+      checkmark;
+
+      struct{
+         u32 pstr_name, 
+             id_entrypoint,  /* ent_menuitem */
+             id_viewpoint;   /* ent_camera */
+      }
+      page;
+
+      struct{
+         u32 pstr_bind,
+             font_variant;
+      }
+      binding;
+   };
+};
+
+struct ent_worldinfo{
+   u32 pstr_name, pstr_author, pstr_desc;
+   f32 timezone;
+   u32 pstr_skybox;
+   u32 flags;
+};
+
+ent_marker *ent_find_marker( mdl_context *mdl, mdl_array_ptr *arr, 
+                             const char *alias );
+
+enum channel_behaviour{
+   k_channel_behaviour_unlimited = 0,
+   k_channel_behaviour_discard_if_full = 1,
+   k_channel_behaviour_crossfade_if_full = 2
+};
+
+enum probability_curve{
+   k_probability_curve_constant = 0,
+   k_probability_curve_wildlife_day = 1,
+   k_probability_curve_wildlife_night = 2
+};
+
+struct ent_font{
+   u32 alias,
+       variant_start,
+       variant_count,
+       glyph_start,
+       glyph_count,
+       glyph_utf32_base;
+};
+
+struct ent_font_variant{
+   u32 name,
+       material_id;
+};
+
+struct ent_glyph{
+   v2f size;
+   u32 indice_start,
+       indice_count;
+};
+
+struct ent_ccmd{
+   u32 pstr_command;
+};
+
+enum ent_objective_filter{
+   k_ent_objective_filter_none            = 0x00000000,
+   k_ent_objective_filter_trick_shuvit    = 0x00000001,
+   k_ent_objective_filter_trick_kickflip  = 0x00000002,
+   k_ent_objective_filter_trick_treflip   = 0x00000004,
+   k_ent_objective_filter_trick_any       = 
+      k_ent_objective_filter_trick_shuvit|
+      k_ent_objective_filter_trick_treflip|
+      k_ent_objective_filter_trick_kickflip,
+   k_ent_objective_filter_flip_back       = 0x00000008,
+   k_ent_objective_filter_flip_front      = 0x00000010,
+   k_ent_objective_filter_flip_any        =
+      k_ent_objective_filter_flip_back|
+      k_ent_objective_filter_flip_front,
+   k_ent_objective_filter_grind_truck_any = 0x00000020,
+   k_ent_objective_filter_grind_board_any = 0x00000040,
+   k_ent_objective_filter_grind_any       =
+      k_ent_objective_filter_grind_truck_any|
+      k_ent_objective_filter_grind_board_any,
+   k_ent_objective_filter_footplant       = 0x00000080,
+   k_ent_objective_filter_passthrough     = 0x00000100
+};
+
+enum ent_objective_flag {
+   k_ent_objective_hidden = 0x1,
+   k_ent_objective_passed = 0x2
+};
+
+struct ent_objective{
+   mdl_transform transform;
+   u32 submesh_start,
+       submesh_count,
+       flags,
+       id_next,
+       filter,filter2,
+       id_win;
+   i32 win_event;
+   f32 time_limit;
+};
+
+enum ent_challenge_flag {
+   k_ent_challenge_timelimit = 0x1
+};
+
+struct ent_challenge{
+   mdl_transform transform;
+   u32 pstr_alias,
+       flags,
+       target;
+   i32 target_event;
+   u32 reset;
+   i32 reset_event;
+   u32 first,
+       camera,
+       status;
+};
+
+struct ent_relay {
+   u32 targets[4][2];
+   i32 targets_events[4];
+};
+
+struct ent_cubemap {
+   v3f co;
+   u32 resolution, live, texture_id, 
+       framebuffer_id, renderbuffer_id, placeholder[2];
+};
+
+struct ent_miniworld {
+   mdl_transform transform;
+   u32 pstr_world;
+   u32 camera;
+   u32 proxy;
+};
+
+struct ent_prop {
+   mdl_transform transform;
+   u32 submesh_start, submesh_count, flags, pstr_alias;
+};
+
+struct ent_region {
+   mdl_transform transform;
+   u32 submesh_start, submesh_count, pstr_title, flags, zone_volume,
+
+       /* 105+ */
+       target0[2];
+};
+
+struct ent_glider {
+   mdl_transform transform;
+   u32 flags;
+   f32 cooldown;
+};
+
+struct ent_npc 
+{
+   mdl_transform transform;
+   u32 id, context, camera;
+};
+
+#include "world.h"
+
+struct ent_call{
+   u32 id;
+   i32 function;
+   void *data;
+};
+
+typedef enum entity_call_result 
+   (*fn_entity_call_handler)( world_instance *, ent_call *);
+
+void entity_call( world_instance *world, ent_call *call );
diff --git a/src/font.h b/src/font.h
new file mode 100644 (file)
index 0000000..fa144c4
--- /dev/null
@@ -0,0 +1,324 @@
+#pragma once
+#include "model.h"
+#include "entity.h"
+#include "vg/vg_camera.h"
+#include "shaders/model_font.h"
+#include "shaders/scene_font.h"
+#include "world_render.h"
+#include "depth_compare.h"
+#include "vg/vg_tex.h"
+#include <string.h>
+
+enum efont_SRglyph{
+   k_SRglyph_end           = 0x00, /* control characters */
+   k_SRglyph_ctrl_variant  = 0x01,
+   k_SRglyph_ctrl_size     = 0x02, /* normalized 0-1 */
+   k_SRglyph_ctrl_center   = 0x03, /* useful when text is scaled down */
+   k_SRglyph_ctrl_baseline = 0x04, /* . */
+   k_SRglyph_ctrl_top      = 0x05, /* . */
+   k_SRglyph_mod_circle    = 0x1e, /* surround and center next charater */
+   k_SRglyph_mod_square    = 0x1f, /* surround and center next character */
+   k_SRglyph_ascii_min     = 0x20, /* standard ascii */
+   k_SRglyph_ascii_max     = 0x7e,
+   k_SRglyph_ps4_square    = 0x7f,/* playstation buttons */
+   k_SRglyph_ps4_triangle  = 0x80,
+   k_SRglyph_ps4_circle    = 0x81,
+   k_SRglyph_ps4_cross     = 0x82,
+   k_SRglyph_xb1_x         = 0x83,/* xbox buttons */
+   k_SRglyph_xb1_y         = 0x84,
+   k_SRglyph_xb1_a         = 0x85,
+   k_SRglyph_xb1_b         = 0x86,
+   k_SRglyph_gen_ls        = 0x87,/* generic gamepad */
+   k_SRglyph_gen_lsh       = 0x88,
+   k_SRglyph_gen_lsv       = 0x89,
+   k_SRglyph_gen_lshv      = 0x8a,
+   k_SRglyph_gen_rs        = 0x8b,
+   k_SRglyph_gen_rsh       = 0x8c,
+   k_SRglyph_gen_rsv       = 0x8d,
+   k_SRglyph_gen_rshv      = 0x8e,
+   k_SRglyph_gen_lt        = 0x8f,
+   k_SRglyph_gen_rt        = 0x90,
+   k_SRglyph_gen_lb        = 0x91,
+   k_SRglyph_gen_rb        = 0x92,
+   k_SRglyph_gen_left      = 0x93,
+   k_SRglyph_gen_up        = 0x94,
+   k_SRglyph_gen_right     = 0x95,
+   k_SRglyph_gen_down      = 0x96,
+   k_SRglyph_gen_options   = 0x97,
+   k_SRglyph_gen_shareview = 0x98,
+   k_SRglyph_kbm_m0        = 0x99,/* mouse */
+   k_SRglyph_kbm_m1        = 0x9a,
+   k_SRglyph_kbm_m01       = 0x9b,
+   k_SRglyph_kbm_m2        = 0x9c,
+   k_SRglyph_kbm_m2s       = 0x9d,
+   k_SRglyph_kbm_shift     = 0x9e,/* modifiers */
+   k_SRglyph_kbm_ctrl      = 0x9f,
+   k_SRglyph_kbm_alt       = 0xa0,
+   k_SRglyph_kbm_space     = 0xa1,
+   k_SRglyph_kbm_return    = 0xa2,
+   k_SRglyph_kbm_escape    = 0xa3,
+   k_SRglyph_kbm_mousemove = 0xa4,
+
+#if 0
+   k_SRglyph_vg_ret        = 0xa5,
+   k_SRglyph_vg_link       = 0xa6,
+   k_SRglyph_vg_square     = 0xa7,
+   k_SRglyph_vg_triangle   = 0xa8,
+   k_SRglyph_vg_circle     = 0xa9
+#endif
+};
+
+typedef struct font3d font3d;
+struct font3d{
+   mdl_context mdl;
+   GLuint texture;
+   glmesh mesh;
+
+   ent_font info;
+   mdl_array_ptr font_variants,
+                 glyphs;
+};
+
+static void font3d_load( font3d *font, const char *mdl_path, void *alloc ){
+   mdl_open( &font->mdl, mdl_path, alloc );
+   mdl_load_metadata_block( &font->mdl, alloc );
+
+   vg_linear_clear( vg_mem.scratch );
+   mdl_array_ptr fonts;
+   MDL_LOAD_ARRAY( &font->mdl, &fonts, ent_font, vg_mem.scratch );
+   font->info = *((ent_font *)mdl_arritm(&fonts,0));
+
+   MDL_LOAD_ARRAY( &font->mdl, &font->font_variants, ent_font_variant, alloc);
+   MDL_LOAD_ARRAY( &font->mdl, &font->glyphs, ent_glyph, alloc );
+
+   vg_linear_clear( vg_mem.scratch );
+
+   if( !mdl_arrcount( &font->mdl.textures ) )
+      vg_fatal_error( "No texture in font file" );
+
+   mdl_texture *tex0 = mdl_arritm( &font->mdl.textures, 0 );
+   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
+   mdl_fread_pack_file( &font->mdl, &tex0->file, data );
+
+   mdl_async_load_glmesh( &font->mdl, &font->mesh, NULL );
+   vg_tex2d_load_qoi_async( data, tex0->file.pack_size, 
+                            VG_TEX2D_LINEAR|VG_TEX2D_CLAMP,
+                            &font->texture );
+
+   mdl_close( &font->mdl );
+}
+
+static u32 font3d_find_variant( font3d *font, const char *name ){
+   for( u32 i=0; i<mdl_arrcount( &font->font_variants ); i ++ ){
+      ent_font_variant *variant = mdl_arritm( &font->font_variants, i );
+
+      if( !strcmp( mdl_pstr( &font->mdl, variant->name ), name ) ){
+         return i;
+      }
+   }
+
+   return 0;
+}
+
+struct _font3d_render{
+   v4f offset;
+   font3d *font;
+   u32 variant_id;
+
+   enum font_shader {
+      k_font_shader_default,
+      k_font_shader_world
+   }
+   shader;
+}
+static gui_font3d;
+
+/*
+ * world can be null if not using world shader 
+ */
+static void font3d_bind( font3d *font, enum font_shader shader, 
+                         int depth_compare, world_instance *world,
+                         vg_camera *cam ){
+   gui_font3d.shader = shader;
+   gui_font3d.font = font;
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, font->texture );
+
+   if( shader == k_font_shader_default )
+   {
+      shader_model_font_use();
+      shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
+      shader_model_font_uTexMain( 1 );
+      shader_model_font_uDepthMode( depth_compare );
+
+      if( depth_compare ){
+         depth_compare_bind( 
+            shader_model_font_uTexSceneDepth,
+            shader_model_font_uInverseRatioDepth,
+            shader_model_font_uInverseRatioMain, cam );
+      }
+
+      shader_model_font_uPv( cam->mtx.pv );
+   }
+   else if( shader == k_font_shader_world )
+   {
+      shader_scene_font_use();
+      shader_scene_font_uTexGarbage(0);
+      shader_scene_font_uTexMain(1);
+
+      shader_scene_font_uPv( g_render.cam.mtx.pv );
+      shader_scene_font_uTime( vg.time );
+
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_font );
+
+      bind_terrain_noise();
+      shader_scene_font_uCamera( g_render.cam.transform[3] );
+   }
+   mesh_bind( &font->mesh );
+}
+
+static ent_glyph *font3d_glyph( font3d *font, u32 variant_id, u32 utf32 ){
+   if( utf32 < font->info.glyph_utf32_base ) return NULL;
+   if( utf32 >= font->info.glyph_utf32_base+font->info.glyph_count) return NULL;
+
+   u32 index = utf32 - font->info.glyph_utf32_base;
+       index += font->info.glyph_start;
+       index += font->info.glyph_count * variant_id;
+   return mdl_arritm( &font->glyphs, index );
+}
+
+static void font3d_set_transform( const char *text,
+                                  vg_camera *cam, m4x3f transform ){
+   v4_copy( (v4f){0.0f,0.0f,0.0f,1.0f}, gui_font3d.offset );
+
+   m4x4f prev_mtx;
+   m4x3_expand( transform, prev_mtx );
+   m4x4_mul( cam->mtx_prev.pv, prev_mtx, prev_mtx );
+
+   if( gui_font3d.shader == k_font_shader_default ){
+      shader_model_font_uPvmPrev( prev_mtx );
+      shader_model_font_uMdl( transform );
+   }
+   else if( gui_font3d.shader == k_font_shader_world ){
+      shader_scene_font_uPvmPrev( prev_mtx );
+      shader_scene_font_uMdl( transform );
+   }
+}
+
+static void font3d_setoffset( v4f offset ){
+   if( gui_font3d.shader == k_font_shader_default )
+      shader_model_font_uOffset( offset );
+   else if( gui_font3d.shader == k_font_shader_world )
+      shader_scene_font_uOffset( offset );
+}
+
+static void font3d_setcolour( v4f colour ){
+   if( gui_font3d.shader == k_font_shader_default )
+      shader_model_font_uColour( colour );
+#if 0
+   else if( gui_font3d.shader == k_font_shader_world )
+      shader_scene_font_uColour( colour );
+#endif
+}
+
+static void font3d_draw( const char *text ){
+   u8 *u8pch = (u8*)text;
+
+   u32 max_chars = 512;
+   while( u8pch && max_chars ){
+      max_chars --;
+
+      u32 c0 = *u8pch, c1;
+      u8pch ++;
+
+      if( !c0 ) break;
+
+      ent_glyph *glyph0 = font3d_glyph( gui_font3d.font, 
+                                        gui_font3d.variant_id, c0 ),
+                *glyph1 = NULL;
+
+      /* multibyte characters */
+      if( c0 >= 1 && c0 < k_SRglyph_ascii_min ){
+         c1 = *u8pch;
+         if( !c1 ) break;
+         glyph1 = font3d_glyph( gui_font3d.font, gui_font3d.variant_id, c1 );
+      }
+
+      if( c0 == k_SRglyph_ctrl_variant ){
+         gui_font3d.variant_id = c1;
+         u8pch ++;
+         continue;
+      }
+      else if( c0 == k_SRglyph_ctrl_size ){
+         gui_font3d.offset[3] = (float)c1 * (1.0f/255.0f);
+         u8pch ++;
+         continue;
+      }
+      else if( c0 == k_SRglyph_ctrl_baseline ){
+         gui_font3d.offset[1] = 0.0f;
+         continue;
+      }
+      else if( c0 == k_SRglyph_ctrl_center ){
+         if( glyph1 ){
+            float diff = glyph1->size[1] - glyph1->size[1]*gui_font3d.offset[3];
+            gui_font3d.offset[1] = diff * 0.5f;
+         }
+         continue;
+      }
+      else if( c0 == k_SRglyph_ctrl_top ){
+         if( glyph1 ){
+            float diff = glyph1->size[1] - glyph1->size[1]*gui_font3d.offset[3];
+            gui_font3d.offset[1] = diff;
+         }
+         continue;
+      }
+
+      if( !glyph0 ) continue;
+
+      if( glyph1 && (c0 == k_SRglyph_mod_square || c0 == k_SRglyph_mod_circle)){
+         v4f v0;
+         v2_sub( glyph0->size, glyph1->size, v0 );
+         v2_muladds( gui_font3d.offset, v0, -0.5f, v0 );
+         v0[2] = gui_font3d.offset[2];
+         v0[3] = gui_font3d.offset[3];
+
+         font3d_setoffset( v0 );
+         mesh_drawn( glyph0->indice_start, glyph0->indice_count );
+         continue;
+      }
+      else{
+         font3d_setoffset( gui_font3d.offset );
+         mesh_drawn( glyph0->indice_start, glyph0->indice_count );
+      }
+
+      gui_font3d.offset[0] += glyph0->size[0]*gui_font3d.offset[3];
+   }
+}
+
+static f32 font3d_simple_draw( u32 variant_id, const char *text, 
+                               vg_camera *cam, m4x3f transform ){
+   if( !text ) return 0.0f;
+
+   gui_font3d.variant_id = variant_id;
+   font3d_set_transform( text, cam, transform );
+   font3d_draw( text );
+   return gui_font3d.offset[0];
+}
+
+static f32 font3d_string_width( u32 variant_id, const char *text ){
+   if( !text ) return 0.0f;
+   float width = 0.0f;
+
+   const u8 *buf = (const u8 *)text;
+   for( int i=0;; i++ ){
+      u32 c = buf[i];
+      if(!c) break;
+
+      ent_glyph *glyph = font3d_glyph( gui_font3d.font, variant_id, c );
+      if( !glyph ) continue;
+
+      width += glyph->size[0];
+   }
+
+   return width;
+}
diff --git a/src/freecam.c b/src/freecam.c
new file mode 100644 (file)
index 0000000..d961ed6
--- /dev/null
@@ -0,0 +1,44 @@
+#include "skaterift.h"
+#include "player.h"
+#include "player_render.h"
+#include "player_replay.h"
+#include "input.h"
+
+void freecam_preupdate(void)
+{
+   vg_camera *cam = &player_replay.replay_freecam;
+   v3f angles;
+   v3_copy( cam->angles, angles );
+   player_look( angles, 1.0f );
+
+   f32 decay = vg_maxf(0.0f,1.0f-vg.time_frame_delta*10.0f);
+
+   v3f d;
+   v3_sub( angles, cam->angles, d );
+   v3_muladds( player_replay.freecam_w, d, 20.0f, player_replay.freecam_w );
+   v3_muls( player_replay.freecam_w, decay, player_replay.freecam_w );
+   v3_muladds( cam->angles, player_replay.freecam_w, vg.time_frame_delta,
+               cam->angles );
+   cam->angles[1] = vg_clampf( cam->angles[1], -VG_PIf*0.5f,VG_PIf*0.5f);
+
+   vg_camera_update_transform( cam );
+
+   v3f lookdir = { 0.0f, 0.0f, -1.0f },
+       sidedir = { 1.0f, 0.0f,  0.0f };
+   
+   m3x3_mulv( cam->transform, lookdir, lookdir );
+   m3x3_mulv( cam->transform, sidedir, sidedir );
+
+   v2f input;
+   joystick_state( k_srjoystick_steer, input );
+   v2_muls( input, vg.time_frame_delta*6.0f*20.0f, input );
+   
+   v3_muladds( player_replay.freecam_v, lookdir, -input[1], 
+               player_replay.freecam_v );
+   v3_muladds( player_replay.freecam_v, sidedir, input[0], 
+               player_replay.freecam_v );
+
+   v3_muls( player_replay.freecam_v, decay, player_replay.freecam_v );
+   v3_muladds( cam->pos,
+               player_replay.freecam_v, vg.time_frame_delta, cam->pos );
+}
diff --git a/src/freecam.h b/src/freecam.h
new file mode 100644 (file)
index 0000000..1f9f5e2
--- /dev/null
@@ -0,0 +1,3 @@
+#pragma once
+void freecam_preupdate(void);
+int freecam_cmd( int argc, const char *argv[] );
diff --git a/src/gameserver.c b/src/gameserver.c
new file mode 100644 (file)
index 0000000..e315fd0
--- /dev/null
@@ -0,0 +1,1136 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#define _DEFAULT_SOURCE
+#include <signal.h>
+#include <unistd.h>
+#include <time.h>
+#include <string.h>
+
+volatile sig_atomic_t sig_stop;
+
+#include "gameserver.h" 
+#include "vg/vg_opt.h"
+#include "network_common.h"
+#include "gameserver_db.h"
+#include "vg/vg_m.h"
+#include "vg/vg_msg.h"
+
+static u64 const k_steamid_max = 0xffffffffffffffff;
+
+static void inthandler( int signum ) {
+   sig_stop = 1;
+}
+
+static void release_message( SteamNetworkingMessage_t *msg )
+{
+   msg->m_nUserData --;
+
+   if( msg->m_nUserData == 0 )
+      SteamAPI_SteamNetworkingMessage_t_Release( msg );
+}
+
+/*
+ * Send message to single client, with authentication checking
+ */
+static void gameserver_send_to_client( i32 client_id,
+                                       const void *pData, u32 cbData,
+                                       int nSendFlags )
+{
+   struct gameserver_client *client = &gameserver.clients[ client_id ];
+
+   if( gameserver.loopback_test && !client->connection ) 
+      return;
+
+   if( !client->steamid )
+      return;
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, client->connection,
+         pData, cbData, nSendFlags, NULL );
+}
+
+/*
+ * Send message to all clients if they are authenticated
+ */
+static void gameserver_send_to_all( int ignore, 
+                                    const void *pData, u32 cbData, 
+                                    int nSendFlags )
+{
+   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
+   {
+      struct gameserver_client *client = &gameserver.clients[i];
+
+      if( i != ignore )
+         gameserver_send_to_client( i, pData, cbData, nSendFlags );
+   }
+}
+
+static void gameserver_send_version_to_client( int index )
+{
+   struct gameserver_client *client = &gameserver.clients[index];
+
+   if( gameserver.loopback_test && !client->connection ) 
+      return;
+
+   netmsg_version version;
+   version.inetmsg_id = k_inetmsg_version;
+   version.version = NETWORK_SKATERIFT_VERSION;
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, client->connection,
+         &version, sizeof(netmsg_version), 
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+/*
+ * handle server update that client #'index' has joined
+ */
+static void gameserver_player_join( int index )
+{
+   struct gameserver_client *joiner = &gameserver.clients[index];
+   
+   netmsg_playerjoin join = { .inetmsg_id = k_inetmsg_playerjoin,
+                              .index = index,
+                              .steamid = joiner->steamid };
+
+   gameserver_send_to_all( index, &join, sizeof(join),
+                           k_nSteamNetworkingSend_Reliable );
+
+   /* 
+    * update the joining user about current connections and our version
+    */
+   gameserver_send_version_to_client( index );
+
+   netmsg_playerusername *username = 
+      alloca( sizeof(netmsg_playerusername) + NETWORK_USERNAME_MAX );
+   username->inetmsg_id = k_inetmsg_playerusername;
+
+   netmsg_playeritem *item = 
+      alloca( sizeof(netmsg_playeritem) + ADDON_UID_MAX );
+   item->inetmsg_id = k_inetmsg_playeritem;
+
+   netmsg_region *region = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
+   region->inetmsg_id = k_inetmsg_region;
+
+   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
+   {
+      struct gameserver_client *client = &gameserver.clients[i];
+
+      if( (i == index) || !client->steamid )
+         continue;
+
+      /* join */
+      netmsg_playerjoin init = { .inetmsg_id = k_inetmsg_playerjoin,
+                                 .index = i,
+                                 .steamid = client->steamid };
+      gameserver_send_to_client( index, &init, sizeof(init),
+                                 k_nSteamNetworkingSend_Reliable );
+
+      /* username */
+      username->index = i;
+      u32 chs = vg_strncpy( client->username, username->name, 
+                            NETWORK_USERNAME_MAX,
+                            k_strncpy_always_add_null );
+      u32 size = sizeof(netmsg_playerusername) + chs + 1;
+      gameserver_send_to_client( index, username, size,
+                                 k_nSteamNetworkingSend_Reliable );
+
+      /* items */
+      for( int j=0; j<k_netmsg_playeritem_max; j++ )
+      {
+         chs = vg_strncpy( client->items[j].uid, item->uid, ADDON_UID_MAX, 
+                           k_strncpy_always_add_null );
+         item->type_index = j;
+         item->client = i;
+         size = sizeof(netmsg_playeritem) + chs + 1;
+         gameserver_send_to_client( index, item, size,
+                                    k_nSteamNetworkingSend_Reliable );
+      }
+
+      /* region */
+      
+      region->client = i;
+      region->flags = client->region_flags;
+      u32 l = vg_strncpy( client->region, region->loc, NETWORK_REGION_MAX, 
+                          k_strncpy_always_add_null );
+      size = sizeof(netmsg_region) + l + 1;
+
+      gameserver_send_to_client( index, region, size,
+                                 k_nSteamNetworkingSend_Reliable );
+   }
+}
+
+/*
+ * Handle server update that player has left
+ */
+static void gameserver_player_leave( int index ){
+   if( gameserver.auth_mode == eServerModeAuthentication ){
+      if( !gameserver.clients[ index ].steamid )
+         return;
+   }
+
+   netmsg_playerleave leave;
+   leave.inetmsg_id = k_inetmsg_playerleave;
+   leave.index = index;
+
+   vg_info( "Player leave (%d)\n", index );
+   gameserver_send_to_all( index, &leave, sizeof(leave),
+                           k_nSteamNetworkingSend_Reliable );
+}
+
+static void gameserver_update_all_knowledge( int client, int clear );
+
+/*
+ * Deletes client at index and disconnects the connection handle if it was 
+ * set.
+ */
+static void remove_client( int index ){
+   struct gameserver_client *client = &gameserver.clients[index];
+   if( client->connection ){
+      SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
+            hSteamNetworkingSockets, client->connection, -1 );
+      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+            hSteamNetworkingSockets, client->connection,
+            k_ESteamNetConnectionEnd_Misc_InternalError,
+            NULL, 1 );
+   }
+   memset( client, 0, sizeof(struct gameserver_client) );
+   gameserver_update_all_knowledge( index, 1 );
+}
+
+/*
+ * Handle incoming new connection and init flags on the steam handle. if the 
+ * server is full the userdata (client_id) will be set to -1 on the handle.
+ */
+static void handle_new_connection( HSteamNetConnection conn )
+{
+   SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
+         hSteamNetworkingSockets, conn, -1 );
+
+   int index = -1;
+
+   for( int i=0; i<vg_list_size(gameserver.clients); i++ ){
+      if( !gameserver.clients[i].active ){
+         index = i;
+         break;
+      }
+   }
+
+   if( index == -1 ){
+      vg_error( "Server full\n" );
+      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+            hSteamNetworkingSockets, conn, 
+            4500,
+            NULL, 1 );
+      return;
+   }
+
+   struct gameserver_client *client = &gameserver.clients[index];
+   EResult accept_status = SteamAPI_ISteamNetworkingSockets_AcceptConnection(
+            hSteamNetworkingSockets, conn );
+
+   if( accept_status == k_EResultOK )
+   {
+      vg_success( "Accepted client (id: %u, index: %d)\n", conn, index );
+
+      client->active = 1;
+      client->connection = conn;
+
+      SteamAPI_ISteamNetworkingSockets_SetConnectionPollGroup(
+            hSteamNetworkingSockets, conn, gameserver.client_group );
+      
+      SteamAPI_ISteamNetworkingSockets_SetConnectionUserData(
+            hSteamNetworkingSockets, conn, index );
+
+      if( gameserver.loopback_test )
+      {
+         vg_warn( "[DEV] Creating loopback client\n" );
+         struct gameserver_client *loopback = &gameserver.clients[1];
+         loopback->active = 1;
+         loopback->connection = 0;
+      }
+   }
+   else
+   {
+      vg_warn( "Error accepting connection (id: %u)\n", conn );
+      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+            hSteamNetworkingSockets, conn, 
+            k_ESteamNetConnectionEnd_Misc_InternalError,
+            NULL, 1 );
+   }
+}
+
+static void on_auth_status( CallbackMsg_t *msg ){
+   SteamNetAuthenticationStatus_t *info = (void *)msg->m_pubParam;
+   vg_info( "  Authentication availibility: %s\n", 
+         string_ESteamNetworkingAvailability(info->m_eAvail) );
+   vg_info( "  %s\n", info->m_debugMsg );
+}
+
+/*
+ * Get client id of connection handle. Will be -1 if unkown to us either because
+ * the server is full or we already disconnected them
+ */
+static i32 gameserver_conid( HSteamNetConnection hconn )
+{
+   i64 id;
+
+   if( hconn == 0 )
+   {
+      if( gameserver.loopback_test )
+         return 1;
+      else
+         return -1;
+   }
+   else
+      id = SteamAPI_ISteamNetworkingSockets_GetConnectionUserData(
+               hSteamNetworkingSockets, hconn );
+
+   if( (id < 0) || (id >= NETWORK_MAX_PLAYERS) )
+      return -1;
+
+   return id;
+}
+
+/*
+ * Callback for steam connection state change
+ */
+static void on_connect_status( CallbackMsg_t *msg )
+{
+   SteamNetConnectionStatusChangedCallback_t *info = (void *)msg->m_pubParam;
+   vg_info( "  Connection status changed for %lu\n", info->m_hConn );
+
+   vg_info( "  %s -> %s\n", 
+         string_ESteamNetworkingConnectionState(info->m_eOldState),
+         string_ESteamNetworkingConnectionState(info->m_info.m_eState) );
+
+   if( info->m_info.m_eState==k_ESteamNetworkingConnectionState_Connecting )
+   {
+      handle_new_connection( info->m_hConn );
+   }
+
+   if( (info->m_info.m_eState == 
+            k_ESteamNetworkingConnectionState_ClosedByPeer ) ||
+       (info->m_info.m_eState == 
+        k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) ||
+       (info->m_info.m_eState == 
+        k_ESteamNetworkingConnectionState_Dead) ||
+       (info->m_info.m_eState ==
+        k_ESteamNetworkingConnectionState_None) )
+   {
+      vg_info( "End reason: %d\n", info->m_info.m_eEndReason );
+
+      int client_id = gameserver_conid( info->m_hConn );
+      if( client_id != -1 )
+      {
+         gameserver_player_leave( client_id );
+         remove_client( client_id );
+
+         if( gameserver.loopback_test )
+         {
+            gameserver_player_leave( 1 );
+            remove_client( 1 );
+         }
+      }
+      else 
+      {
+         SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+               hSteamNetworkingSockets, info->m_hConn, 0, NULL, 0 );
+      }
+   }
+}
+
+static void gameserver_rx_version( SteamNetworkingMessage_t *msg )
+{
+   netmsg_version *version = msg->m_pData;
+
+   int client_id = gameserver_conid( msg->m_conn );
+   if( client_id == -1 ) 
+   {
+      vg_warn( "Recieved version from unkown connection (%u)\n", msg->m_conn );
+      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+            hSteamNetworkingSockets, msg->m_conn,
+            k_ESteamNetConnectionEnd_Misc_InternalError,
+            NULL, 1 );
+      return;
+   }
+
+   struct gameserver_client *client = &gameserver.clients[ client_id ];
+
+   if( client->version )
+   {
+      vg_warn( "Already have version for this client (%d conn: %u)", 
+               client_id, msg->m_conn );
+      return;
+   }
+
+   client->version = version->version;
+
+   if( client->version != NETWORK_SKATERIFT_VERSION )
+   {
+      gameserver_send_version_to_client( client_id );
+      remove_client( client_id );
+      return;
+   }
+
+   /* this is the sign on point for non-auth servers,
+    * for auth servers it comes at the end of rx_auth 
+    */
+   if( gameserver.auth_mode != eServerModeAuthentication )
+   {
+      client->steamid = k_steamid_max;
+      gameserver_player_join( client_id );
+
+      if( gameserver.loopback_test )
+      {
+         struct gameserver_client *loopback = &gameserver.clients[1];
+         loopback->steamid = k_steamid_max;
+         gameserver_player_join( 1 );
+      }
+   }
+}
+
+/* 
+ * recieve auth ticket from connection. will only accept it if we've added them
+ * to the client list first.
+ */
+static void gameserver_rx_auth( SteamNetworkingMessage_t *msg ){
+   if( gameserver.auth_mode != eServerModeAuthentication ){
+      vg_warn( "Running server without authentication. "
+               "Connection %u tried to authenticate.\n", msg->m_conn );
+      return;
+   }
+
+   int client_id = gameserver_conid( msg->m_conn );
+   if( client_id == -1 ) {
+      vg_warn( "Recieved auth ticket from unkown connection (%u)\n", 
+               msg->m_conn );
+      SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+            hSteamNetworkingSockets, msg->m_conn,
+            k_ESteamNetConnectionEnd_Misc_InternalError, NULL, 1 );
+      return;
+   }
+
+   struct gameserver_client *client = &gameserver.clients[ client_id ];
+   if( client->steamid ){
+      vg_warn( "Already authorized this user but another app ticket was sent"
+               " again (%d conn: %u)\n", client_id, msg->m_conn );
+      return;
+   }
+
+   if( client->version == 0 ){
+      vg_error( "Client has not sent their version yet (%u)\n", msg->m_conn );
+      remove_client( client_id );
+      return;
+   }
+
+   vg_low( "Attempting to verify user\n" );
+
+   if( msg->m_cbSize < sizeof(netmsg_auth) ){
+      vg_error( "Malformed auth ticket, too small (%u)\n", msg->m_conn );
+      remove_client( client_id );
+      return;
+   }
+
+   netmsg_auth *auth = msg->m_pData;
+
+   if( msg->m_cbSize < sizeof(netmsg_auth)+auth->ticket_length ||
+       auth->ticket_length > 1024 ){
+      vg_error( "Malformed auth ticket, ticket_length incorrect (%u)\n",
+                  auth->ticket_length );
+      remove_client( client_id );
+      return;
+   }
+
+   u8 decrypted[1024];
+   u32 ticket_len = 1024;
+
+   int success = SteamEncryptedAppTicket_BDecryptTicket(
+         auth->ticket, auth->ticket_length, decrypted,
+         &ticket_len, gameserver.app_symmetric_key,
+         k_nSteamEncryptedAppTicketSymmetricKeyLen );
+
+   if( !success ){
+      vg_error( "Failed to decrypt users ticket (client %u)\n", msg->m_conn );
+      vg_error( "  ticket length: %u\n", auth->ticket_length );
+      remove_client( client_id );
+      return;
+   }
+
+   if( SteamEncryptedAppTicket_GetTicketIssueTime( decrypted, ticket_len )){
+      RTime32 ctime = time(NULL),
+              tickettime = SteamEncryptedAppTicket_GetTicketIssueTime(
+                    decrypted, ticket_len ),
+              expiretime = tickettime + 24*3*60*60;
+      
+      if( ctime > expiretime ){
+         vg_error( "Ticket expired (client %u)\n", msg->m_conn );
+         remove_client( client_id );
+         return;
+      }
+   }
+
+   CSteamID steamid;
+   SteamEncryptedAppTicket_GetTicketSteamID( decrypted, ticket_len, &steamid );
+   vg_success( "User is authenticated! steamid %lu (%u)\n", 
+         steamid.m_unAll64Bits, msg->m_conn );
+
+   client->steamid = steamid.m_unAll64Bits;
+   gameserver_player_join( client_id );
+}
+
+/*
+ * Player updates sent to us
+ * -----------------------------------------------------------------------------
+ */
+
+static int packet_minsize( SteamNetworkingMessage_t *msg, u32 size ){
+   if( msg->m_cbSize < size ) {
+      vg_error( "Invalid packet size (must be at least %u)\n", size );
+      return 0;
+   }
+   else{
+      return 1;
+   }
+}
+
+struct db_set_username_thread_data {
+   u64 steamid;
+   char username[ NETWORK_USERNAME_MAX ];
+};
+
+static void gameserver_update_db_username( db_request *db_req ){
+   struct db_set_username_thread_data *inf = (void *)db_req->data;
+
+   if( inf->steamid == k_steamid_max )
+      return;
+
+   int admin = 0;
+   if( inf->steamid == 76561198072130043 )
+      admin = 2;
+
+   db_updateuser( inf->steamid, inf->username, admin );
+}
+
+static int gameserver_item_eq( struct gameserver_item *ia, 
+                               struct gameserver_item *ib ){
+   if( ia->hash == ib->hash )
+      if( !strcmp(ia->uid,ib->uid) )
+         return 1;
+
+   return 0;
+}
+
+/*
+ * Match addons between two player IDs. if clear is set, then the flags between
+ * those two IDs will all be set to 0.
+ */
+static void gameserver_update_knowledge_table( int client0, int client1, 
+                                               int clear ){
+   u32 idx = network_pair_index( client0, client1 );
+
+   struct gameserver_client *c0 = &gameserver.clients[client0],
+                            *c1 = &gameserver.clients[client1];
+
+   u8 flags = 0x00;
+
+   if( !clear ){
+      if( gameserver_item_eq(&c0->items[k_netmsg_playeritem_world0],
+                             &c1->items[k_netmsg_playeritem_world0]))
+         flags |= CLIENT_KNOWLEDGE_SAME_WORLD0;
+
+      if( gameserver_item_eq(&c0->items[k_netmsg_playeritem_world1],
+                             &c1->items[k_netmsg_playeritem_world1]))
+         flags |= CLIENT_KNOWLEDGE_SAME_WORLD1;
+   }
+
+   gameserver.client_knowledge_mask[idx] = flags;
+}
+
+/*
+ * If a change has been made on this client, then it will adjust the entire
+ * table of other players. if clear is set, all references to client will be set
+ * to 0.
+ */
+static void gameserver_update_all_knowledge( int client, int clear ){
+   for( int i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
+      if( i == client )
+         continue;
+
+      struct gameserver_client *ci = &gameserver.clients[i];
+
+      if( ci->steamid )
+         gameserver_update_knowledge_table( client, i, clear );
+   }
+}
+
+static void gameserver_propogate_player_frame( int client_id, 
+                                               netmsg_playerframe *frame, 
+                                               u32 size ){
+   u32 basic_size = sizeof(netmsg_playerframe) + ((24*3)/8);
+   netmsg_playerframe *full = alloca(size),
+                      *basic= alloca(basic_size);
+
+   memcpy( full, frame, size );
+   memcpy( basic, frame, basic_size );
+
+   full->client = client_id;
+   basic->client = client_id;
+   basic->subsystem = 4; /* (.._basic_info: 24f*3 animator ) */
+   basic->sound_effects = 0;
+
+   struct gameserver_client *c0 = &gameserver.clients[client_id];
+   c0->instance = frame->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
+
+   for( int i=0; i<vg_list_size(gameserver.clients); i++ )
+   {
+      if( i == client_id )
+         continue;
+
+      struct gameserver_client *ci = &gameserver.clients[i];
+
+      int send_full = 0;
+
+      if( c0->instance == ci->instance )
+      {
+         u32 k_index = network_pair_index( client_id, i );
+         u8 k_mask = gameserver.client_knowledge_mask[ k_index ];
+         
+         if( (k_mask & (CLIENT_KNOWLEDGE_SAME_WORLD0<<c0->instance)) )
+            send_full = 1;
+      }
+
+      if( send_full )
+      {
+         gameserver_send_to_client( i, full, size, 
+                                    k_nSteamNetworkingSend_Unreliable );
+      }
+      else 
+      {
+         gameserver_send_to_client( i, basic, basic_size, 
+                                    k_nSteamNetworkingSend_Unreliable );
+      }
+   }
+}
+
+static void gameserver_rx_200_300( SteamNetworkingMessage_t *msg )
+{
+   netmsg_blank *tmp = msg->m_pData;
+
+   int client_id = gameserver_conid( msg->m_conn );
+   if( client_id == -1 ) return;
+
+   struct gameserver_client *client = &gameserver.clients[ client_id ];
+
+   if( tmp->inetmsg_id == k_inetmsg_playerusername )
+   {
+      if( !packet_minsize( msg, sizeof(netmsg_playerusername)+1 ))
+         return;
+      
+      netmsg_playerusername *src = msg->m_pData;
+
+      u32 name_len = network_msgstring( src->name, msg->m_cbSize, 
+                                        sizeof(netmsg_playerusername),
+                                        client->username, 
+                                        NETWORK_USERNAME_MAX );
+
+      /* update other users about this change */
+      netmsg_playerusername *prop = alloca(sizeof(netmsg_playerusername)+
+                                             NETWORK_USERNAME_MAX );
+                                           
+      prop->inetmsg_id = k_inetmsg_playerusername;
+      prop->index = client_id;
+      u32 chs = vg_strncpy( client->username, prop->name, NETWORK_USERNAME_MAX,
+                            k_strncpy_always_add_null );
+
+      vg_info( "client #%d changed name to: %s\n", client_id, prop->name );
+
+      u32 propsize = sizeof(netmsg_playerusername) + chs + 1;
+      gameserver_send_to_all( client_id, prop, propsize,
+                              k_nSteamNetworkingSend_Reliable );
+
+      /* update database about this */
+      db_request *call = db_alloc_request( 
+                           sizeof(struct db_set_username_thread_data) );
+      struct db_set_username_thread_data *inf = (void *)call->data;
+      inf->steamid = client->steamid;
+      vg_strncpy( client->username, inf->username, 
+                  sizeof(inf->username), k_strncpy_always_add_null );
+      call->handler = gameserver_update_db_username;
+      db_send_request( call );
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playerframe )
+   {
+      gameserver_propogate_player_frame( client_id, 
+                                         msg->m_pData, msg->m_cbSize );
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playeritem )
+   {
+      netmsg_playeritem *item = msg->m_pData;
+
+      /* record */
+      if( item->type_index >= k_netmsg_playeritem_max )
+      {
+         vg_warn( "Client #%d invalid equip type %u\n", 
+                  client_id, (u32)item->type_index );
+         return;
+      }
+      
+      char *dest = client->items[ item->type_index ].uid;
+
+      network_msgstring( item->uid, msg->m_cbSize, sizeof(netmsg_playeritem),
+                         dest, ADDON_UID_MAX );
+
+      vg_info( "Client #%d equiped: [%s] %s\n", 
+               client_id, 
+               (const char *[]){[k_netmsg_playeritem_board]="board",
+                                [k_netmsg_playeritem_player]="player",
+                                [k_netmsg_playeritem_world0]="world0",
+                                [k_netmsg_playeritem_world1]="world1"
+               }[item->type_index], item->uid );
+
+      gameserver_update_all_knowledge( client_id, 0 );
+                           
+      /* propogate */
+      netmsg_playeritem *prop = alloca(msg->m_cbSize);
+      memcpy( prop, msg->m_pData, msg->m_cbSize );
+      prop->client = client_id;
+      gameserver_send_to_all( client_id, prop, msg->m_cbSize, 
+                              k_nSteamNetworkingSend_Reliable );
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_chat )
+   {
+      netmsg_chat *chat = msg->m_pData,
+                  *prop = alloca( sizeof(netmsg_chat) + NETWORK_MAX_CHAT );
+      prop->inetmsg_id = k_inetmsg_chat;
+      prop->client = client_id;
+
+      u32 l = network_msgstring( chat->msg, msg->m_cbSize, sizeof(netmsg_chat),
+                                 prop->msg, NETWORK_MAX_CHAT );
+      vg_info( "[%d]: %s\n", client_id, prop->msg );
+
+      gameserver_send_to_all( client_id, prop, sizeof(netmsg_chat)+l+1, 
+                              k_nSteamNetworkingSend_Reliable );
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_region )
+   {
+      netmsg_region *region = msg->m_pData,
+                    *prop = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
+
+      prop->inetmsg_id = k_inetmsg_region;
+      prop->client = client_id;
+      prop->flags = region->flags;
+
+      u32 l = network_msgstring( 
+            region->loc, msg->m_cbSize, sizeof(netmsg_region),
+            client->region, NETWORK_REGION_MAX );
+      client->region_flags = region->flags;
+
+      l = vg_strncpy( client->region, prop->loc, NETWORK_REGION_MAX, 
+                      k_strncpy_always_add_null );
+
+      gameserver_send_to_all( client_id, prop, sizeof(netmsg_region)+l+1, 
+                              k_nSteamNetworkingSend_Reliable );
+      vg_info( "client %d moved to region: %s\n", client_id, client->region );
+   }
+   else 
+   {
+      vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
+               tmp->inetmsg_id );
+   }
+}
+
+static void gameserver_request_respond( enum request_status status,
+                                        netmsg_request *res, vg_msg *body,
+                                        SteamNetworkingMessage_t *msg ){
+   int client_id = gameserver_conid( msg->m_conn );
+   u32 len = 0;
+   if( body ){
+      len = body->cur.co;
+      vg_low( "[%d#%d] Response: %d\n", client_id, (i32)res->id, status );
+      vg_msg_print( body, len );
+   }
+
+   res->status = status;
+
+   if( gameserver.loopback_test && !msg->m_conn )
+   {
+      release_message( msg );
+      return;
+   }
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, msg->m_conn,
+         res, sizeof(netmsg_request) + len,
+         k_nSteamNetworkingSend_Reliable, NULL );
+
+   release_message( msg );
+}
+
+struct user_request_thread_data {
+   SteamNetworkingMessage_t *msg;
+};
+
+static u32 gameserver_get_current_week(void){
+   return time(NULL) / (7*24*60*60);
+}
+
+static enum request_status gameserver_cat_table( 
+      vg_msg *msg, 
+      const char *mod, const char *route, u32 week, const char *alias )
+{
+   char table_name[ DB_TABLE_UID_MAX ];
+   if( !db_get_highscore_table_name( mod, route, week, table_name ) )
+      return k_request_status_out_of_memory;
+
+   char buf[512];
+   vg_str q;
+   vg_strnull( &q, buf, 512 );
+   vg_strcat( &q, "SELECT * FROM \"" );
+   vg_strcat( &q, table_name );
+   vg_strcat( &q, "\" ORDER BY time ASC LIMIT 10;" );
+   if( !vg_strgood(&q) )
+      return k_request_status_out_of_memory;
+
+   sqlite3_stmt *stmt = db_stmt( q.buffer );
+   if( !stmt )
+      return k_request_status_database_error;
+
+   vg_msg_frame( msg, alias );
+   for( u32 i=0; i<10; i ++ ){
+      int fc = sqlite3_step( stmt );
+
+      if( fc == SQLITE_ROW ){
+         i32 time = sqlite3_column_int( stmt, 1 );
+         i64 steamid_i64 = sqlite3_column_int64( stmt, 0 );
+         u64 steamid = *((u64 *)&steamid_i64);
+
+         if( steamid == k_steamid_max )
+            continue;
+
+         vg_msg_frame( msg, "" );
+         vg_msg_wkvnum( msg, "time", k_vg_msg_u32, 1, &time );
+         vg_msg_wkvnum( msg, "steamid", k_vg_msg_u64, 1, &steamid );
+
+         char username[32];
+         if( db_getuserinfo( steamid, username, sizeof(username), NULL ) )
+            vg_msg_wkvstr( msg, "username", username );
+         vg_msg_end_frame( msg );
+      }
+      else if( fc == SQLITE_DONE ){
+         break;
+      }
+      else {
+         log_sqlite3( fc );
+         break;
+      }
+   }
+
+   sqlite3_finalize( stmt );
+   vg_msg_end_frame( msg );
+   return k_request_status_ok;
+}
+
+static void gameserver_process_user_request( db_request *db_req )
+{
+   struct user_request_thread_data *inf = (void *)db_req->data;
+   SteamNetworkingMessage_t *msg = inf->msg;
+
+   int client_id = gameserver_conid( msg->m_conn );
+   if( client_id == -1 )
+   {
+      release_message( msg );
+      return;
+   }
+
+   struct gameserver_client *client = &gameserver.clients[ client_id ];
+
+   netmsg_request *req = (netmsg_request *)msg->m_pData;
+   vg_msg data;
+   vg_msg_init( &data, req->q, msg->m_cbSize - sizeof(netmsg_request) );
+
+   /* create response packet */
+   netmsg_request *res = alloca( sizeof(netmsg_request) + NETWORK_REQUEST_MAX );
+   res->inetmsg_id = k_inetmsg_response;
+   res->id = req->id;
+   vg_msg body;
+   vg_msg_init( &body, res->q, NETWORK_REQUEST_MAX );
+
+   const char *endpoint = vg_msg_getkvstr( &data, "endpoint" );
+
+   if( !endpoint ){
+      gameserver_request_respond( k_request_status_invalid_endpoint,
+                                  res, NULL, msg );
+      return;
+   }
+
+   if( !strcmp( endpoint, "scoreboard" ) ){
+      const char *mod = vg_msg_getkvstr( &data, "mod" );
+      const char *route = vg_msg_getkvstr( &data, "route" );
+      u32 week;
+      vg_msg_getkvintg( &data, "week", k_vg_msg_u32, &week, NULL );
+      
+      if( week == NETWORK_LEADERBOARD_CURRENT_WEEK ){
+         gameserver_cat_table( &body, mod, route, 
+                               gameserver_get_current_week(), "rows_weekly" );
+      }
+      else if( week == NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK ){
+         gameserver_cat_table( &body, mod, route, 0, "rows" );
+         gameserver_cat_table( &body, mod, route, 
+                               gameserver_get_current_week(), "rows_weekly" );
+      }
+      else 
+         gameserver_cat_table( &body, mod, route, week, "rows" );
+
+      if( body.error != k_vg_msg_error_OK ){
+         gameserver_request_respond( k_request_status_out_of_memory,
+                                     res, NULL, msg );
+         return;
+      }
+
+      gameserver_request_respond( k_request_status_ok, res, &body, msg );
+   }
+   else if( !strcmp( endpoint, "setlap" ) ){
+      if( client->steamid == k_steamid_max ){
+         gameserver_request_respond( k_request_status_unauthorized,
+                                     res, NULL, msg );
+         return;
+      }
+
+      const char *mod = vg_msg_getkvstr( &data, "mod" );
+      const char *route = vg_msg_getkvstr( &data, "route" );
+      
+      char weekly_table[ DB_TABLE_UID_MAX ],
+           alltime_table[ DB_TABLE_UID_MAX ];
+
+      u32 week = gameserver_get_current_week();
+
+      if( !db_get_highscore_table_name( mod, route, 0, alltime_table ) ||
+          !db_get_highscore_table_name( mod, route, week, weekly_table ) ){
+         gameserver_request_respond( k_request_status_out_of_memory,
+                                     res, NULL, msg );
+         return;
+      }
+
+      i32 centiseconds;
+      vg_msg_getkvintg( &data, "time", k_vg_msg_i32, &centiseconds, NULL );
+      if( centiseconds < 5*100 ){
+         gameserver_request_respond( k_request_status_client_error,
+                                     res, NULL, msg );
+         return;
+      }
+
+      db_writeusertime( alltime_table, client->steamid, centiseconds, 1 );
+      db_writeusertime( weekly_table, client->steamid, centiseconds, 1 );
+      gameserver_request_respond( k_request_status_ok, res, NULL, msg );
+   }
+   else{
+      gameserver_request_respond( k_request_status_invalid_endpoint,
+                                  res, NULL, msg );
+   }
+}
+
+static void gameserver_rx_300_400( SteamNetworkingMessage_t *msg )
+{
+   netmsg_blank *tmp = msg->m_pData;
+
+   int client_id = gameserver_conid( msg->m_conn );
+   if( client_id == -1 )
+   {
+      release_message( msg );
+      return;
+   }
+
+   if( tmp->inetmsg_id == k_inetmsg_request )
+   {
+      if( gameserver.loopback_test && (client_id == 1) )
+      {
+         release_message( msg );
+         return;
+      }
+
+      if( !packet_minsize( msg, sizeof(netmsg_request)+1 ))
+      {
+         release_message( msg );
+         return;
+      }
+
+      db_request *call = db_alloc_request( 
+                           sizeof(struct user_request_thread_data) );
+      struct user_request_thread_data *inf = (void *)call->data;
+      inf->msg = msg;
+      call->handler = gameserver_process_user_request;
+      db_send_request( call );
+   }
+   else 
+   {
+      vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
+               tmp->inetmsg_id );
+      release_message( msg );
+   }
+}
+
+static void process_network_message( SteamNetworkingMessage_t *msg )
+{
+   if( msg->m_cbSize < sizeof(netmsg_blank) ){
+      vg_warn( "Discarding message (too small: %d)\n", 
+            msg->m_cbSize );
+      return;
+   }
+
+   netmsg_blank *tmp = msg->m_pData;
+
+   if( (tmp->inetmsg_id >= 200) && (tmp->inetmsg_id < 300) )
+   {
+      gameserver_rx_200_300( msg );
+      release_message( msg );
+   }
+   else if( (tmp->inetmsg_id >= 300) && (tmp->inetmsg_id < 400) )
+   {
+      gameserver_rx_300_400( msg );
+   }
+   else{
+      if( tmp->inetmsg_id == k_inetmsg_auth )
+         gameserver_rx_auth( msg );
+      else if( tmp->inetmsg_id == k_inetmsg_version ){
+         gameserver_rx_version( msg );
+      }
+      else {
+         vg_warn( "Unknown inetmsg_id recieved from client. (%u)\n",
+                  tmp->inetmsg_id );
+      }
+      release_message( msg );
+   }
+}
+
+static void poll_connections(void)
+{
+   SteamNetworkingMessage_t *messages[32];
+   int len;
+
+   while(1)
+   {
+      len = SteamAPI_ISteamNetworkingSockets_ReceiveMessagesOnPollGroup(
+            hSteamNetworkingSockets,
+            gameserver.client_group, messages, vg_list_size(messages) );
+
+      if( len <= 0 )
+         return;
+
+      for( int i=0; i<len; i++ )
+      {
+         SteamNetworkingMessage_t *msg = messages[i];
+         msg->m_nUserData = 1;
+
+         if( gameserver.loopback_test )
+         {
+            HSteamNetConnection conid = msg->m_conn;
+            msg->m_conn = 0;
+            msg->m_nUserData ++;
+            process_network_message( msg );
+            msg->m_conn = conid;
+         }
+
+         process_network_message( msg );
+      }
+   }
+}
+
+static u64 seconds_to_server_ticks( double s ){
+   return s / 0.01;
+}
+
+int main( int argc, char *argv[] ){
+   signal( SIGINT, inthandler );
+   signal( SIGQUIT, inthandler );
+   signal( SIGPIPE, SIG_IGN );
+
+   char *arg;
+   while( vg_argp( argc, argv ) )
+   {
+      if( vg_long_opt( "noauth" ) )
+         gameserver.auth_mode = eServerModeNoAuthentication;
+
+      if( vg_long_opt( "loopback" ) )
+         gameserver.loopback_test = 1;
+   }
+   
+   vg_set_mem_quota( 80*1024*1024 );
+   vg_alloc_quota();
+   db_init();
+
+   /* steamworks init 
+    * --------------------------------------------------------------- */
+   steamworks_ensure_txt( "2103940" );
+   if( gameserver.auth_mode == eServerModeAuthentication ){
+      if( !vg_load_steam_symetric_key( "application_key", 
+                                       gameserver.app_symmetric_key )){
+         return 0;
+      }
+   }
+   else{
+      vg_warn( "Running without user authentication.\n" );
+   }
+
+   if( !SteamGameServer_Init( 0, NETWORK_PORT, NETWORK_PORT+1, 
+                              gameserver.auth_mode, "1.0.0.0" ) ){
+      vg_error( "SteamGameServer_Init failed\n" );
+      return 0;
+   }
+
+   void *hSteamGameServer = SteamAPI_SteamGameServer();
+   SteamAPI_ISteamGameServer_LogOnAnonymous( hSteamGameServer );
+
+   SteamAPI_ManualDispatch_Init();
+   HSteamPipe hsteampipe = SteamGameServer_GetHSteamPipe();
+   hSteamNetworkingSockets = 
+      SteamAPI_SteamGameServerNetworkingSockets_SteamAPI();
+
+   steam_register_callback( k_iSteamNetAuthenticationStatus, on_auth_status );
+   steam_register_callback( k_iSteamNetConnectionStatusChangedCallBack,
+                             on_connect_status );
+
+   vg_success( "Steamworks API running\n" );
+   steamworks_event_loop( hsteampipe );
+
+   /*
+    * Create a listener
+    */
+   HSteamListenSocket listener;
+   SteamNetworkingIPAddr localAddr;
+   SteamAPI_SteamNetworkingIPAddr_Clear( &localAddr );
+   localAddr.m_port = NETWORK_PORT;
+
+   listener = SteamAPI_ISteamNetworkingSockets_CreateListenSocketIP(
+                  hSteamNetworkingSockets, &localAddr, 0, NULL );
+   gameserver.client_group = SteamAPI_ISteamNetworkingSockets_CreatePollGroup(
+         hSteamNetworkingSockets );
+
+   u64 server_ticks = 8000,
+       last_record_save = 8000,
+       last_scoreboard_gen = 0;
+
+   while( !sig_stop ){
+      steamworks_event_loop( hsteampipe );
+      poll_connections();
+
+      usleep(10000);
+      server_ticks ++;
+
+      if( db_killed() )
+         break;
+   }
+   
+   SteamAPI_ISteamNetworkingSockets_DestroyPollGroup( hSteamNetworkingSockets,
+         gameserver.client_group );
+   SteamAPI_ISteamNetworkingSockets_CloseListenSocket( 
+         hSteamNetworkingSockets, listener );
+   
+   vg_info( "Shutting down\n..." );
+   SteamGameServer_Shutdown();
+   db_kill();
+   db_free();
+
+   return 0;
+}
diff --git a/src/gameserver.h b/src/gameserver.h
new file mode 100644 (file)
index 0000000..a03b1a0
--- /dev/null
@@ -0,0 +1,52 @@
+#pragma once
+#define VG_SERVER
+
+#include "vg/vg_platform.h"
+#include "vg/vg_steam.h"
+#include "vg/vg_steam_networking.h"
+#include "vg/vg_steam_http.h"
+#include "vg/vg_steam_auth.h"
+#include "network_msg.h"
+#include "network_common.h"
+#include <sys/socket.h>
+
+#define CLIENT_KNOWLEDGE_SAME_WORLD0 0x1
+#define CLIENT_KNOWLEDGE_SAME_WORLD1 0x2
+#define CLIENT_KNOWLEDGE_FRIENDS     0x4 /* unused */
+
+struct {
+   HSteamNetPollGroup client_group;
+   EServerMode auth_mode;
+
+   struct gameserver_client {
+      int active;
+      u32 version;
+      int authenticated;
+      HSteamNetConnection connection;
+      char username[ NETWORK_USERNAME_MAX ];
+
+      u8 instance;
+
+      struct gameserver_item {
+         char uid[ADDON_UID_MAX];
+         u32  hash;
+      }
+      items[k_netmsg_playeritem_max];
+
+      char region[ NETWORK_REGION_MAX ];
+      u32  region_flags;
+
+      u64  steamid;
+   }
+   clients[ NETWORK_MAX_PLAYERS ];
+
+   u8 client_knowledge_mask[ (NETWORK_MAX_PLAYERS*(NETWORK_MAX_PLAYERS-1))/2 ];
+   u8 app_symmetric_key[ k_nSteamEncryptedAppTicketSymmetricKeyLen ];
+
+   bool loopback_test;
+}
+static gameserver = {
+   .auth_mode = eServerModeAuthentication
+};
+
+static ISteamNetworkingSockets *hSteamNetworkingSockets = NULL;
diff --git a/src/gameserver_db.h b/src/gameserver_db.h
new file mode 100644 (file)
index 0000000..fe39931
--- /dev/null
@@ -0,0 +1,398 @@
+#ifndef GAMESERVER_DB_H
+#define GAMESERVER_DB_H
+
+#include "vg/vg_log.h"
+#include "vg/vg_mem_queue.h"
+#include "network_common.h"
+#include "dep/sqlite3/sqlite3.h"
+#include <pthread.h>
+#include <unistd.h>
+
+#define DB_COURSE_UID_MAX 32
+#define DB_TABLE_UID_MAX (ADDON_UID_MAX+DB_COURSE_UID_MAX+32)
+//#define DB_CRASH_ON_SQLITE_ERROR
+#define DB_LOG_SQL_STATEMENTS
+#define DB_REQUEST_BUFFER_SIZE (1024*2)
+
+typedef struct db_request db_request;
+struct db_request {
+   void (*handler)( db_request *req );
+   u32 size,_;
+   u8 data[];
+};
+
+struct {
+   sqlite3 *db;
+   pthread_t thread;
+   pthread_mutex_t mux;
+
+   vg_queue queue;
+   int kill;
+}
+static database;
+
+/*
+ * Log the error code (or carry on if its OK).
+ */
+static void log_sqlite3( int code ){
+   if( code == SQLITE_OK ) return;
+   vg_print_backtrace();
+   vg_error( "sqlite3(%d): %s\n", code, sqlite3_errstr(code) );
+
+#ifdef DB_CRASH_ON_SQLITE_ERROR 
+   int crash = *((int*)2);
+#endif
+}
+
+/*
+ * Perpare statement and auto throw away if fails. Returns NULL on failure.
+ */
+static sqlite3_stmt *db_stmt( const char *code ){
+#ifdef DB_LOG_SQL_STATEMENTS
+   vg_low( code );
+#endif
+
+   sqlite3_stmt *stmt;
+   int fc = sqlite3_prepare_v2( database.db, code, -1, &stmt, NULL );
+
+   if( fc != SQLITE_OK ){
+      log_sqlite3( fc );
+      sqlite3_finalize( stmt );
+      return NULL;
+   }
+
+   return stmt;
+}
+
+/*
+ * bind zero terminated string
+ */
+static int db_sqlite3_bind_sz( sqlite3_stmt *stmt, int pos, const char *sz ){
+   return sqlite3_bind_text( stmt, pos, sz, -1, SQLITE_STATIC );
+}
+
+/*
+ * Allowed characters in sqlite table names. We use "" as delimiters.
+ */
+static int db_verify_charset( const char *str, int mincount ){
+   for( int i=0; ; i++ ){
+      char c = str[i];
+      if( c == '\0' ){
+         if( i < mincount ) return 0;
+         else return 1;
+      }
+
+      if( !((c==' ')||(c=='!')||(c>='#'&&c<='~')) ) return 0;
+   }
+
+   return 0;
+}
+
+/*
+ * Find table name from mod UID and course UID, plus the week number
+ */
+static int db_get_highscore_table_name( const char *mod_uid,
+                                        const char *run_uid,
+                                        u32 week,
+                                        char table_name[DB_TABLE_UID_MAX] ){
+   if( !db_verify_charset( mod_uid, 13 ) ||
+       !db_verify_charset( run_uid, 1 ) ) return 0;
+
+   vg_str a;
+   vg_strnull( &a, table_name, DB_TABLE_UID_MAX );
+   vg_strcat( &a, mod_uid );
+   vg_strcat( &a, ":" );
+   vg_strcat( &a, run_uid );
+
+   if( week ){
+      vg_strcat( &a, "#" );
+      vg_strcati32( &a, week );
+   }
+
+   return vg_strgood( &a );
+}
+
+/*
+ * Read value from highscore table. If not found or error, returns 0
+ */
+static i32 db_readusertime( char table[DB_TABLE_UID_MAX], u64 steamid ){
+   char buf[ 512 ];
+   vg_str q;
+   vg_strnull( &q, buf, 512 );
+   vg_strcat( &q, "SELECT time FROM \"" );
+   vg_strcat( &q, table );
+   vg_strcat( &q, "\" WHERE steamid = ?;" );
+   if( !vg_strgood(&q) ) return 0;
+
+   sqlite3_stmt *stmt = db_stmt( q.buffer );
+   if( stmt ){
+      sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
+      int fc = sqlite3_step( stmt );
+
+      i32 result = 0;
+
+      if( fc == SQLITE_ROW )
+         result = sqlite3_column_int( stmt, 0 );
+      else if( fc != SQLITE_DONE )
+         log_sqlite3(fc);
+
+      sqlite3_finalize( stmt );
+      return result;
+   }
+   else return 0;
+}
+
+/*
+ * Write to highscore table
+ */
+static int db_writeusertime( char table[DB_TABLE_UID_MAX], u64 steamid, 
+                             i32 score, int only_if_faster ){
+   /* auto create table 
+    * ------------------------------------------*/
+   char buf[ 512 ];
+   vg_str q;
+   vg_strnull( &q, buf, 512 );
+   vg_strcat( &q, "CREATE TABLE IF NOT EXISTS \n \"" );
+   vg_strcat( &q, table );
+   vg_strcat( &q, "\"\n (steamid BIGINT UNIQUE, time INT);" );
+   if( !vg_strgood(&q) ) return 0;
+
+   vg_str str;
+   sqlite3_stmt *create_table = db_stmt( q.buffer );
+
+   if( create_table ){
+      db_sqlite3_bind_sz( create_table, 1, table );
+
+      int fc = sqlite3_step( create_table );
+      sqlite3_finalize( create_table );
+      if( fc != SQLITE_DONE )
+         return 0;
+   }
+   else return 0;
+
+   if( only_if_faster ){
+      i32 current = db_readusertime( table, steamid );
+      if( (current != 0) && (score > current) )
+         return 1;
+   }
+
+   /* insert score 
+    * -------------------------------------------------*/
+   vg_strnull( &q, buf, 512 );
+   vg_strcat( &q, "REPLACE INTO \"" );
+   vg_strcat( &q, table );
+   vg_strcat( &q, "\"(steamid,time)\n VALUES (?,?);" );
+   if( !vg_strgood(&q) ) return 0;
+
+   sqlite3_stmt *stmt = db_stmt( q.buffer );
+
+   if( stmt ){
+      sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
+      sqlite3_bind_int( stmt, 2, score );
+
+      int fc = sqlite3_step( stmt );
+      sqlite3_finalize( stmt );
+      if( fc != SQLITE_DONE )
+         return 0;
+      else 
+         return 1;
+   }
+   else return 0;
+}
+
+/*
+ * Set username and type
+ */
+static int db_updateuser( u64 steamid, const char *username, int admin ){
+   sqlite3_stmt *stmt = db_stmt(
+         "INSERT OR REPLACE INTO users (steamid, name, type) "
+         "VALUES (?,?,?);" );
+
+   if( stmt ){
+      sqlite3_bind_int64( stmt, 1, *((i64*)(&steamid)) ); 
+      db_sqlite3_bind_sz( stmt, 2, username );
+      sqlite3_bind_int( stmt, 3, admin );
+
+      int fc = sqlite3_step( stmt );
+      sqlite3_finalize(stmt);
+
+      if( fc == SQLITE_DONE ){
+         vg_success( "Inserted %lu (%s), type: %d\n",
+                     steamid, username, admin );
+         return 1;
+      }
+      else{
+         log_sqlite3( fc );
+         return 0;
+      }
+   }
+   else return 0;
+}
+
+/*
+ * Get user info 
+ */
+static int db_getuserinfo( u64 steamid, char *out_username, u32 username_max, 
+                           i32 *out_type ){
+   sqlite3_stmt *stmt = db_stmt( "SELECT * FROM users WHERE steamid = ?;" );
+   if( !stmt ) return 0;
+
+   sqlite3_bind_int64( stmt, 1, *((i64 *)&steamid) );
+   int fc = sqlite3_step( stmt );
+
+   if( fc != SQLITE_ROW ){
+      log_sqlite3( fc );
+      sqlite3_finalize( stmt );
+      return 0;
+   }
+
+   if( out_username ){
+      const char *name = (const char *)sqlite3_column_text( stmt, 1 );
+      vg_strncpy( name, out_username, username_max, k_strncpy_allow_cutoff );
+   }
+   
+   if( out_type )
+      *out_type = sqlite3_column_int( stmt, 2 );
+   
+   sqlite3_finalize( stmt );
+   return 1;
+}
+
+static void _db_thread_end(void){
+   pthread_mutex_lock( &database.mux );
+   database.kill = 1;
+   pthread_mutex_unlock( &database.mux );
+   sqlite3_close( database.db );
+}
+
+static void *db_loop(void *_){
+   int rc = sqlite3_open( "highscores.db", &database.db );
+
+   if( rc ){
+      vg_error( "database failure: %s\n", sqlite3_errmsg(database.db) );
+      _db_thread_end();
+      return NULL;
+   }
+
+   sqlite3_stmt *stmt = db_stmt(
+         "CREATE TABLE IF NOT EXISTS \n"
+         " users(steamid BIGINT UNIQUE, name VARCHAR(128), type INT);" );
+
+   if( stmt ){
+      int fc = sqlite3_step( stmt );
+      sqlite3_finalize(stmt);
+
+      if( fc == SQLITE_DONE ){
+         vg_success( "Created users table\n" );
+         db_updateuser( 76561198072130043, "harry", 2 );
+      }
+      else{
+         log_sqlite3( fc );
+         _db_thread_end();
+         return NULL;
+      }
+   }
+   else {
+      _db_thread_end();
+      return NULL;
+   }
+
+   /*
+    * Request processing loop 
+    */
+   while(1){
+      pthread_mutex_lock( &database.mux );
+
+      if( database.kill ){
+         pthread_mutex_unlock( &database.mux );
+         _db_thread_end();
+         break;
+      }
+
+      u32 processed = 0;
+
+      for( u32 i=0; i<16; i ++ ){
+         db_request *req = NULL;
+         if( database.queue.tail ){
+            req = (db_request *)database.queue.tail->data;
+            pthread_mutex_unlock( &database.mux );
+         }
+         else{
+            pthread_mutex_unlock( &database.mux );
+            break;
+         }
+         
+         req->handler( req );
+         processed ++;
+
+         pthread_mutex_lock( &database.mux );
+         vg_queue_pop( &database.queue );
+      }
+
+      if( processed )
+         vg_low( "Processed %u database requests.\n", processed );
+
+      usleep(50000);
+   }
+
+   vg_low( "Database thread terminates.\n" );
+   return NULL;
+}
+
+/*
+ * Create database connection and users table
+ */
+static int db_init(void){
+   database.queue.buffer = 
+      (u8 *)vg_linear_alloc( vg_mem.rtmemory, DB_REQUEST_BUFFER_SIZE ),
+   database.queue.size = DB_REQUEST_BUFFER_SIZE;
+
+   if( pthread_mutex_init( &database.mux, NULL ) )
+      return 0;
+
+   if( pthread_create( &database.thread, NULL, db_loop, NULL ) )
+      return 0;
+
+   return 1;
+}
+
+static int db_killed(void){
+   pthread_mutex_lock( &database.mux );
+   int result = database.kill;
+   pthread_mutex_unlock( &database.mux );
+   return result;
+}
+
+static void db_kill(void){
+   pthread_mutex_lock( &database.mux );
+   database.kill = 1;
+   pthread_mutex_unlock( &database.mux );
+   pthread_join( database.thread, NULL );
+}
+
+static void db_free(void){
+   pthread_mutex_destroy( &database.mux );
+}
+
+static db_request *db_alloc_request( u32 size ){
+   u32 total = sizeof(db_request) + size;
+
+   pthread_mutex_lock( &database.mux );
+   vg_queue_frame *frame = vg_queue_alloc( &database.queue, total );
+
+   if( frame ){
+      db_request *req = (db_request *)frame->data;
+      req->size = size;
+      return req;
+   }
+   else {
+      pthread_mutex_unlock( &database.mux );
+      return NULL;
+   }
+}
+
+static void db_send_request( db_request *request ){
+   pthread_mutex_unlock( &database.mux );
+}
+
+#endif /* GAMESERVER_DB_H */
diff --git a/src/gui.h b/src/gui.h
new file mode 100644 (file)
index 0000000..46e2e1e
--- /dev/null
+++ b/src/gui.h
@@ -0,0 +1,360 @@
+#pragma once
+#include "font.h"
+#include "input.h"
+#include "player.h"
+#include "vg/vg_engine.h"
+#include "vg/vg_ui/imgui.h"
+
+#define GUI_COL_DARK   ui_opacity( 0x00000000, 0.7f )
+#define GUI_COL_NORM   ui_opacity( 0x00101010, 0.7f )
+#define GUI_COL_ACTIVE ui_opacity( 0x00444444, 0.7f )
+#define GUI_COL_CLICK  ui_opacity( 0x00858585, 0.7f )
+#define GUI_COL_HI     ui_opacity( 0x00ffffff, 0.8f )
+
+enum gui_icon {
+   k_gui_icon_tick    = 0,
+   k_gui_icon_tick_2d,
+   k_gui_icon_exclaim,
+   k_gui_icon_exclaim_2d,
+   k_gui_icon_board,
+   k_gui_icon_world,
+   k_gui_icon_rift,
+   k_gui_icon_rift_run,
+   k_gui_icon_rift_run_2d,
+   k_gui_icon_friend,
+   k_gui_icon_player,
+   k_gui_icon_rift_run_gold,
+   k_gui_icon_rift_run_silver,
+   k_gui_icon_glider,
+   k_gui_icon_spawn,
+   k_gui_icon_spawn_select,
+
+   k_gui_icon_count,
+};
+
+#define GUI_HELPER_TEXT_LENGTH 32
+
+struct{
+   struct gui_helper{
+      vg_input_op *binding;
+      char text[GUI_HELPER_TEXT_LENGTH];
+      int greyed;
+   }
+   helpers[4];
+   u32 helper_count;
+
+   int active_positional_helper;
+
+   struct icon_call {
+      enum gui_icon icon;
+      v4f location;
+      v4f colour;
+      int colour_changed;
+   }
+   icon_draw_buffer[64];
+   u32 icon_draw_count;
+   v4f cur_icon_colour;
+   int colour_changed;
+
+   char location[64];
+   f64  location_time;
+
+   f32 factive;
+   font3d font;
+
+   v3f trick_co;
+
+   mdl_context model_icons;
+   GLuint icons_texture;
+   glmesh icons_mesh;
+
+   mdl_submesh *icons[ k_gui_icon_count ];
+}
+static gui = {.cur_icon_colour = {1.0f,1.0f,1.0f,1.0f},.colour_changed=1};
+
+static void gui_helper_clear(void){
+   gui.helper_count = 0;
+   gui.active_positional_helper = 0;
+}
+
+static struct gui_helper *gui_new_helper( vg_input_op *bind, vg_str *out_text ){
+   if( gui.helper_count >= VG_ARRAY_LEN(gui.helpers) ){
+      vg_error( "Too many helpers\n" );
+      return NULL;
+   }
+
+   struct gui_helper *helper = &gui.helpers[ gui.helper_count ++ ];
+   helper->greyed = 0;
+   helper->binding = bind;
+   vg_strnull( out_text, helper->text, sizeof(helper->text) );
+   return helper;
+}
+
+static void gui_render_icons(void)
+{
+   vg_camera ortho;
+
+   float fl = 0.0f,
+         fr = vg.window_x,
+         fb = 0.0f,
+         ft = vg.window_y,
+         rl = 1.0f / (fr-fl),
+         tb = 1.0f / (ft-fb);
+
+   m4x4_zero( ortho.mtx.p );
+   ortho.mtx.p[0][0] = 2.0f * rl;
+   ortho.mtx.p[1][1] = 2.0f * tb;
+   ortho.mtx.p[3][0] = (fr + fl) * -rl;
+   ortho.mtx.p[3][1] = (ft + fb) * -tb;
+   ortho.mtx.p[3][3] = 1.0f;
+   m4x3_identity( ortho.transform );
+   vg_camera_update_view( &ortho );
+   m4x4_mul( ortho.mtx.p, ortho.mtx.v, ortho.mtx.pv );   /* HACK */
+   vg_camera_finalize( &ortho );
+
+   /* icons */
+   font3d_bind( &gui.font, k_font_shader_default, 0, NULL, &ortho );
+   mesh_bind( &gui.icons_mesh );
+
+   m4x3f mmdl;
+   m4x3_identity( mmdl );
+   shader_model_font_uMdl( mmdl );
+
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, gui.icons_texture );
+   shader_model_font_uTexMain( 0 );
+
+   for( u32 i=0; i<gui.icon_draw_count; i++ ){
+      struct icon_call *call = &gui.icon_draw_buffer[i];
+
+      if( call->colour_changed )
+         shader_model_font_uColour( call->colour );
+
+      shader_model_font_uOffset( call->location );
+
+      mdl_submesh *sm = gui.icons[ call->icon ];
+      if( sm )
+         mdl_draw_submesh( sm );
+   }
+
+   gui.icon_draw_count = 0;
+}
+
+static void gui_draw( ui_context *ctx )
+{
+   if( gui.active_positional_helper && 
+         (v3_dist2(localplayer.rb.co,gui.trick_co) > 2.0f) )
+      gui_helper_clear();
+
+   /* helpers 
+    * -----------------------------------------------------------------  */
+
+   gui.factive = vg_lerpf( gui.factive, gui.helper_count?1.0f:0.0f,
+                           vg.time_frame_delta*2.0f );
+   
+   ctx->font = &vgf_default_title;
+   ui_px height = ctx->font->ch + 16;
+   ui_rect lwr = { 0, vg.window_y - height, vg.window_x, height };
+
+   ui_px x = 0;
+   for( u32 i=0; i<gui.helper_count; i++ )
+   {
+      struct gui_helper *helper = &gui.helpers[i];
+
+      char buf[128];
+      vg_str str;
+      vg_strnull( &str, buf, sizeof(buf) );
+      vg_input_string( &str, helper->binding, 1 );
+      
+      ui_rect box = { x, lwr[1], 1000, lwr[3] };
+
+      u32 fg = 0;
+      f32 opacity = 0.4f;
+      if( helper->greyed ) 
+      {
+         fg = ui_colour(ctx, k_ui_fg+2);
+         opacity = 0.1f;
+      }
+
+      struct ui_vert *bg = ui_fill( ctx, box, 
+                                    ui_opacity( GUI_COL_DARK, opacity ) );
+
+      u32 w;
+      box[0] += 16;
+      w = ui_text( ctx, box, buf, 1, k_ui_align_middle_left, fg );
+      w *= ctx->font->sx;
+      bg[1].co[0] = x + w + 32;
+      bg[2].co[0] = x + w + 32;
+      x += w + 32;
+
+      box[0] = x;
+      bg = ui_fill( ctx, box, ui_opacity( GUI_COL_NORM, opacity*0.7f ) );
+      box[0] += 8;
+      w = ui_text( ctx, box, helper->text, 1, k_ui_align_middle_left, fg );
+      w *= ctx->font->sx;
+      bg[1].co[0] = box[0] + w + 16;
+      bg[2].co[0] = box[0] + w + 16;
+      x += w + 32;
+   }
+
+   vg_ui.frosting = gui.factive*0.015f; 
+   ui_flush( ctx, k_ui_shader_colour, NULL );
+   vg_ui.frosting = 0.0f;
+
+
+   f64 loc_t = (vg.time_real - gui.location_time) / 5.0;
+   if( (loc_t < 1.0) && (gui.location_time != 0.0) )
+   {
+      f32 t = 1.0f-vg_minf(1.0f,vg_minf(loc_t*20.0f,2.0f-loc_t*2.0f)),
+          o = 1.0f-t*t*(2.0f-t);
+
+      ui_rect box = { 0, (vg.window_y*2)/3 - height/2, vg.window_x, height };
+      ui_fill( ctx, box, ui_opacity( GUI_COL_NORM, 0.5f ) );
+      ui_text( ctx, box, gui.location, 1, k_ui_align_middle_center, 0 );
+
+      vg_ui.colour[3] = o;
+      ui_flush( ctx, k_ui_shader_colour, NULL );
+   }
+
+   vg_ui.colour[3] = 1.0f;
+   ctx->font = &vgf_default_small;
+}
+
+static int gui_location_print_ccmd( int argc, const char *argv[] )
+{
+   if( argc > 0 )
+   {
+      char new_loc[64];
+      vg_str str;
+      vg_strnull( &str, new_loc, 64 );
+      for( int i=0; i<argc; i++ )
+      {
+         vg_strcat( &str, argv[i] );
+         vg_strcat( &str, " " );
+      }
+      if( !strcmp(gui.location,new_loc) ) return 0;
+      vg_strncpy( new_loc, gui.location, 64, k_strncpy_always_add_null );
+      gui.location_time = vg.time_real;
+   }
+   return 0;
+}
+
+static int gui_showtrick_ccmd( int argc, const char *argv[] )
+{
+   if( argc == 1 )
+   {
+      gui_helper_clear();
+      vg_str text;
+
+           if( !strcmp( argv[0], "pump" ) ){
+         if( gui_new_helper( input_axis_list[k_sraxis_grab], &text ) )
+            vg_strcat( &text, "Pump" );
+      }
+      else if( !strcmp( argv[0], "flip" ) ){
+         if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ) )
+            vg_strcat( &text, "Flip" );
+      }
+      else if( !strcmp( argv[0], "ollie" ) ){
+         if( gui_new_helper( input_button_list[k_srbind_jump], &text ) )
+            vg_strcat( &text, "Ollie" );
+      }
+      else if( !strcmp( argv[0], "trick" ) ){
+         if( gui_new_helper( input_button_list[k_srbind_trick0], &text ) )
+            vg_strcat( &text, "Shuvit" );
+         if( gui_new_helper( input_button_list[k_srbind_trick1], &text ) )
+            vg_strcat( &text, "Kickflip" );
+         if( gui_new_helper( input_button_list[k_srbind_trick2], &text ) )
+            vg_strcat( &text, "Tre-Flip" );
+      }
+      else if( !strcmp( argv[0], "misc" ) ){
+         if( gui_new_helper( input_button_list[k_srbind_camera], &text ) )
+            vg_strcat( &text, "Camera" );
+         if( gui_new_helper( input_button_list[k_srbind_use], &text ) )
+            vg_strcat( &text, "Skate/Walk" );
+      }
+      else return 1;
+
+      v3_copy( localplayer.rb.co, gui.trick_co );
+      gui.active_positional_helper = 1;
+      return 0;
+   }
+   return 1;
+}
+
+static void gui_draw_icon( enum gui_icon icon, v2f co, f32 size )
+{
+   if( gui.icon_draw_count == VG_ARRAY_LEN(gui.icon_draw_buffer) )
+      return;
+
+   struct icon_call *call = &gui.icon_draw_buffer[ gui.icon_draw_count ++ ];
+
+   call->icon = icon;
+   call->location[0] = co[0] * (f32)vg.window_x;
+   call->location[1] = co[1] * (f32)vg.window_y;
+   call->location[2] = 0.0f;
+   call->location[3] = size * (f32)vg.window_x;
+
+   v4_copy( gui.cur_icon_colour, call->colour );
+   call->colour_changed = gui.colour_changed;
+   gui.colour_changed = 0;
+}
+
+static void gui_icon_setcolour( v4f colour ){
+   gui.colour_changed = 1;
+   v4_copy( colour, gui.cur_icon_colour );
+}
+
+static mdl_submesh *gui_find_icon( const char *name ){
+   mdl_mesh *mesh = mdl_find_mesh( &gui.model_icons, name );
+   if( mesh ){
+      if( mesh->submesh_count ){
+         return mdl_arritm( &gui.model_icons.submeshs, mesh->submesh_start );
+      }
+   }
+
+   return NULL;
+}
+
+static void gui_init(void)
+{
+   font3d_load( &gui.font, "models/rs_font.mdl", vg_mem.rtmemory );
+   vg_console_reg_cmd( "gui_location", gui_location_print_ccmd, NULL );
+   vg_console_reg_cmd( "showtrick", gui_showtrick_ccmd, NULL );
+
+   /* load icons */
+   void *alloc = vg_mem.rtmemory;
+   mdl_open( &gui.model_icons, "models/rs_icons.mdl", alloc );
+   mdl_load_metadata_block( &gui.model_icons, alloc );
+
+   gui.icons[ k_gui_icon_tick ] = gui_find_icon( "icon_tick" );
+   gui.icons[ k_gui_icon_tick_2d ] = gui_find_icon( "icon_tick2d" );
+   gui.icons[ k_gui_icon_exclaim ] = gui_find_icon( "icon_exclaim" );
+   gui.icons[ k_gui_icon_exclaim_2d ] = gui_find_icon( "icon_exclaim2d" );
+   gui.icons[ k_gui_icon_board ] = gui_find_icon( "icon_board" );
+   gui.icons[ k_gui_icon_world ] = gui_find_icon( "icon_world" );
+   gui.icons[ k_gui_icon_rift ] = gui_find_icon( "icon_rift" );
+   gui.icons[ k_gui_icon_rift_run ] = gui_find_icon( "icon_rift_run" );
+   gui.icons[ k_gui_icon_rift_run_2d ] = gui_find_icon( "icon_rift_run2d" );
+   gui.icons[ k_gui_icon_friend ] = gui_find_icon( "icon_friend" );
+   gui.icons[ k_gui_icon_player ] = gui_find_icon( "icon_player" );
+   gui.icons[ k_gui_icon_glider ] = gui_find_icon( "icon_glider" );
+   gui.icons[ k_gui_icon_spawn ] = gui_find_icon( "icon_spawn" );
+   gui.icons[ k_gui_icon_spawn_select ] = gui_find_icon( "icon_spawn_select" );
+   gui.icons[ k_gui_icon_rift_run_gold ] =
+      gui_find_icon("icon_rift_run_medal_gold");
+   gui.icons[ k_gui_icon_rift_run_silver]=
+      gui_find_icon("icon_rift_run_medal_silver");
+
+   vg_linear_clear( vg_mem.scratch );
+   if( !mdl_arrcount( &gui.model_icons.textures ) )
+      vg_fatal_error( "No texture in menu file" );
+   mdl_texture *tex0 = mdl_arritm( &gui.model_icons.textures, 0 );
+   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
+   mdl_fread_pack_file( &gui.model_icons, &tex0->file, data );
+   vg_tex2d_load_qoi_async( data, tex0->file.pack_size, 
+                            VG_TEX2D_LINEAR|VG_TEX2D_CLAMP,
+                            &gui.icons_texture );
+
+   mdl_async_load_glmesh( &gui.model_icons, &gui.icons_mesh, NULL );
+   mdl_close( &gui.model_icons );
+}
diff --git a/src/input.h b/src/input.h
new file mode 100644 (file)
index 0000000..3377824
--- /dev/null
@@ -0,0 +1,311 @@
+#pragma once
+#include "vg/vg_platform.h" 
+#include "vg/vg_console.h"
+#include "vg/vg_input.h"
+#include "vg/vg_m.h"
+#include "font.h"
+
+enum sr_bind
+{
+   k_srbind_jump = 0,
+   k_srbind_push,
+   k_srbind_skid,
+   k_srbind_trick0,
+   k_srbind_trick1,
+   k_srbind_trick2,
+   k_srbind_sit,
+   k_srbind_use,
+   k_srbind_reset,
+   k_srbind_dead_respawn,
+   k_srbind_camera,
+   k_srbind_mleft, 
+   k_srbind_mright, 
+   k_srbind_mup, 
+   k_srbind_mdown,
+   k_srbind_mback, 
+   k_srbind_maccept,
+   k_srbind_mopen,
+   k_srbind_mhub,
+   k_srbind_replay_play,
+   k_srbind_replay_freecam,
+   k_srbind_replay_resume,
+   k_srbind_world_left,
+   k_srbind_world_right,
+   k_srbind_home,
+   k_srbind_lobby,
+   k_srbind_chat,
+   k_srbind_run,
+   
+   k_srbind_miniworld_teleport,
+   k_srbind_miniworld_resume,
+   k_srbind_devbutton,
+   k_srbind_max,
+};
+
+enum sr_joystick{
+   k_srjoystick_steer = 0,
+   k_srjoystick_grab,
+   k_srjoystick_look,
+   k_srjoystick_max
+};
+
+enum sr_axis{
+   k_sraxis_grab = 0,
+   k_sraxis_mbrowse_h,
+   k_sraxis_mbrowse_v,
+   k_sraxis_replay_h,
+   k_sraxis_skid,
+   k_sraxis_max
+};
+
+
+#define INPUT_BASIC( KB, JS ) \
+   (vg_input_op[]){vg_keyboard, KB, vg_joy_button, JS, vg_end}
+
+static vg_input_op *input_button_list[] = {
+[k_srbind_jump]  = INPUT_BASIC( SDLK_SPACE, SDL_CONTROLLER_BUTTON_A ),
+[k_srbind_push]  = INPUT_BASIC( SDLK_w, SDL_CONTROLLER_BUTTON_B ),
+[k_srbind_trick0] = (vg_input_op[]){
+   vg_mouse, SDL_BUTTON_LEFT,
+   vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end
+},
+[k_srbind_trick1] = (vg_input_op[]){
+   vg_mouse, SDL_BUTTON_RIGHT,
+   vg_joy_button, SDL_CONTROLLER_BUTTON_B, vg_end
+},
+[k_srbind_trick2] = (vg_input_op[]){ 
+   vg_mouse, SDL_BUTTON_LEFT, vg_mode_mul, vg_mouse, SDL_BUTTON_RIGHT, 
+   vg_mode_absmax, vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end 
+},
+[k_srbind_use]   = INPUT_BASIC( SDLK_e, SDL_CONTROLLER_BUTTON_Y ),
+[k_srbind_reset] = INPUT_BASIC( SDLK_r, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
+[k_srbind_dead_respawn] =
+   INPUT_BASIC( SDLK_q, SDL_CONTROLLER_BUTTON_DPAD_UP ),
+[k_srbind_camera]= INPUT_BASIC( SDLK_c, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
+[k_srbind_mleft] = INPUT_BASIC( SDLK_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
+[k_srbind_mright]= INPUT_BASIC( SDLK_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
+[k_srbind_world_left] = 
+                   INPUT_BASIC( SDLK_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
+[k_srbind_world_right] =
+                   INPUT_BASIC( SDLK_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT ),
+[k_srbind_mup]   = INPUT_BASIC( SDLK_UP, SDL_CONTROLLER_BUTTON_DPAD_UP ),
+[k_srbind_mdown] = INPUT_BASIC( SDLK_DOWN, SDL_CONTROLLER_BUTTON_DPAD_DOWN ),
+[k_srbind_mback] = INPUT_BASIC( SDLK_ESCAPE, SDL_CONTROLLER_BUTTON_B ),
+[k_srbind_mopen] = INPUT_BASIC( SDLK_ESCAPE, SDL_CONTROLLER_BUTTON_START ),
+[k_srbind_mhub]  = INPUT_BASIC( SDLK_h, SDL_CONTROLLER_BUTTON_Y ),
+[k_srbind_maccept] = (vg_input_op[]){
+   vg_keyboard, SDLK_e, vg_gui_visible, 0,
+      vg_keyboard, SDLK_RETURN, vg_keyboard, SDLK_RETURN2,
+   vg_gui_visible, 1,
+   vg_joy_button, SDL_CONTROLLER_BUTTON_A, vg_end
+},
+[k_srbind_replay_play]    = INPUT_BASIC( SDLK_g, SDL_CONTROLLER_BUTTON_X ),
+[k_srbind_replay_resume]  = INPUT_BASIC( SDLK_SPACE, SDL_CONTROLLER_BUTTON_A ),
+[k_srbind_replay_freecam] = INPUT_BASIC( SDLK_f, SDL_CONTROLLER_BUTTON_Y ),
+[k_srbind_sit]   = INPUT_BASIC( SDLK_z, SDL_CONTROLLER_BUTTON_B ),
+[k_srbind_lobby] = INPUT_BASIC( SDLK_TAB, SDL_CONTROLLER_BUTTON_DPAD_LEFT ),
+[k_srbind_chat ] = (vg_input_op[]){ vg_keyboard, SDLK_y, vg_end },
+[k_srbind_run ] = (vg_input_op[]){ vg_keyboard, SDLK_LSHIFT, 
+   vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT, vg_end },
+
+[k_srbind_miniworld_resume]  = (vg_input_op[]){
+   vg_keyboard, SDLK_RETURN, vg_gui_visible, 0,
+   vg_keyboard, SDLK_RETURN2,
+   vg_gui_visible, 1,
+   vg_joy_button, SDL_CONTROLLER_BUTTON_X, vg_end
+},
+[k_srbind_miniworld_teleport]= INPUT_BASIC( SDLK_q, 
+                                       SDL_CONTROLLER_BUTTON_LEFTSHOULDER ),
+[k_srbind_skid] = (vg_input_op[]){ vg_keyboard, SDLK_LCTRL, vg_end },
+[k_srbind_devbutton] = (vg_input_op[]){ vg_keyboard, SDLK_3, vg_end },
+[k_srbind_max]=NULL
+};
+
+static vg_input_op *input_axis_list[] = {
+[k_sraxis_grab] = (vg_input_op[]){
+   vg_keyboard, SDLK_LSHIFT, 
+   vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, vg_end
+},
+[k_sraxis_mbrowse_h] = (vg_input_op[]){
+   vg_mode_sub, vg_keyboard, SDLK_LEFT,
+   vg_mode_add, vg_keyboard, SDLK_RIGHT,
+   vg_mode_add, vg_joy_axis, SDL_CONTROLLER_AXIS_LEFTX,
+   vg_end
+},
+[k_sraxis_mbrowse_v] = (vg_input_op[]){
+   vg_mode_sub, vg_keyboard, SDLK_DOWN,
+   vg_mode_add, vg_keyboard, SDLK_UP,
+   vg_mode_sub, vg_joy_axis, SDL_CONTROLLER_AXIS_LEFTY,
+   vg_end
+},
+[k_sraxis_replay_h] = (vg_input_op[]){
+   vg_mode_sub, vg_keyboard, SDLK_q,
+   vg_mode_add, vg_keyboard, SDLK_e,
+   vg_mode_sub, vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERLEFT,
+   vg_mode_add, vg_joy_axis, SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
+   vg_end
+},
+[k_sraxis_skid] = (vg_input_op[]){
+   vg_mode_sub, vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
+   vg_mode_add, vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
+   vg_end
+},
+[k_sraxis_max]=NULL
+};
+
+static vg_input_op *input_joy_list[] = {
+[k_srjoystick_steer] = (vg_input_op[]){
+   vg_index, 0, vg_mode_sub, vg_keyboard, SDLK_a,
+                vg_mode_add, vg_keyboard, SDLK_d,
+   vg_index, 1, vg_mode_sub, vg_keyboard, SDLK_w,
+                vg_mode_add, vg_keyboard, SDLK_s,
+                vg_mode_absmax, vg_joy_ls,
+   vg_end
+},
+[k_srjoystick_grab] = (vg_input_op[]){
+   vg_joy_rs, vg_end
+},
+[k_srjoystick_look] = (vg_input_op[]){
+   vg_joy_rs, vg_end
+},
+[k_srjoystick_max]=NULL
+};
+
+struct {
+   float axis_states[ k_sraxis_max ][2];
+   v2f joystick_states[ k_srjoystick_max ][2];
+   u8 button_states[ k_srbind_max ][2];
+
+   enum input_state {
+      k_input_state_enabled,
+      k_input_state_resume,
+      k_input_state_resuming,
+      k_input_state_pause
+   }
+   state;
+}
+static srinput;
+
+static int input_filter_generic(void){
+   if( (srinput.state != k_input_state_enabled) || vg_console.enabled ||
+       (workshop_form.page != k_workshop_form_hidden)  )
+      return 1;
+   else 
+      return 0;
+}
+
+static int buttons_filter_fixed(void){
+   if( input_filter_generic() ) 
+      return 1;
+
+   if( vg.engine_stage == k_engine_stage_update_fixed )
+      if( vg.fixed_iterations > 0 )
+         return 1;
+
+   return 0;
+}
+
+/* Rising edge of button */
+static int button_down( enum sr_bind button ){
+   if( buttons_filter_fixed() ) return 0;
+   
+   if(  srinput.button_states[ button ][0] && 
+       !srinput.button_states[ button ][1] )
+      return 1;
+   else
+      return 0;
+}
+
+/* Falling edge of button */
+static int button_up( enum sr_bind button ){
+   if( buttons_filter_fixed() ) return 0;
+   
+   if( !srinput.button_states[ button ][0] && 
+        srinput.button_states[ button ][1] )
+      return 1;
+   else
+      return 0;
+}
+
+/* State of button */
+static int button_press( enum sr_bind button ){
+   if( input_filter_generic() )
+      return 0;
+   return 
+      srinput.button_states[ button ][0];
+}
+
+static void joystick_state( enum sr_joystick joystick, v2f state ){
+   if( input_filter_generic() )
+      v2_zero( state );
+   else
+      v2_copy( srinput.joystick_states[ joystick ][0], state );
+}
+
+static float axis_state( enum sr_axis axis ){
+   if( input_filter_generic() )
+      return 0.0f;
+   else 
+      return srinput.axis_states[axis][0];
+}
+
+static void skaterift_preupdate_inputs(void){
+   if( srinput.state == k_input_state_resuming )
+      srinput.state = k_input_state_enabled;
+
+   if( srinput.state == k_input_state_resume )
+      srinput.state = k_input_state_resuming;
+
+   for( u32 i=0; i<k_srbind_max; i++ ){
+      srinput.button_states[i][1] = srinput.button_states[i][0];
+      srinput.button_states[i][0] = 0;
+   }
+
+   for( u32 i=0; i<k_srjoystick_max; i++ ){
+      v2_copy( srinput.joystick_states[i][0], srinput.joystick_states[i][1] );
+      v2_zero( srinput.joystick_states[i][0] );
+   }
+
+   for( u32 i=0; i<k_sraxis_max; i++ ){
+      srinput.axis_states[i][1] = srinput.axis_states[i][0];
+      srinput.axis_states[i][0] = 0.0f;
+   }
+
+   for( int i=0; i<k_srbind_max; i++ ){
+      vg_input_op *prog = input_button_list[i];
+      if( prog ){
+         vg_exec_input_program( k_vg_input_type_button_u8, prog,
+                                &srinput.button_states[i][0] );
+      }
+   }
+
+   for( int i=0; i<k_sraxis_max; i++ ){
+      vg_input_op *prog = input_axis_list[i];
+      if( prog ){
+         vg_exec_input_program( k_vg_input_type_axis_f32, prog,
+                                &srinput.axis_states[i][0] );
+      }
+   }
+
+   for( int i=0; i<k_srjoystick_max; i++ ){
+      vg_input_op *prog = input_joy_list[i];
+      if( prog ){
+         vg_exec_input_program( k_vg_input_type_joy_v2f, prog,
+                                srinput.joystick_states[i][0] );
+      }
+   }
+
+   f32 x = srinput.axis_states[k_sraxis_mbrowse_h][0],
+       y = srinput.axis_states[k_sraxis_mbrowse_v][0],
+       sensitivity = 0.35f;
+
+   if( fabsf(x) > sensitivity ){
+      if( x > 0.0f ) srinput.button_states[k_srbind_mright][0] = 1;
+      else           srinput.button_states[k_srbind_mleft][0] = 1;
+   }
+
+   if( fabsf(y) > sensitivity ){
+      if( y > 0.0f ) srinput.button_states[k_srbind_mup][0] = 1;
+      else           srinput.button_states[k_srbind_mdown][0] = 1;
+   }
+}
diff --git a/src/menu.c b/src/menu.c
new file mode 100644 (file)
index 0000000..1f2b9c8
--- /dev/null
@@ -0,0 +1,1095 @@
+#pragma once
+#include "skaterift.h"
+#include "menu.h"
+#include "model.h"
+#include "entity.h"
+#include "input.h"
+#include "world_map.h"
+#include "ent_miniworld.h"
+#include "audio.h"
+#include "workshop.h"
+#include "gui.h"
+#include "control_overlay.h"
+#include "network.h"
+#include "shaders/model_menu.h"
+
+struct global_menu menu = { .skip_starter = 0 };
+
+void menu_at_begin(void)
+{
+   if( menu.skip_starter ) return;
+
+   skaterift.activity = k_skaterift_menu;
+   menu.page = k_menu_page_starter;
+}
+
+void menu_init(void)
+{
+   vg_console_reg_var( "skip_starter_menu", &menu.skip_starter,
+                       k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_tex2d_load_qoi_async_file( "textures/prem.qoi", 
+         VG_TEX2D_CLAMP|VG_TEX2D_NOMIP|VG_TEX2D_NEAREST,
+         &menu.prem_tex );
+}
+
+void menu_open( enum menu_page page )
+{
+   skaterift.activity = k_skaterift_menu;
+
+   if( page != k_menu_page_any )
+   {
+      menu.page = page;
+   }
+}
+
+bool menu_viewing_map(void)
+{
+   return (skaterift.activity == k_skaterift_menu) && 
+          (menu.page == k_menu_page_main) &&
+          (menu.main_index == k_menu_main_map);
+}
+
+static void menu_decor_select( ui_context *ctx, ui_rect rect )
+{
+   ui_px b = ctx->font->sx, hb = b/2;
+   ui_rect a0 = { rect[0] - 20 - hb,           rect[1] + rect[3]/2 - hb, b,b },
+           a1 = { rect[0] + rect[2] + 20 + hb, rect[1] + rect[3]/2 - hb, b,b };
+
+   ui_text( ctx, a0, "\x95", 1, k_ui_align_middle_center, 0 );
+   ui_text( ctx, a1, "\x93", 1, k_ui_align_middle_center, 0 );
+}
+
+static void menu_standard_widget( ui_context *ctx,
+                                  ui_rect inout_panel, ui_rect rect, ui_px s )
+{
+   ui_split( inout_panel, k_ui_axis_h, ctx->font->sy*s*2, 
+             8, rect, inout_panel );
+}
+
+static bool menu_slider( ui_context *ctx,
+                         ui_rect inout_panel, bool select, const char *label,
+                         const f32 disp_min, const f32 disp_max, f32 *value, 
+                         const char *format )
+{
+   ui_rect rect, box;
+   menu_standard_widget( ctx, inout_panel, rect, 1 );
+   ui_label( ctx, rect, label, 1, 8, box );
+
+   f32 t;
+   enum ui_button_state state = ui_slider_base( ctx, box, 0, 1, value, &t ),
+         mask_using = 
+         k_ui_button_holding_inside |
+         k_ui_button_holding_outside |
+         k_ui_button_click,
+      mask_brighter = mask_using | k_ui_button_hover;
+
+   if( vg_input.display_input_method == k_input_method_controller )
+   {
+      if( select )
+      {
+         f32 m = axis_state( k_sraxis_mbrowse_h );
+         if( fabsf(m) > 0.5f )
+         {
+            *value += m * vg.time_frame_delta * (1.0f/2.0f);
+            *value = vg_clampf( *value, 0, 1 );
+         }
+
+         menu_decor_select( ctx, rect );
+         state |= k_ui_button_hover;
+      }
+      else
+         state = k_ui_button_none;
+   }
+
+   ui_rect line = { box[0], box[1], t * (f32)box[2], box[3] };
+   ui_fill( ctx, line, state&mask_brighter? GUI_COL_ACTIVE: GUI_COL_NORM );
+   ui_fill( ctx, (ui_rect){ box[0]+line[2], box[1], box[2]-line[2], box[3] },
+               GUI_COL_DARK );
+
+   ui_outline( ctx, box, 1, state? GUI_COL_HI: GUI_COL_ACTIVE, 0 );
+   ui_slider_text( ctx, box, 
+                   format, disp_min + (*value)*( disp_max-disp_min ) );
+
+   return (state & mask_using) && 1;
+}
+
+static bool menu_button( ui_context *ctx,
+                         ui_rect inout_panel, bool select, const char *text )
+{
+   ui_rect rect;
+   menu_standard_widget( ctx, inout_panel, rect, 1 );
+
+   enum ui_button_state state = k_ui_button_none;
+
+   if( vg_input.display_input_method == k_input_method_controller )
+   {
+      if( select )
+      {
+         menu_decor_select( ctx, rect );
+
+         if( button_down( k_srbind_maccept ) )
+            state = k_ui_button_click;
+      }
+   }
+   else
+   {
+      state = ui_button_base( ctx, rect );
+      select = 0;
+   }
+
+   if( state == k_ui_button_click )
+   {
+      ui_fill( ctx, rect, GUI_COL_DARK );
+   }
+   else if( state == k_ui_button_holding_inside )
+   {
+      ui_fill( ctx, rect, GUI_COL_DARK );
+   }
+   else if( state == k_ui_button_holding_outside )
+   {
+      ui_fill( ctx, rect, GUI_COL_DARK );
+      ui_outline( ctx, rect, 1, GUI_COL_CLICK, 0 );
+   }
+   else if( state == k_ui_button_hover )
+   {
+      ui_fill( ctx, rect, GUI_COL_ACTIVE );
+      ui_outline( ctx, rect, 1, GUI_COL_CLICK, 0 );
+   }
+   else 
+   {
+      ui_fill( ctx, rect, select? GUI_COL_ACTIVE: GUI_COL_NORM );
+      if( select )
+         ui_outline( ctx, rect, 1, GUI_COL_HI, 0 );
+   }
+
+   ui_text( ctx, rect, text, 1, k_ui_align_middle_center, 0 );
+
+   if( state == k_ui_button_click )
+   {
+      audio_lock();
+      audio_oneshot( &audio_ui[0], 1.0f, 0.0f );
+      audio_unlock();
+      return 1;
+   }
+   else return 0;
+}
+
+static bool menu_checkbox( ui_context *ctx, ui_rect inout_panel, bool select, 
+                           const char *str_label, i32 *data )
+{
+   ui_rect rect, label, box;
+   menu_standard_widget( ctx, inout_panel, rect, 1 );
+
+   ui_split( rect, k_ui_axis_v, -rect[3], 0, label, box );
+   ui_text( ctx, label, str_label, ctx->scale, k_ui_align_middle_left, 0 );
+   
+   enum ui_button_state state = k_ui_button_none;
+
+   if( vg_input.display_input_method == k_input_method_controller )
+   {
+      if( select )
+      {
+         menu_decor_select( ctx, rect );
+
+         if( button_down( k_srbind_maccept ) )
+         {
+            *data = (*data) ^ 0x1;
+            state = k_ui_button_click;
+         }
+      }
+   }
+   else
+   {
+      state = ui_checkbox_base( ctx, box, data );
+      select = 0;
+   }
+
+   if( state == k_ui_button_holding_inside )
+   {
+      ui_fill( ctx, box, GUI_COL_ACTIVE );
+      ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
+   }
+   else if( state == k_ui_button_holding_outside )
+   {
+      ui_fill( ctx, box, GUI_COL_DARK );
+      ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
+   }
+   else if( state == k_ui_button_hover )
+   {
+      ui_fill( ctx, box, GUI_COL_ACTIVE );
+      ui_outline( ctx, box, 1, GUI_COL_HI, 0 );
+   }
+   else 
+   {
+      ui_fill( ctx, box, select? GUI_COL_ACTIVE: GUI_COL_DARK );
+      ui_outline( ctx, box, 1, select? GUI_COL_HI: GUI_COL_NORM, 0 );
+   }
+
+   if( *data )
+   {
+      ui_rect_pad( box, (ui_px[2]){8,8} );
+      ui_fill( ctx, box, GUI_COL_HI );
+   }
+
+   if( state == k_ui_button_click )
+   {
+      audio_lock();
+      audio_oneshot( &audio_ui[0], 1.0f, 0.0f );
+      audio_unlock();
+      return 1;
+   }
+   else return 0;
+}
+
+static void menu_heading( ui_context *ctx,
+                          ui_rect inout_panel, const char *label, u32 colour )
+{
+   ui_rect rect;
+   menu_standard_widget( ctx, inout_panel, rect, 1 );
+
+   rect[0] -= 8;
+   rect[2] += 16;
+
+   u32 c0 = ui_opacity( GUI_COL_DARK, 0.36f ),
+       c1 = ui_opacity( GUI_COL_DARK, 0.5f );
+
+   struct ui_vert *vs = ui_fill( ctx, rect, c0 );
+
+   vs[0].colour = c1;
+   vs[1].colour = c1;
+
+   rect[1] += 4;
+   ui_text( ctx, rect, label, 1, k_ui_align_middle_center, 1 );
+   rect[0] += 1;
+   rect[1] -= 1;
+   ui_text( ctx, rect, label, 1, k_ui_align_middle_center, colour? colour:
+            ui_colour(ctx, k_ui_blue+k_ui_brighter) );
+}
+
+static u32 medal_colour( ui_context *ctx, u32 flags )
+{
+   if( flags & k_ent_route_flag_achieve_gold ) 
+      return ui_colour( ctx, k_ui_yellow );
+   else if( flags & k_ent_route_flag_achieve_silver )
+      return ui_colour( ctx, k_ui_fg );
+   else return 0;
+}
+
+static i32 menu_nav( i32 *p_row, int mv, i32 max )
+{
+   i32 row_prev = *p_row;
+
+   if( mv < 0 ) *p_row = vg_min( max, (*p_row) +1 );
+   if( mv > 0 ) *p_row = vg_max( 0,   (*p_row) -1 );
+
+   if( *p_row != row_prev )
+   {
+      audio_lock();
+      audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+      audio_unlock();
+   }
+
+   return *p_row;
+}
+
+static void menu_try_find_cam( i32 id )
+{
+   world_instance *world = &world_static.instances[0];
+   for( u32 i=0; i<mdl_arrcount(&world->ent_npc); i ++ )
+   {
+      ent_npc *fnpc = mdl_arritm( &world->ent_npc, i );
+      if( (fnpc->id == 50) && (fnpc->context == id) )
+      {
+         if( mdl_entity_id_type(fnpc->camera) == k_ent_camera )
+         {
+            u32 index = mdl_entity_id_id( fnpc->camera );
+            menu.bg_cam = mdl_arritm( &world->ent_camera, index );
+            menu.bg_blur = 0;
+         }
+      }
+   }
+}
+
+static void menu_link_modal( const char *url )
+{
+   menu.web_link = url;
+
+   /* Only reset the choice of browser if 'No' was selected. */
+   if( menu.web_choice == 2 )
+      menu.web_choice = 0;
+}
+
+void menu_gui( ui_context *ctx )
+{
+   if( button_down( k_srbind_mopen ) )
+   {
+      if( skaterift.activity == k_skaterift_default )
+      {
+         menu_open( k_menu_page_main );
+         return;
+      }
+   }
+
+   if( skaterift.activity != k_skaterift_menu ) 
+      return;
+
+   menu.bg_blur = 1;
+   menu.bg_cam = NULL;
+
+   /* get buttons inputs
+    * -------------------------------------------------------------------*/
+   int ml = button_press( k_srbind_mleft ),
+       mr = button_press( k_srbind_mright ),
+       mu = button_press( k_srbind_mup ),
+       md = button_press( k_srbind_mdown ),
+       mh = ml-mr,
+       mv = mu-md,
+       enter = button_down( k_srbind_maccept );
+
+   /* TAB CONTROL */
+   u8 lb_down = 0, rb_down = 0;
+   vg_exec_input_program( k_vg_input_type_button_u8, 
+         (vg_input_op[]){
+            vg_joy_button, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, vg_end 
+         }, &rb_down );
+   vg_exec_input_program( k_vg_input_type_button_u8, 
+         (vg_input_op[]){
+            vg_joy_button, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, vg_end 
+         }, &lb_down );
+
+   int mtab = (i32)rb_down - (i32)lb_down;
+
+   if( menu.repeater > 0.0f )
+   {
+      menu.repeater -= vg_minf( vg.time_frame_delta, 0.5f );
+      mv = 0;
+      mh = 0;
+      mtab = 0;
+   }
+   else
+   {
+      if( mv || mh || mtab )
+         menu.repeater += 0.2f;
+   }
+
+   if( vg_input.display_input_method == k_input_method_kbm )
+   {
+      ui_capture_mouse(ctx, 1);
+   }
+
+   if( skaterift.activity != k_skaterift_menu ) return;
+
+
+   if( menu.web_link )
+   {
+      menu_try_find_cam( 3 );
+
+      ui_rect panel = { 0,0, 800, 200 },
+              screen = { 0,0, vg.window_x,vg.window_y };
+      ui_rect_center( screen, panel );
+      ui_fill( ctx, panel, GUI_COL_DARK );
+      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
+      ui_rect_pad( panel, (ui_px[]){8,8} );
+
+      ui_rect title;
+      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
+      ctx->font = &vgf_default_title;
+      ui_text( ctx, title, "Open Link?", 1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
+      ctx->font = &vgf_default_large;
+      ui_text( ctx, title, menu.web_link, 1, k_ui_align_middle_center, 0 );
+
+      ui_rect end = { panel[0], panel[1] + panel[3] - 48, panel[2], 48 };
+
+      ui_rect a,b,c;
+      ui_split_ratio( end, k_ui_axis_v, 2.0/3.0, 2, a, c );
+      ui_split_ratio( a, k_ui_axis_v, 1.0/2.0, 2, a, b );
+
+      i32 R = menu_nav( &menu.web_choice, mh, 2 );
+
+      if( menu_button( ctx, a, R==0, "Steam Overlay" ) )
+      {
+         if( steam_ready )
+         {
+            ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
+            SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage( hSteamFriends,
+                  menu.web_link,
+                  k_EActivateGameOverlayToWebPageMode_Default );
+            menu.web_link = NULL;
+         }
+      }
+
+      if( menu_button( ctx, b, R==1, "Web Browser" ) )
+      {
+         char buf[512];
+         vg_str str;
+         vg_strnull( &str, buf, sizeof(buf) );
+#ifdef _WIN32
+         vg_strcat( &str, "start " );
+#else
+         vg_strcat( &str, "xdg-open " );
+#endif
+         vg_strcat( &str, menu.web_link );
+
+         if( vg_strgood(&str) )
+            system( buf );
+
+         menu.web_link = NULL;
+      }
+      
+      if( menu_button( ctx, c, R==2, "No" ) || button_down( k_srbind_mback ) )
+      {
+         audio_lock();
+         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+         audio_unlock();
+         menu.web_link = NULL;
+      }
+
+      goto menu_draw;
+   }
+
+
+   if( vg.settings_open )
+   {
+      if( button_down( k_srbind_mback ) )
+      {
+         audio_lock();
+         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+         audio_unlock();
+         vg_settings_close();
+         srinput.state = k_input_state_resume;
+      }
+
+      return;
+   }
+
+   if( menu.page == k_menu_page_credits )
+   {
+      ui_rect panel = { 0,0, 600, 400 },
+              screen = { 0,0, vg.window_x,vg.window_y };
+      ui_rect_center( screen, panel );
+      ui_fill( ctx, panel, GUI_COL_DARK );
+      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
+      ui_rect_pad( panel, (ui_px[]){8,8} );
+
+      ui_rect title;
+      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
+      ctx->font = &vgf_default_title;
+      ui_text( ctx, title, "Skate Rift - Credits", 
+               1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
+      ctx->font = &vgf_default_large;
+      ui_text( ctx, title, 
+               "Mt.Zero Software", 1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 8, 0, title, panel );
+      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
+      ctx->font = &vgf_default_title;
+      ui_text( ctx, title, "Free Software", 1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 8, 0, title, panel );
+      ctx->font = &vgf_default_large;
+      ui_text( ctx, panel, 
+            "Sam Lantinga       - SDL2 - libsdl.org\n"
+            "Hunter WB          - Anyascii\n"
+            "David Herberth     - GLAD\n"
+            "Dominic Szablewski - QOI - qoiformat.org\n"
+            "Sean Barrett       - stb_image, stb_vorbis,\n"
+            "                     stb_include\n"
+            "Khronos Group      - OpenGL\n"
+            , 1, k_ui_align_left, 0 );
+
+      ui_rect end = { panel[0], panel[1] + panel[3] - 64, panel[2], 64 };
+
+      if( menu_button( ctx, end, 1, "Back" ) || button_down( k_srbind_mback ) )
+      {
+         menu.page = k_menu_page_main;
+      }
+
+      goto menu_draw;
+   }
+   else if( menu.page == k_menu_page_starter )
+   {
+      i32 R = menu_nav( &menu.intro_row, mv, 3 );
+      ui_rect panel = { 0,0, 600, 400 },
+              screen = { 0,0, vg.window_x,vg.window_y };
+      ui_rect_center( screen, panel );
+      ui_fill( ctx, panel, ui_opacity( GUI_COL_DARK, 0.35f ) );
+      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
+      ui_rect_pad( panel, (ui_px[]){8,8} );
+
+      ui_rect title;
+      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
+      ctx->font = &vgf_default_title;
+      ui_text( ctx, title, 
+               "Welcome to Skate Rift", 1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
+      ctx->font = &vgf_default_large;
+
+      menu_checkbox( ctx, panel, R == 0, 
+            "Show controls overlay (good for new players)",
+            &control_overlay.enabled );
+      menu_checkbox( ctx, panel, R == 1, "Auto connect to global server",
+            &network_client.auto_connect );
+
+      ui_rect end = { panel[0], panel[1] + panel[3] - 100, panel[2], 100 };
+      menu_checkbox( ctx, end, R == 2, 
+                     "Don't show this again", &menu.skip_starter );
+      if( menu_button( ctx, end, R == 3, "OK" ) )
+      {
+         menu.page = k_menu_page_main;
+         skaterift.activity = k_skaterift_default;
+      }
+
+      menu_try_find_cam( 3 );
+      goto menu_draw;
+   }
+   else if( menu.page == k_menu_page_premium )
+   {
+      i32 R = menu_nav( &menu.prem_row, mh, 1 );
+      ui_rect panel = { 0,0, 600, 400+240 },
+              screen = { 0,0, vg.window_x,vg.window_y };
+      ui_rect_center( screen, panel );
+      ui_fill( ctx, panel, ui_opacity( GUI_COL_DARK, 0.35f ) );
+      ui_outline( ctx, panel, 1, GUI_COL_NORM, 0 );
+      ui_rect_pad( panel, (ui_px[]){8,8} );
+
+      ui_rect title;
+      ui_split( panel, k_ui_axis_h, 28*2, 0, title, panel );
+      ctx->font = &vgf_default_title;
+      ui_text( ctx, title, "Content is in the full game.", 
+               1, k_ui_align_middle_center, 0 );
+
+      ui_split( panel, k_ui_axis_h, 28, 0, title, panel );
+      ctx->font = &vgf_default_large;
+
+      ui_rect img;
+      ui_split( panel, k_ui_axis_h, 456, 0, img, panel );
+      ui_image( ctx, img, &menu.prem_tex );
+
+      ui_rect end = { panel[0], panel[1] + panel[3] - 48, panel[2], 48 }, a,b;
+      ui_split_ratio( end, k_ui_axis_v, 0.5f, 2, a, b );
+
+      if( menu_button( ctx, a, R == 0, "Store Page" ) )
+      {
+         if( steam_ready )
+            SteamAPI_ISteamFriends_ActivateGameOverlayToStore( 
+                  SteamAPI_SteamFriends(), 2103940, k_EOverlayToStoreFlag_None);
+      }
+
+      if( menu_button( ctx, b, R == 1, "Nah" ) || button_down( k_srbind_mback ) )
+      {
+         audio_lock();
+         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+         audio_unlock();
+         skaterift.activity = k_skaterift_default;
+         return;
+      }
+
+      goto menu_draw;
+   }
+
+   /*                              TOP BAR 
+    * -------------------------------------------------------------------*/
+
+   ctx->font = &vgf_default_title;
+   ui_px height = ctx->font->ch + 16;
+   ui_rect topbar = { 0, 0, vg.window_x, height };
+
+   const char *opts[] = {
+      [k_menu_main_main] = "Menu",
+      [k_menu_main_map]  = "Map",
+      [k_menu_main_settings ] = "Settings",
+      [k_menu_main_guide ] = "Guides"
+   };
+
+   if( mtab )
+   {
+      menu.main_index += mtab;
+
+      if( menu.main_index == -1 )
+         menu.main_index ++;
+
+      if( menu.main_index == VG_ARRAY_LEN(opts) )
+         menu.main_index --;
+
+      audio_lock();
+      audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+      audio_unlock();
+   }
+
+   ui_px x = 0, spacer = 8;
+   for( u32 draw=0; draw<2; draw ++ )
+   {
+      if( vg_input.display_input_method == k_input_method_controller )
+      {
+         if( draw )
+         {
+            ui_rect inf_lb = { x, 0, 32, height };
+            ui_fill( ctx, inf_lb, lb_down? GUI_COL_NORM: GUI_COL_NORM );
+            ui_text( ctx, inf_lb, "\x91", 1, k_ui_align_middle_center, 0 );
+         }
+         x += 32 + spacer;
+      }
+
+      for( i32 i=0; i<VG_ARRAY_LEN(opts); i ++ )
+      {
+         ui_rect box = { x, 0, ui_text_line_width(ctx, opts[i]) + 32, height };
+
+         if( draw )
+         {
+            enum ui_button_state state = ui_button_base( ctx, box );
+            if( state == k_ui_button_click )
+            {
+               ui_fill( ctx, box, GUI_COL_DARK );
+               menu.main_index = i;
+            }
+            else if( state == k_ui_button_holding_inside )
+            {
+               ui_fill( ctx, box, GUI_COL_DARK );
+            }
+            else if( state == k_ui_button_holding_outside )
+            {
+               ui_fill( ctx, box, GUI_COL_DARK );
+               ui_outline( ctx, box, 1, GUI_COL_CLICK, 0 );
+            }
+            else if( state == k_ui_button_hover )
+            {
+               ui_fill( ctx, box, GUI_COL_NORM );
+               ui_outline( ctx, box, 1, GUI_COL_ACTIVE, 0 );
+            }
+            else 
+               ui_fill( ctx, box, 
+                        i==menu.main_index? GUI_COL_ACTIVE: GUI_COL_NORM );
+
+            ui_text( ctx, box, opts[i], 1, k_ui_align_middle_center, 0 );
+         }
+
+         x += box[2] + spacer;
+      }
+
+      if( vg_input.display_input_method == k_input_method_controller )
+      {
+         if( draw )
+         {
+            ui_rect inf_rb = { x, 0, 32, height };
+            ui_fill( ctx, inf_rb, rb_down? GUI_COL_NORM: GUI_COL_NORM );
+            ui_text( ctx, inf_rb, "\x92", 1, k_ui_align_middle_center, 0 );
+         }
+         x += 32;
+      }
+
+      if( draw )
+         ui_fill( ctx,
+                  (ui_rect){ x+8,0, vg.window_x-(x+8),height }, GUI_COL_NORM );
+      else
+      {
+         x = vg.window_x/2 - x/2;
+         ui_fill( ctx,
+                  (ui_rect){ 0, 0, x-8, height }, GUI_COL_NORM );
+      }
+   }
+
+   if( menu.main_index == k_menu_main_map )
+   {
+      menu.bg_blur = 0;
+      ctx->font = &vgf_default_large;
+      ui_rect title = { vg.window_x/2- 512/2, height+8, 512, 64 };
+
+      ui_px x = 8,
+            y = height+8;
+
+      struct ui_vert *vs = 
+         ui_fill( ctx, (ui_rect){ x,y, 0,height }, 
+               world_static.active_instance? GUI_COL_DARK: GUI_COL_ACTIVE );
+
+      char buf[64];
+      vg_str str;
+      vg_strnull( &str, buf, sizeof(buf) );
+      vg_strcat( &str, "Hub World" );
+      
+      if( world_static.active_instance )
+      {
+         vg_strcat( &str, " (" );
+         vg_input_string( &str, input_button_list[k_srbind_mhub], 1 );
+         vg_strcatch( &str, '\x06' );
+         vg_strcatch( &str, '\x00' );
+         vg_strcat( &str, "\x1B[0m)" );
+      }
+      
+      ui_px w = ui_text( ctx, (ui_rect){ x+8, y, 1000, height }, buf, 1,
+                         k_ui_align_middle_left, 0 );
+      w *= ctx->font->sx;
+      x += w + 16;
+
+      vs[1].co[0] = x + 8;
+      vs[2].co[0] = x;
+
+      x += 2;
+
+      world_instance *world = &world_static.instances[1];
+      if( world->status == k_world_status_loaded )
+      {
+         const char *world_name = 
+            mdl_pstr( &world->meta, world->info.pstr_name );
+
+         vg_strnull( &str, buf, sizeof(buf) );
+         vg_strcat( &str, world_name );
+
+         if( !world_static.active_instance &&
+               (vg_input.display_input_method == k_input_method_controller) )
+         {
+            vg_strcat( &str, " (" );
+            vg_input_string( &str, input_button_list[k_srbind_mhub], 1 );
+            vg_strcatch( &str, '\x06' );
+            vg_strcatch( &str, '\x00' );
+            vg_strcat( &str, "\x1B[0m)" );
+         }
+
+         vs = ui_fill( ctx, (ui_rect){ x,y, 1000,height }, 
+               world_static.active_instance? GUI_COL_ACTIVE: GUI_COL_DARK );
+         w = ui_text( ctx, (ui_rect){ x+16,y, 1000,height }, buf, 
+                      1, k_ui_align_middle_left, 0 );
+
+         w = w*ctx->font->sx + 8*3;
+         x += w;
+
+         if( button_down( k_srbind_mhub ) ||
+            ui_button_base( ctx, (ui_rect){0,y,x,height} ) == k_ui_button_click )
+         {
+            world_switch_instance( world_static.active_instance ^ 0x1 );
+            skaterift.activity = k_skaterift_default;
+            world_map.view_ready = 0;
+         }
+
+         vs[0].co[0] += 8;
+         vs[1].co[0] = x + 8;
+         vs[2].co[0] = x;
+      }
+
+      x = 8;
+      y += 8 + height;
+
+      if( world_static.active_instance )
+      {
+         ui_rect stat_panel = { x,y, 256,vg.window_y-y };
+         u32 c0 = ui_opacity( GUI_COL_DARK, 0.36f );
+         struct ui_vert *vs = ui_fill( ctx, stat_panel, c0 );
+
+         ui_rect_pad( stat_panel, (ui_px[2]){8,0} );
+
+         for( u32 i=0; i<mdl_arrcount( &world->ent_region ); i ++ )
+         {
+            ent_region *region = mdl_arritm( &world->ent_region, i );
+
+            if( !region->zone_volume )
+               continue;
+
+            const char *title = mdl_pstr( &world->meta, region->pstr_title );
+            ctx->font = &vgf_default_large;
+
+            ui_rect title_box;
+            menu_standard_widget( ctx, stat_panel, title_box, 1 );
+            
+            stat_panel[0] += 16;
+            stat_panel[2] -= 16;
+            ctx->font = &vgf_default_small;
+
+            ent_volume *volume = mdl_arritm(&world->ent_volume,
+                  mdl_entity_id_id(region->zone_volume));
+
+            u32 combined = k_ent_route_flag_achieve_gold | 
+                           k_ent_route_flag_achieve_silver;
+
+            char buf[128];
+            vg_str str;
+
+            for( u32 j=0; j<mdl_arrcount(&world->ent_route); j ++ )
+            {
+               ent_route *route = mdl_arritm(&world->ent_route, j );
+
+               v3f local;
+               m4x3_mulv( volume->to_local, route->board_transform[3], local );
+               if( !((fabsf(local[0]) <= 1.0f) &&
+                     (fabsf(local[1]) <= 1.0f) &&
+                     (fabsf(local[2]) <= 1.0f)) )
+               {
+                  continue;
+               }
+
+               combined &= route->flags;
+
+               vg_strnull( &str, buf, sizeof(buf) );
+               vg_strcat( &str, "(Race) " );
+               vg_strcat( &str, mdl_pstr(&world->meta, route->pstr_name));
+
+               if( route->flags & k_ent_route_flag_achieve_silver )
+                  vg_strcat( &str, " \xb3");
+               if( route->flags & k_ent_route_flag_achieve_gold )
+                  vg_strcat( &str, "\xb3");
+
+               ui_rect r;
+               ui_standard_widget( ctx, stat_panel, r, 1 );
+               ui_text( ctx, r, buf, 1, k_ui_align_middle_left, 
+                        medal_colour( ctx, route->flags ) );
+            }
+
+            for( u32 j=0; j<mdl_arrcount(&world->ent_challenge); j ++ )
+            {
+               ent_challenge *challenge = mdl_arritm( &world->ent_challenge, j );
+
+               v3f local;
+               m4x3_mulv( volume->to_local, challenge->transform.co, local );
+               if( !((fabsf(local[0]) <= 1.0f) &&
+                     (fabsf(local[1]) <= 1.0f) &&
+                     (fabsf(local[2]) <= 1.0f)) )
+               {
+                  continue;
+               }
+
+               vg_strnull( &str, buf, sizeof(buf) );
+               vg_strcat( &str, mdl_pstr(&world->meta, challenge->pstr_alias));
+
+               u32 flags = 0x00;
+               if( challenge->status )
+               {
+                  flags |= k_ent_route_flag_achieve_gold;
+                  flags |= k_ent_route_flag_achieve_silver;
+                  vg_strcat( &str, " \xb3\xb3" );
+               }
+
+               combined &= flags;
+
+               ui_rect r;
+               ui_standard_widget( ctx, stat_panel, r, 1 );
+               ui_text( ctx, r, buf, 1, 
+                        k_ui_align_middle_left, medal_colour( ctx, flags ) );
+            }
+
+            stat_panel[0] -= 16;
+            stat_panel[2] += 16;
+
+            u32 title_col = 0;
+            if( combined & k_ent_route_flag_achieve_gold ) 
+               title_col = ui_colour( ctx, k_ui_yellow );
+            else if( combined & k_ent_route_flag_achieve_silver )
+               title_col = ui_colour( ctx, k_ui_fg );
+
+            menu_heading( ctx, title_box, title, title_col );
+         }
+
+         vs[2].co[1] = stat_panel[1];
+         vs[3].co[1] = stat_panel[1];
+      }
+   }
+   else
+   {
+      if( button_down( k_srbind_mback ) )
+      {
+         audio_lock();
+         audio_oneshot( &audio_ui[3], 1.0f, 0.0f );
+         audio_unlock();
+         skaterift.activity = k_skaterift_default;
+         return;
+      }
+
+      ui_rect list0 = { vg.window_x/2 - 512/2, height+32, 
+                        512, vg.window_y-height-64 }, list;
+      rect_copy( list0, list );
+      ui_rect_pad( list, (ui_px[2]){8,8} );
+
+      /*                              MAIN / MAIN
+       * -------------------------------------------------------------------*/
+
+      if( menu.main_index == k_menu_main_main )
+      {
+         i32 R = menu_nav( &menu.main_row, mv, 2 );
+
+         if( menu_button( ctx, list, R == 0, "Resume" ) )
+         {
+            skaterift.activity = k_skaterift_default;
+            return;
+         }
+
+         if( menu_button( ctx, list, R == 1, "Credits" ) )
+         {
+            menu.page = k_menu_page_credits;
+         }
+
+         ui_rect end = { list[0], list[1]+list[3]-64, list[2], 72 };
+         if( menu_button( ctx, end, R == 2, "Quit Game" ) )
+         {
+            vg.window_should_close = 1;
+         }
+      }
+      else if( menu.main_index == k_menu_main_settings )
+      {
+         ui_fill( ctx, list0, ui_opacity( GUI_COL_DARK, 0.36f ) );
+         ui_outline( ctx, list0, 1, GUI_COL_NORM, 0 );
+         i32 R = menu_nav( &menu.settings_row, mv, 8 );
+
+         ctx->font = &vgf_default_large;
+         list[1] -= 8;
+         menu_heading( ctx, list, "Game", 0 );
+         menu_checkbox( ctx, list, R == 0, "Show controls overlay",
+               &control_overlay.enabled );
+         menu_checkbox( ctx, list, R == 1, "Auto connect to global server",
+               &network_client.auto_connect );
+
+         menu_heading( ctx, list, "Audio/Video", 0 );
+         menu_slider( ctx, list, R == 2, "Volume", 0, 100, 
+                      &vg_audio.external_global_volume, "%.f%%" );
+         menu_slider( ctx, list, R == 3, "Resolution", 0, 100,
+                      &k_render_scale, "%.f%%" );
+         menu_checkbox( ctx, list, R == 4, "Motion Blur", &k_blur_effect );
+
+         menu_heading( ctx, list, "Camera", 0 );
+         menu_slider( ctx, list, R == 5, "Fov", 97, 135,
+                      &k_fov, "%.1f\xb0" );
+         menu_slider( ctx, list, R == 6, "Cam Height", -0.4f, +1.4f,
+                      &k_cam_height, vg_lerpf(-0.4f,1.4f,k_cam_height)>=0.0f? 
+                                      "+%.2fm": "%.2fm" );
+         menu_checkbox( ctx, list, R == 7, "Invert Y Axis", &k_invert_y );
+
+
+         ui_rect end = { list[0], list[1]+list[3]-64, list[2], 72 };
+         ctx->font = &vgf_default_small;
+         menu_heading( ctx, end, "Advanced", 0 );
+         if( menu_button( ctx, end, R == 8, "Open Engine Settings" ) )
+         {
+            vg_settings_open();
+         }
+      }
+      else if( menu.main_index == k_menu_main_guide )
+      {
+         list0[0] = 8;
+         list[0] = 16;
+
+         ui_px w = 700,
+               marg = list0[0]+list0[2],
+               pw = vg.window_x - marg,
+               infx = pw/2 - w/2 + marg;
+         ui_rect inf = { infx, height +32, w, 800 };
+
+         struct ui_vert *vs = NULL;
+
+         if( menu.guide_sel && (menu.guide_sel <= 3) )
+         {
+            vs = ui_fill( ctx, inf, ui_opacity( GUI_COL_DARK, 0.20f ) );
+            ui_rect_pad( inf, (ui_px[]){8,8} );
+         }
+
+         ui_fill( ctx, list0, ui_opacity( GUI_COL_DARK, 0.36f ) );
+         ui_outline( ctx, list0, 1, GUI_COL_NORM, 0 );
+         i32 R = menu_nav( &menu.guides_row, mv, 6 );
+
+         ctx->font = &vgf_default_large;
+         list[1] -= 8;
+         menu_heading( ctx, list, "Info", 0 );
+         if( menu.guide_sel == 1 ) 
+         {
+            menu_try_find_cam( 1 );
+
+            ui_rect title;
+            ui_split( inf, k_ui_axis_h, 28*2, 0, title, inf );
+            ctx->font = &vgf_default_title;
+            ui_text( ctx, 
+                     title, "Where to go", 1, k_ui_align_middle_center, 0 );
+
+            ui_split( inf, k_ui_axis_h, 28, 0, title, inf );
+            ctx->font = &vgf_default_large;
+            ui_text( ctx, inf, 
+                  "Visit the sandcastles built by John Cockroach to be\n"
+                  "transported into the real location. Use the map in the\n"
+                  "menu to return back this hub island.\n"
+                  "\n"
+                  "You can begin training at the Volcano Island, where you\n"
+                  "can learn movement skills. Then travel to Mt.Zero Island\n"
+                  "or Bort Downtown to test them out. Finally the most\n"
+                  "challenging course can be taken on at the Valley.\n"
+                  "\n"
+                  "Also visit the Steam Workshop for community made\n"
+                  "locations to skate!"
+                  , 1, k_ui_align_left, 0 );
+
+            vs[2].co[1] = vs[0].co[1] + 84 + vgf_default_large.sy*11 + 16;
+            vs[3].co[1] = vs[2].co[1];
+         }
+         if( menu_button( ctx, list, R == 0, "Where to go" ) ) 
+            menu.guide_sel = 1;
+
+         if( menu.guide_sel == 3 )
+         {
+            menu_try_find_cam( 2 );
+
+            ui_rect title;
+            ui_split( inf, k_ui_axis_h, 28*2, 0, title, inf );
+            ctx->font = &vgf_default_title;
+            ui_text( ctx, title, "Online", 1, k_ui_align_middle_center, 0 );
+
+            ui_split( inf, k_ui_axis_h, 28, 0, title, inf );
+            ctx->font = &vgf_default_large;
+            ui_text( ctx, inf, 
+                  "Connection to the global server is managed by this radar\n"
+                  "dish in the hub island. Come back here to turn the\n"
+                  "connection on or off.\n"
+                  "\n"
+                  "You'll then see other players or your friends if you go\n"
+                  "to the same location, and your highscores will be saved\n"
+                  "onto the scoreboards."
+                  , 1, k_ui_align_left, 0 );
+
+            vs[2].co[1] = vs[0].co[1] + 84 + vgf_default_large.sy*7 + 16;
+            vs[3].co[1] = vs[2].co[1];
+         }
+         if( menu_button( ctx, list, R == 1, "Playing Online" ) ) 
+            menu.guide_sel = 3;
+
+         menu_heading( ctx, list, "Controls", 0 );
+         if( menu_button( ctx, list, R == 2, "Skating \xb2" ) )
+         {
+            menu.guide_sel = 0;
+            menu_link_modal( 
+                  "https://skaterift.com/index.php?page=movement" );
+         }
+         if( menu.guide_sel == 0 || menu.guide_sel > 3 ) menu_try_find_cam( 3 );
+
+         if( menu_button( ctx, list, R == 3, "Tricks \xb2" ) )
+         {
+            menu.guide_sel = 0;
+            menu_link_modal( 
+                  "https://skaterift.com/index.php?page=tricks" );
+         }
+
+         menu_heading( ctx, list, "Workshop", 0 );
+         if( menu_button( ctx, list, R == 4, "Create a Board \xb2" ) )
+         {
+            menu.guide_sel = 0;
+            menu_link_modal( 
+                  "https://skaterift.com/index.php?page=workshop_board" );
+         }
+         if( menu_button( ctx, list, R == 5, "Create a World \xb2" ) )
+         {
+            menu.guide_sel = 0;
+            menu_link_modal( 
+                  "https://skaterift.com/index.php?page=workshop_world" );
+         }
+         if( menu_button( ctx, list, R == 6, "Create a Playermodel \xb2" ) )
+         {
+            menu.guide_sel = 0;
+            menu_link_modal( 
+                  "https://skaterift.com/index.php?page=workshop_player" );
+         }
+      }
+   }
+
+menu_draw:
+
+   vg_ui.frosting = 0.015f;
+   ui_flush( ctx, k_ui_shader_colour, NULL );
+   vg_ui.frosting = 0.0f;
+   ctx->font = &vgf_default_small;
+}
diff --git a/src/menu.h b/src/menu.h
new file mode 100644 (file)
index 0000000..5a6dbc4
--- /dev/null
@@ -0,0 +1,56 @@
+#pragma once
+
+#define MENU_STACK_SIZE 8
+
+#include "vg/vg_engine.h"
+#include "entity.h"
+
+enum menu_page
+{
+   k_menu_page_any,
+   k_menu_page_starter,
+   k_menu_page_premium,
+   k_menu_page_main,
+   k_menu_page_credits,
+   k_menu_page_help,
+};
+
+enum menu_main_subpage
+{
+   k_menu_main_main = 0,
+   k_menu_main_map  = 1,
+   k_menu_main_settings = 2,
+   k_menu_main_guide = 3
+};
+
+struct global_menu
+{
+   int disable_open;
+   i32 skip_starter;
+   enum menu_page page;
+   i32 main_index, 
+       main_row,
+       settings_row,
+       guides_row,
+       intro_row,
+       guide_sel,
+       prem_row;
+   f32 mouse_dist;  /* used for waking up mouse */
+
+   f32 repeater;
+
+   bool bg_blur;
+   ent_camera *bg_cam;
+
+   const char *web_link;   /* if set; modal */
+   i32 web_choice;
+
+   GLuint prem_tex;
+}
+extern menu;
+
+void menu_init(void);
+void menu_at_begin(void);
+void menu_gui( ui_context *ctx );
+void menu_open( enum menu_page page );
+bool menu_viewing_map(void);
diff --git a/src/model.c b/src/model.c
new file mode 100644 (file)
index 0000000..f3f7ab2
--- /dev/null
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+
+#include "vg/vg_io.h"
+
+#ifdef VG_3D
+#include "vg/vg_async.h"
+#include "vg/vg_tex.h"
+#endif
+
+#include "vg/vg_msg.h"
+#include "vg/vg_string.h"
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include "model.h"
+#include "shader_props.h"
+
+static void mdl_load_fatal_corrupt( mdl_context *mdl )
+{
+   vg_fatal_condition();
+   vg_file_error_info( mdl->file );
+   fclose( mdl->file );
+   vg_fatal_exit();
+}
+
+/*
+ * Model implementation
+ */
+
+void mdl_fread_pack_file( mdl_context *mdl, mdl_file *info, void *dst )
+{
+   if( !info->pack_size )
+   {
+      vg_fatal_condition();
+      vg_info( "Packed file is only a header; it is not packed" );
+      vg_info( "path: %s\n", mdl_pstr( mdl, info->pstr_path ) );
+      vg_fatal_exit();
+   }
+
+   fseek( mdl->file, mdl->pack_base_offset+info->pack_offset, SEEK_SET );
+   u64 l = fread( dst, info->pack_size, 1, mdl->file );
+
+   if( l != 1 ) mdl_load_fatal_corrupt( mdl );
+}
+
+/* TODO: Rename these */
+static void mdl_load_array_file_buffer( mdl_context *mdl, mdl_array *arr, 
+                                        void *buffer, u32 stride )
+{
+   if( arr->item_count )
+   {
+      fseek( mdl->file, arr->file_offset, SEEK_SET );
+
+      if( stride == arr->item_size )
+      {
+         u64 l = fread( buffer, arr->item_size*arr->item_count, 1, mdl->file );
+         if( l != 1 ) mdl_load_fatal_corrupt( mdl );
+      }
+      else 
+      {
+         vg_warn( "Applying alignment fixup to array @%p [%u -> %u] x %u\n", 
+                  buffer, arr->item_size, stride, arr->item_count );
+
+         if( stride > arr->item_size )
+            memset( buffer, 0, stride*arr->item_count );
+
+         u32 read_size = VG_MIN( stride, arr->item_size );
+
+         for( u32 i=0; i<arr->item_count; i++ )
+         {
+            u64 l = fread( buffer+i*stride, read_size, 1, mdl->file );
+            if( stride < arr->item_size )
+               fseek( mdl->file, arr->item_size-stride, SEEK_CUR );
+
+            if( l != 1 ) mdl_load_fatal_corrupt( mdl );
+         }
+      }
+   }
+}
+
+static void mdl_load_array_file( mdl_context *mdl, mdl_array_ptr *ptr, 
+                                 mdl_array *arr, void *lin_alloc, u32 stride )
+{
+   if( arr->item_count )
+   {
+      u32 size = stride*arr->item_count;
+      ptr->data = lin_alloc? vg_linear_alloc( lin_alloc, vg_align8(size) ):
+                             malloc( size );
+      mdl_load_array_file_buffer( mdl, arr, ptr->data, stride );
+   }
+   else
+   {
+      ptr->data = NULL;
+   }
+   
+   ptr->stride = stride;
+   ptr->count = arr->item_count;
+}
+
+void *mdl_arritm( mdl_array_ptr *arr, u32 index )
+{
+   return ((u8 *)arr->data) + index*arr->stride;
+}
+
+u32 mdl_arrcount( mdl_array_ptr *arr )
+{
+   return arr->count;
+}
+
+static mdl_array *mdl_find_array( mdl_context *mdl, const char *name )
+{
+   for( u32 i=0; i<mdl_arrcount(&mdl->index); i++ )
+   {
+      mdl_array *arr = mdl_arritm( &mdl->index, i );
+      
+      if( !strncmp(arr->name,name,16) )
+         return arr;
+   }
+
+   return NULL;
+}
+
+int _mdl_load_array( mdl_context *mdl, mdl_array_ptr *ptr,
+                     const char *name, void *lin_alloc, u32 stride )
+{
+   mdl_array *arr = mdl_find_array( mdl, name );
+
+   if( arr )
+   {
+      mdl_load_array_file( mdl, ptr, arr, lin_alloc, stride );
+      return 1;
+   }
+   else
+   {
+      ptr->data = NULL;
+      ptr->count = 0;
+      ptr->stride = 0;
+      return 0;
+   }
+}
+
+int mdl_load_mesh_block( mdl_context *mdl, void *lin_alloc )
+{
+   int success = 1;
+
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->verts,    mdl_vert,   lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->indices,  mdl_indice, lin_alloc );
+
+   return success;
+}
+
+int mdl_load_metadata_block( mdl_context *mdl, void *lin_alloc )
+{
+   int success = 1;
+
+   success &= _mdl_load_array( mdl, &mdl->strings, "strings", lin_alloc, 1 );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->meshs,     mdl_mesh,     lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->submeshs,  mdl_submesh,  lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->textures,  mdl_texture,  lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->armatures, mdl_armature, lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->bones,     mdl_bone,     lin_alloc );
+   success &= MDL_LOAD_ARRAY( mdl, &mdl->animations,mdl_animation,lin_alloc );
+
+   success &= mdl_load_materials( mdl, lin_alloc );
+
+   return success;
+}
+
+int mdl_load_animation_block( mdl_context *mdl, void *lin_alloc )
+{
+   return MDL_LOAD_ARRAY( mdl, &mdl->keyframes, mdl_keyframe, lin_alloc );
+}
+
+void *mdl_shader_standard( vg_msg *msg, void *alloc )
+{
+   struct shader_props_standard *props = 
+      vg_linear_alloc( alloc, sizeof(struct shader_props_standard) );
+
+   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
+                     NULL );
+
+   return props;
+}
+
+void *mdl_shader_terrain( vg_msg *msg, void *alloc )
+{
+   struct shader_props_terrain *props = 
+      vg_linear_alloc( alloc, sizeof(struct shader_props_terrain) );
+
+   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
+                     NULL );
+   vg_msg_getkvvecf( msg, "sand_colour", k_vg_msg_v4f, 
+                     props->sand_colour, (v4f){ 0.79, 0.63, 0.48, 1.0 } );
+   vg_msg_getkvvecf( msg, "blend_offset", k_vg_msg_v2f,
+                     props->blend_offset, (v2f){ 0.5, 0.0 } );
+
+   return props;
+}
+
+void *mdl_shader_vertex_blend( vg_msg *msg, void *alloc )
+{
+   struct shader_props_vertex_blend *props = 
+      vg_linear_alloc( alloc, sizeof(struct shader_props_vertex_blend) );
+
+   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
+                     NULL );
+   vg_msg_getkvvecf( msg, "blend_offset", k_vg_msg_v2f,
+                     props->blend_offset, (v2f){ 0.5, 0.0 } );
+   return props;
+}
+
+void *mdl_shader_water( vg_msg *msg, void *alloc )
+{
+   struct shader_props_water *props = 
+      vg_linear_alloc( alloc, sizeof(struct shader_props_water) );
+
+   vg_msg_getkvvecf( msg, "shore_colour", k_vg_msg_v4f,
+                     props->shore_colour, (v4f){0.03,0.32,0.61,1.0} );
+   vg_msg_getkvvecf( msg, "deep_colour", k_vg_msg_v4f,
+                     props->deep_colour, (v4f){0.0,0.006,0.03,1.0} );
+   vg_msg_getkvintg( msg, "fog_scale", k_vg_msg_f32, &props->fog_scale,
+                     (f32[]){0.04} );
+   vg_msg_getkvintg( msg, "fresnel", k_vg_msg_f32, &props->fresnel,
+                     (f32[]){5.0} );
+   vg_msg_getkvintg( msg, "water_scale", k_vg_msg_f32, &props->water_sale,
+                     (f32[]){ 0.008 } );
+   vg_msg_getkvvecf( msg, "wave_speed", k_vg_msg_v4f,
+                     props->wave_speed, (v4f){0.008,0.006,0.003,0.03} );
+   return props;
+}
+
+void *mdl_shader_cubemapped( vg_msg *msg, void *alloc )
+{
+   struct shader_props_cubemapped *props = 
+      vg_linear_alloc( alloc, sizeof(struct shader_props_cubemapped) );
+
+   vg_msg_getkvintg( msg, "tex_diffuse", k_vg_msg_u32, &props->tex_diffuse,
+                     NULL );
+   vg_msg_getkvintg( msg, "cubemap_entity", k_vg_msg_u32, 
+                     &props->cubemap_entity, NULL );
+   vg_msg_getkvvecf( msg, "tint", k_vg_msg_v4f,
+                     props->tint, (v4f){1.0,1.0,1.0,1.0} );
+   return props;
+}
+
+bool _mdl_legacy_v105_properties( struct mdl_material_v105 *mat, vg_msg *dst )
+{
+   vg_msg_wkvnum( dst, "tex_diffuse", k_vg_msg_u32, 1, &mat->tex_diffuse );
+
+   if( mat->shader == k_shader_cubemap )
+   {
+      vg_msg_wkvnum( dst, "cubemap", k_vg_msg_u32, 1, &mat->tex_none0 );
+      vg_msg_wkvnum( dst, "tint", k_vg_msg_f32, 4, mat->colour );
+   }
+   else if( mat->shader == k_shader_terrain_blend )
+   {
+      vg_msg_wkvnum( dst, "sand_colour", k_vg_msg_f32, 4, mat->colour );
+      vg_msg_wkvnum( dst, "blend_offset", k_vg_msg_f32, 2, mat->colour1 );
+   }
+   else if( mat->shader == k_shader_standard_vertex_blend )
+   {
+      vg_msg_wkvnum( dst, "blend_offset", k_vg_msg_f32, 2, mat->colour1 );
+   }
+   else if( mat->shader == k_shader_water )
+   {
+      vg_msg_wkvnum( dst, "shore_colour", k_vg_msg_f32, 4, mat->colour );
+      vg_msg_wkvnum( dst, "deep_colour", k_vg_msg_f32, 4, mat->colour1 );
+   }
+
+   return 1;
+}
+
+int mdl_load_materials( mdl_context *mdl, void *lin_alloc )
+{
+   MDL_LOAD_ARRAY( mdl, &mdl->materials, mdl_material, lin_alloc );
+
+#if (MDL_VERSION_MIN <= 105)
+   /* load legacy material data into scratch */
+   mdl_array_ptr legacy_materials;
+   if( mdl->info.version <= 105 )
+   {
+      _mdl_load_array( mdl, &legacy_materials, "mdl_material", vg_mem.scratch,
+                       sizeof(struct mdl_material_v105) );
+   }
+#endif
+
+   mdl_array_ptr data;
+   _mdl_load_array( mdl, &data, "shader_data", vg_mem.scratch, 1 );
+
+   if( !lin_alloc )
+      return 1;
+
+   for( u32 i=0; i<mdl_arrcount(&mdl->materials); i ++ )
+   {
+      mdl_material *mat = mdl_arritm( &mdl->materials, i );
+      vg_msg msg;
+
+#if (MDL_VERSION_MIN <= 105)
+      u8 legacy_buf[512];
+      if( mdl->info.version <= 105 )
+      {
+         vg_msg_init( &msg, legacy_buf, sizeof(legacy_buf) );
+         _mdl_legacy_v105_properties( mdl_arritm( &legacy_materials,i ), &msg );
+         vg_msg_init( &msg, legacy_buf, msg.cur.co );
+      }
+      else
+#endif
+      {
+         vg_msg_init( &msg, data.data + mat->props.kvs.offset, 
+                      mat->props.kvs.size );
+      }
+
+      if( mat->shader == k_shader_standard || 
+          mat->shader == k_shader_standard_cutout || 
+          mat->shader == k_shader_foliage ||
+          mat->shader == k_shader_fxglow )
+      {
+         mat->props.compiled = mdl_shader_standard( &msg, lin_alloc );
+      }
+      else if( mat->shader == k_shader_standard_vertex_blend )
+      {
+         mat->props.compiled = mdl_shader_vertex_blend( &msg, lin_alloc );
+      }
+      else if( mat->shader == k_shader_cubemap )
+      {
+         mat->props.compiled = mdl_shader_cubemapped( &msg, lin_alloc );
+      }
+      else if( mat->shader == k_shader_terrain_blend )
+      {
+         mat->props.compiled = mdl_shader_terrain( &msg, lin_alloc );
+      }
+      else if( mat->shader == k_shader_water )
+      {
+         mat->props.compiled = mdl_shader_water( &msg, lin_alloc );
+      }
+      else
+         mat->props.compiled = NULL;
+   }
+
+   return 1;
+}
+
+/*
+ * if calling mdl_open, and the file does not exist, the game will fatal quit
+ */
+void mdl_open( mdl_context *mdl, const char *path, void *lin_alloc )
+{
+   memset( mdl, 0, sizeof( mdl_context ) );
+   mdl->file = fopen( path, "rb" );
+
+   if( !mdl->file )
+   {
+      vg_fatal_condition();
+      vg_info( "mdl_open('%s'): %s\n", path, strerror(errno) );
+      vg_fatal_exit();
+   }
+
+   u64 l = fread( &mdl->info, sizeof(mdl_header), 1, mdl->file );
+   if( l != 1 )
+      mdl_load_fatal_corrupt( mdl );
+
+   if( mdl->info.version < MDL_VERSION_MIN )
+   {
+      vg_fatal_condition();
+      vg_info( "Legacy model version incompatable" );
+      vg_info( "For model: %s\n", path );
+      vg_info( "  version: %u (min: %u, current: %u)\n", 
+               mdl->info.version, MDL_VERSION_MIN, MDL_VERSION_NR );
+      vg_fatal_exit();
+   }
+
+   mdl_load_array_file( mdl, &mdl->index, &mdl->info.index, lin_alloc,
+                        sizeof(mdl_array) );
+
+   mdl_array *pack = mdl_find_array( mdl, "pack" );
+   if( pack ) mdl->pack_base_offset = pack->file_offset;
+   else mdl->pack_base_offset = 0;
+}
+
+/*
+ * close file handle
+ */
+void mdl_close( mdl_context *mdl )
+{
+   fclose( mdl->file );
+   mdl->file = NULL;
+}
+
+/* useful things you can do with the model */
+
+void mdl_transform_m4x3( mdl_transform *transform, m4x3f mtx )
+{
+   q_m3x3( transform->q, mtx );
+   v3_muls( mtx[0], transform->s[0], mtx[0] );
+   v3_muls( mtx[1], transform->s[1], mtx[1] );
+   v3_muls( mtx[2], transform->s[2], mtx[2] );
+   v3_copy( transform->co, mtx[3] );
+}
+
+const char *mdl_pstr( mdl_context *mdl, u32 pstr )
+{
+   return ((char *)mdl_arritm( &mdl->strings, pstr )) + 4;
+}
+
+
+int mdl_pstreq( mdl_context *mdl, u32 pstr, const char *str, u32 djb2 )
+{
+   u32 hash = *((u32 *)mdl_arritm( &mdl->strings, pstr ));
+   if( hash == djb2 ){
+      if( !strcmp( str, mdl_pstr( mdl, pstr ))) return 1;
+      else return 0;
+   }
+   else return 0;
+}
+
+/*
+ * Simple mesh interface for OpenGL
+ * ----------------------------------------------------------------------------
+ */
+
+#ifdef VG_3D
+static void mesh_upload( glmesh *mesh,
+                            mdl_vert *verts, u32 vert_count,
+                            u32 *indices, u32 indice_count )
+{
+   glGenVertexArrays( 1, &mesh->vao );
+   glGenBuffers( 1, &mesh->vbo );
+   glGenBuffers( 1, &mesh->ebo );
+   glBindVertexArray( mesh->vao );
+
+   size_t stride = sizeof(mdl_vert);
+
+   glBindBuffer( GL_ARRAY_BUFFER, mesh->vbo );
+   glBufferData( GL_ARRAY_BUFFER, vert_count*stride, verts, GL_STATIC_DRAW );
+
+   glBindVertexArray( mesh->vao );
+   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh->ebo );
+   glBufferData( GL_ELEMENT_ARRAY_BUFFER, indice_count*sizeof(u32),
+         indices, GL_STATIC_DRAW );
+   
+   /* 0: coordinates */
+   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
+   glEnableVertexAttribArray( 0 );
+
+   /* 1: normal */
+   glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 
+         stride, (void *)offsetof(mdl_vert, norm) );
+   glEnableVertexAttribArray( 1 );
+
+   /* 2: uv */
+   glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 
+         stride, (void *)offsetof(mdl_vert, uv) );
+   glEnableVertexAttribArray( 2 );
+
+   /* 3: colour */
+   glVertexAttribPointer( 3, 4, GL_UNSIGNED_BYTE, GL_TRUE, 
+         stride, (void *)offsetof(mdl_vert, colour) );
+   glEnableVertexAttribArray( 3 );
+
+   /* 4: weights */
+   glVertexAttribPointer( 4, 4, GL_UNSIGNED_SHORT, GL_TRUE, 
+         stride, (void *)offsetof(mdl_vert, weights) );
+   glEnableVertexAttribArray( 4 );
+
+   /* 5: groups */
+   glVertexAttribIPointer( 5, 4, GL_UNSIGNED_BYTE,
+         stride, (void *)offsetof(mdl_vert, groups) );
+   glEnableVertexAttribArray( 5 );
+
+   mesh->indice_count = indice_count;
+   mesh->loaded = 1;
+}
+
+void mesh_bind( glmesh *mesh )
+{
+   glBindVertexArray( mesh->vao );
+}
+
+void mesh_drawn( u32 start, u32 count )
+{
+   glDrawElements( GL_TRIANGLES, count, GL_UNSIGNED_INT, 
+         (void *)(start*sizeof(u32)) );
+}
+
+void mesh_draw( glmesh *mesh )
+{
+   mesh_drawn( 0, mesh->indice_count );
+}
+
+void mesh_free( glmesh *mesh )
+{
+   if( mesh->loaded )
+   {
+      glDeleteVertexArrays( 1, &mesh->vao );
+      glDeleteBuffers( 1, &mesh->ebo );
+      glDeleteBuffers( 1, &mesh->vbo );
+      mesh->loaded = 0;
+   }
+}
+
+void mdl_draw_submesh( mdl_submesh *sm )
+{
+   mesh_drawn( sm->indice_start, sm->indice_count );
+}
+#endif
+
+mdl_mesh *mdl_find_mesh( mdl_context *mdl, const char *name )
+{
+   for( u32 i=0; i<mdl_arrcount( &mdl->meshs ); i++ )
+   {
+      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
+      if( !strcmp( name, mdl_pstr( mdl, mesh->pstr_name )))
+      {
+         return mesh;
+      }
+   }
+   return NULL;
+}
+
+mdl_submesh *mdl_find_submesh( mdl_context *mdl, const char *mesh_name )
+{
+   mdl_mesh *mesh = mdl_find_mesh( mdl, mesh_name );
+
+   if( !mesh ) return NULL;
+   if( !mesh->submesh_count ) return NULL;
+
+   return mdl_arritm( &mdl->submeshs, mesh->submesh_start );
+}
+
+#ifdef VG_3D
+struct payload_glmesh_load
+{
+   mdl_vert *verts;
+   u32 *indices;
+
+   u32 vertex_count,
+       indice_count;
+
+   glmesh *mesh;
+};
+
+static void _sync_mdl_load_glmesh( void *payload, u32 size )
+{
+   struct payload_glmesh_load *job = payload;
+   mesh_upload( job->mesh, job->verts, job->vertex_count,
+                           job->indices, job->indice_count );
+}
+
+void mdl_async_load_glmesh( mdl_context *mdl, glmesh *mesh, u32 *fixup_table )
+{
+   mdl_array *arr_vertices = mdl_find_array( mdl, "mdl_vert" );
+   mdl_array *arr_indices = mdl_find_array( mdl, "mdl_indice" );
+
+   if( arr_vertices && arr_indices )
+   {
+      u32 size_verts   = vg_align8(sizeof(mdl_vert)*arr_vertices->item_count),
+          size_indices = vg_align8(sizeof(mdl_indice)*arr_indices->item_count),
+          size_hdr     = vg_align8(sizeof(struct payload_glmesh_load)),
+          total        = size_hdr + size_verts + size_indices;
+
+      vg_async_item *call = vg_async_alloc( total );
+      struct payload_glmesh_load *job = call->payload;
+
+      u8 *payload = call->payload;
+
+      job->mesh = mesh;
+      job->verts = (void*)(payload + size_hdr);
+      job->indices = (void*)(payload + size_hdr + size_verts);
+      job->vertex_count = arr_vertices->item_count;
+      job->indice_count = arr_indices->item_count;
+
+      mdl_load_array_file_buffer( mdl, arr_vertices, 
+                                  job->verts, sizeof(mdl_vert) );
+      mdl_load_array_file_buffer( mdl, arr_indices, job->indices, 
+                                  sizeof(mdl_indice) );
+
+      if( fixup_table )
+      {
+         for( u32 i=0; i<job->vertex_count; i ++ )
+         {
+            mdl_vert *vert = &job->verts[i];
+            
+            for( u32 j=0; j<4; j++ )
+            {
+               vert->groups[j] = fixup_table[vert->groups[j]];
+            }
+         }
+      }
+
+      /*
+       * Unpack the indices (if there are meshes)
+       * ---------------------------------------------------------
+       */
+
+      if( mdl_arrcount( &mdl->submeshs ) )
+      {
+         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, 0 );
+         u32 offset = sm->vertex_count;
+
+         for( u32 i=1; i<mdl_arrcount( &mdl->submeshs ); i++ )
+         {
+            mdl_submesh *sm = mdl_arritm( &mdl->submeshs, i );
+            u32 *indices    = job->indices + sm->indice_start;
+
+            for( u32 j=0; j<sm->indice_count; j++ )
+               indices[j] += offset;
+
+            offset += sm->vertex_count;
+         }
+      }
+
+      /* 
+       * Dispatch 
+       * -------------------------
+       */
+
+      vg_async_dispatch( call, _sync_mdl_load_glmesh );
+   }
+   else
+   {
+      vg_fatal_condition();
+      vg_info( "No vertex/indice data in model file\n" );
+      vg_fatal_exit();
+   }
+}
+
+/* uploads the glmesh, and textures. everything is saved into the mdl_context */
+void mdl_async_full_load_std( mdl_context *mdl )
+{
+   mdl_async_load_glmesh( mdl, &mdl->mesh, NULL );
+   
+   for( u32 i=0; i<mdl_arrcount( &mdl->textures ); i ++ )
+   {
+      vg_linear_clear( vg_mem.scratch );
+      mdl_texture *tex = mdl_arritm( &mdl->textures, i );
+
+      void *data = vg_linear_alloc( vg_mem.scratch, tex->file.pack_size );
+      mdl_fread_pack_file( mdl, &tex->file, data );
+
+      vg_tex2d_load_qoi_async( data, tex->file.pack_size,
+                               VG_TEX2D_CLAMP|VG_TEX2D_NEAREST, &tex->glname );
+   }
+}
+#endif
diff --git a/src/model.h b/src/model.h
new file mode 100644 (file)
index 0000000..aade46a
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+
+#define MDL_VERSION_MIN 101
+#define MDL_VERSION_NR 106
+
+enum mdl_shader{
+   k_shader_standard                = 0,
+   k_shader_standard_cutout         = 1,
+   k_shader_terrain_blend           = 2,
+   k_shader_standard_vertex_blend   = 3,
+   k_shader_water                   = 4,
+   k_shader_invisible               = 5,
+   k_shader_boundary                = 6,
+   k_shader_fxglow                  = 7,
+   k_shader_cubemap                 = 8,
+   k_shader_walking                 = 9,
+   k_shader_foliage                 = 10,
+   k_shader_override                = 30000
+};
+
+enum mdl_surface_prop{
+   k_surface_prop_concrete = 0,
+   k_surface_prop_wood     = 1,
+   k_surface_prop_grass    = 2,
+   k_surface_prop_tiles    = 3,
+   k_surface_prop_metal    = 4,
+   k_surface_prop_snow     = 5,
+   k_surface_prop_sand     = 6
+};
+
+enum material_flag{
+   k_material_flag_skate_target     = 0x0001,
+   k_material_flag_collision        = 0x0002,
+   k_material_flag_grow_grass       = 0x0004,
+   k_material_flag_grindable        = 0x0008,
+   k_material_flag_invisible        = 0x0010,
+   k_material_flag_boundary         = 0x0020,
+   k_material_flag_preview_visibile = 0x0040,
+   k_material_flag_walking          = 0x0080,
+
+   k_material_flag_ghosts      =
+      k_material_flag_boundary|
+      k_material_flag_invisible|
+      k_material_flag_walking
+};
+
+#pragma pack(push,1)
+
+/* 48 byte */
+struct mdl_vert
+{
+   v3f co,        /* 3*32 */
+       norm;      /* 3*32 */
+   v2f uv;        /* 2*32 */
+
+   u8  colour[4]; /* 4*8 */
+   u16 weights[4];/* 4*16 */
+   u8  groups[4]; /* 4*8 */
+};
+
+#pragma pack(pop)
+
+typedef u32 mdl_indice;
+
+typedef struct mdl_context mdl_context;
+typedef struct mdl_array_ptr mdl_array_ptr;
+typedef struct mdl_vert mdl_vert;
+typedef struct mdl_transform mdl_transform;
+typedef struct mdl_submesh mdl_submesh;
+typedef struct mdl_material mdl_material;
+typedef struct mdl_bone mdl_bone;
+typedef struct mdl_armature mdl_armature;
+typedef struct mdl_animation mdl_animation;
+typedef struct mdl_transform mdl_keyframe;
+typedef struct mdl_mesh mdl_mesh;
+typedef struct mdl_file mdl_file;
+typedef struct mdl_texture mdl_texture;
+typedef struct mdl_array mdl_array;
+typedef struct mdl_header mdl_header;
+
+typedef struct glmesh glmesh;
+struct glmesh
+{
+   u32 vao, vbo, ebo;
+   u32 indice_count;
+   u32 loaded;
+};
+
+struct mdl_transform
+{
+   v3f co, s;
+   v4f q;
+};
+
+static void transform_identity( mdl_transform *transform )
+{
+   v3_zero( transform->co );
+   q_identity( transform->q );
+   v3_fill( transform->s, 1.0f );
+}
+
+static void mdl_transform_vector( mdl_transform *transform, v3f vec, v3f dest )
+{
+   v3_mul( transform->s, vec, dest );
+   q_mulv( transform->q, dest, dest );
+}
+
+static void mdl_transform_point( mdl_transform *transform, v3f co, v3f dest )
+{
+   mdl_transform_vector( transform, co, dest );
+   v3_add( transform->co, dest, dest );
+}
+
+static void mdl_transform_mul( mdl_transform *a, mdl_transform *b, 
+                               mdl_transform *d )
+{
+   mdl_transform_point( a, b->co, d->co );
+   q_mul( a->q, b->q, d->q );
+   q_normalize( d->q );
+   v3_mul( a->s, b->s, d->s );
+}
+
+struct mdl_file
+{
+   u32 pstr_path,
+       pack_offset,
+       pack_size;
+};
+
+#if (MDL_VERSION_MIN <= 105)
+struct mdl_material_v105
+{
+   u32 pstr_name,
+       shader,
+       flags,
+       surface_prop;
+
+   v4f colour,
+       colour1;
+
+   u32 tex_diffuse, /* Indexes start from 1. 0 if missing. */
+       tex_none0,
+       tex_none1;
+};
+#endif
+
+struct mdl_material
+{
+   u32 pstr_name,
+       shader,
+       flags,
+       surface_prop;
+
+   union 
+   {
+      struct 
+      {
+         u32 offset, size;
+         /* -> vg_msg containing KV properties */
+      }
+      kvs;
+      void *compiled;  /* -> shader specific structure for render */
+   } 
+   props;
+};
+
+struct mdl_bone
+{
+   v3f co, end;
+   u32 parent,
+       collider,
+       ik_target,
+       ik_pole,
+       flags,
+       pstr_name;
+
+   boxf hitbox;
+   v3f conevx, conevy, coneva;
+   float conet;
+};
+
+enum bone_flag
+{
+   k_bone_flag_deform               = 0x00000001,
+   k_bone_flag_ik                   = 0x00000002,
+   k_bone_flag_cone_constraint      = 0x00000004
+};
+
+enum bone_collider
+{
+   k_bone_collider_none = 0,
+   k_bone_collider_box = 1,
+   k_bone_collider_capsule = 2
+};
+         
+struct mdl_armature
+{
+   mdl_transform transform;
+   u32 bone_start,
+       bone_count,
+       anim_start,
+       anim_count;
+};
+
+struct mdl_animation
+{
+   u32 pstr_name,
+       length;
+   float rate;
+   u32 offset;
+};
+
+struct mdl_submesh
+{
+   u32 indice_start,
+       indice_count,
+       vertex_start,
+       vertex_count;
+
+   boxf bbx;
+   u16 material_id, flags;
+};
+
+enum esubmesh_flags
+{
+   k_submesh_flag_none     = 0x0000,
+   k_submesh_flag_consumed = 0x0001
+};
+
+struct mdl_mesh
+{
+   mdl_transform transform;
+   u32 submesh_start,
+       submesh_count,
+       pstr_name,
+       entity_id,    /* upper 16 bits: type, lower 16 bits: index */
+       armature_id;
+};
+
+struct mdl_texture
+{
+   mdl_file file;
+   u32 glname;
+};
+
+struct mdl_array
+{
+   u32 file_offset,
+       item_count,
+       item_size;
+
+   char name[16];
+};
+
+struct mdl_header
+{
+   u32 version;
+   mdl_array index;
+};
+
+struct mdl_context
+{
+   FILE *file;
+   mdl_header info;
+
+   struct mdl_array_ptr
+   {
+      void *data;
+      u32 count, stride;
+   }
+   index,
+
+   /* metadata */
+   strings,
+   meshs,
+   submeshs,
+   materials,
+   textures,
+   armatures,
+   bones,
+   animations,
+
+   /* animation buffers */
+   keyframes,
+
+   /* mesh buffers */
+   verts,
+   indices;
+   u32 pack_base_offset;
+
+   /* runtime */
+   glmesh mesh;
+};
+
+void mesh_bind( glmesh *mesh );
+void mesh_drawn( u32 start, u32 count );
+void mesh_draw( glmesh *mesh );
+void mesh_free( glmesh *mesh );
+
+/* file context management */
+void mdl_open( mdl_context *mdl, const char *path, void *lin_alloc );
+void mdl_close( mdl_context *mdl );
+
+/* array loading */
+int _mdl_load_array( mdl_context *mdl, mdl_array_ptr *ptr,
+                     const char *name, void *lin_alloc, u32 stride );
+#define MDL_LOAD_ARRAY( MDL, PTR, STRUCT, ALLOCATOR ) \
+   _mdl_load_array( MDL, PTR, #STRUCT, ALLOCATOR, sizeof(STRUCT) )
+
+/* array access */
+void *mdl_arritm( mdl_array_ptr *arr, u32 index );
+u32 mdl_arrcount( mdl_array_ptr *arr );
+
+/* pack access */
+void mdl_fread_pack_file( mdl_context *mdl, mdl_file *info, void *dst );
+
+/* standard array groups */
+int mdl_load_animation_block( mdl_context *mdl, void *lin_alloc );
+int mdl_load_metadata_block( mdl_context *mdl, void *lin_alloc );
+int mdl_load_mesh_block( mdl_context *mdl, void *lin_alloc );
+int mdl_load_materials( mdl_context *mdl, void *lin_alloc );
+
+/* load mesh */
+void mdl_async_load_glmesh( mdl_context *mdl, glmesh *mesh, u32 *fixup_table );
+
+/* load textures and mesh */
+void mdl_async_full_load_std( mdl_context *mdl );
+
+/* rendering */
+void mdl_draw_submesh( mdl_submesh *sm );
+mdl_mesh *mdl_find_mesh( mdl_context *mdl, const char *name );
+mdl_submesh *mdl_find_submesh( mdl_context *mdl, const char *mesh_name );
+
+/* pstrs */
+const char *mdl_pstr( mdl_context *mdl, u32 pstr );
+int mdl_pstreq( mdl_context *mdl, u32 pstr, const char *str, u32 djb2 );
+#define MDL_CONST_PSTREQ( MDL, Q, CONSTSTR )\
+   mdl_pstreq( MDL, Q, CONSTSTR, vg_strdjb2( CONSTSTR ) )
+
+void mdl_transform_m4x3( mdl_transform *transform, m4x3f mtx );
+
diff --git a/src/network.c b/src/network.c
new file mode 100644 (file)
index 0000000..0869612
--- /dev/null
@@ -0,0 +1,789 @@
+#include "skaterift.h"
+#include "vg/vg_steam.h"
+#include "vg/vg_steam_networking.h"
+#include "vg/vg_steam_auth.h"
+#include "vg/vg_steam_friends.h"
+#include "player.h"
+#include "network.h"
+#include "network_msg.h"
+#include "network_common.h"
+#include "player_remote.h"
+#include "world.h"
+#include "world_sfd.h"
+#include "world_routes.h"
+#include "vg/vg_ui/imgui.h"
+#include "gui.h"
+#include "ent_region.h"
+#include "vg/vg_loader.h"
+
+#ifdef _WIN32
+ #include <winsock2.h>
+ #include <ws2tcpip.h>
+#else
+ #include <sys/socket.h>
+ #include <sys/types.h>
+ #include <netdb.h>
+#endif
+
+struct network_client network_client =
+{
+   .auth_mode = eServerModeAuthentication,
+   .state = k_ESteamNetworkingConnectionState_None,
+   .last_intent_change = -99999.9
+};
+
+static void scores_update(void);
+
+int packet_minsize( SteamNetworkingMessage_t *msg, u32 size ){
+   if( msg->m_cbSize < size ) {
+      vg_error( "Invalid packet size (must be at least %u)\n", size );
+      return 0;
+   }
+   else{
+      return 1;
+   }
+}
+
+static void on_auth_ticket_recieved( void *result, void *context ){
+   EncryptedAppTicketResponse_t *response = result;
+
+   if( response->m_eResult == k_EResultOK ){
+      vg_info( "  New app ticket ready\n" );
+   }
+   else{
+      vg_warn( "  Could not request new encrypted app ticket (%u)\n",
+                  response->m_eResult );
+   }
+   
+   if( SteamAPI_ISteamUser_GetEncryptedAppTicket( hSteamUser, 
+            network_client.app_symmetric_key,
+            VG_ARRAY_LEN(network_client.app_symmetric_key),
+            &network_client.app_key_length )){
+      vg_success( "  Loaded app ticket\n" );
+   }
+   else{
+      vg_error( "  No ticket availible\n" );
+      network_client.app_key_length = 0;
+   }
+}
+
+static void request_auth_ticket(void){
+   /* 
+    * TODO Check for one thats cached on the disk and load it.
+    * This might be OK though because steam seems to cache the result 
+    */
+
+   vg_info( "Requesting new authorization ticket\n" );
+
+   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
+   call->userdata = NULL;
+   call->p_handler = on_auth_ticket_recieved;
+   call->id = 
+      SteamAPI_ISteamUser_RequestEncryptedAppTicket( hSteamUser, NULL, 0 );
+}
+
+static void network_send_username(void){
+   if( !network_connected() )
+      return;
+
+   netmsg_playerusername *update = alloca( sizeof(netmsg_playerusername)+
+                                           NETWORK_USERNAME_MAX );
+   update->inetmsg_id = k_inetmsg_playerusername;
+   update->index = 0xff;
+
+   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
+   const char *username = SteamAPI_ISteamFriends_GetPersonaName(hSteamFriends);
+   u32 chs = str_utf8_collapse( username, update->name, NETWORK_USERNAME_MAX );
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, network_client.remote, 
+         update, sizeof(netmsg_playerusername)+chs+1,
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+void network_send_region(void)
+{
+   if( !network_connected() )
+      return;
+   
+   netmsg_region *region = alloca( sizeof(netmsg_region) + NETWORK_REGION_MAX );
+
+   region->inetmsg_id = k_inetmsg_region;
+   region->client = 0;
+   region->flags = global_ent_region.flags;
+
+   u32 l = vg_strncpy( global_ent_region.location, region->loc, 
+                       NETWORK_REGION_MAX, k_strncpy_always_add_null );
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, network_client.remote, 
+         region, sizeof(netmsg_region)+l+1,
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+static void network_send_request( netmsg_request *req, vg_msg *body,
+                                  void (*callback)( 
+                                     netmsg_request *res, vg_msg *body, 
+                                     u64 userdata),
+                                  u64 userdata ){
+   u32 len = 0;
+   if( body ){
+      len = body->cur.co;
+      vg_info( "Request scoreboard. Info (%u):\n", body->cur.co );
+      vg_msg_print( body, len );
+
+      if( body->error != k_vg_msg_error_OK ){
+         vg_error( "Body not OK\n" );
+         return;
+      }
+   }
+
+   if( callback ){
+      req->id = vg_pool_lru( &network_client.request_pool );
+      if( req->id ){
+         vg_pool_watch( &network_client.request_pool, req->id );
+         struct network_request *pn = 
+            vg_pool_item( &network_client.request_pool, req->id );
+         pn->callback = callback;
+         pn->sendtime = vg.time_real;
+         pn->userdata = userdata;
+      }
+      else{
+         vg_error( "Unable to send request. Pool is full.\n" );
+         return;
+      }
+   }
+   else
+      req->id = 0;
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, network_client.remote, 
+         req, sizeof(netmsg_request)+len,
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+static void network_scoreboard_callback( netmsg_request *res, vg_msg *body,
+                                         u64 userdata ){
+   world_instance *world = world_current_instance();
+
+   world_routes_recv_scoreboard( world, body, userdata, res->status );
+   if( userdata == world_sfd.active_route_board )
+      world_sfd_compile_active_scores();
+}
+
+
+
+/* mod_uid: world mod uid,
+ * route_uid: run name (just a string)
+ * week: 
+ *   0   ALL TIME
+ *   1   CURRENT WEEK
+ *   2   ALL TIME + CURRENT WEEK
+ *   .
+ *   10+ specific week index
+ */
+void network_request_scoreboard( const char *mod_uid, 
+                                 const char *route_uid,
+                                 u32 week, u64 userdata ){
+   if( !network_connected() ) 
+      return;
+
+   netmsg_request *req = alloca( sizeof(netmsg_request) + 512 );
+   req->inetmsg_id = k_inetmsg_request;
+
+   vg_msg data;
+   vg_msg_init( &data, req->q, 512 );
+   vg_msg_wkvstr( &data, "endpoint", "scoreboard" );
+   vg_msg_wkvstr( &data, "mod", mod_uid );
+   vg_msg_wkvstr( &data, "route", route_uid );
+   vg_msg_wkvnum( &data, "week", k_vg_msg_u32, 1, &week );
+   network_send_request( req, &data, network_scoreboard_callback, userdata );
+}
+
+static void network_publish_callback( netmsg_request *res, vg_msg *body,
+                                      u64 userdata ){
+   if( res->status != k_request_status_ok ){
+      vg_error( "Publish laptime, server error #%d\n", (i32)res->status );
+   }
+}
+
+void network_publish_laptime( const char *mod_uid, 
+                              const char *route_uid, f64 lap_time ){
+   if( !network_connected() )
+      return;
+
+   i32 time_centiseconds = lap_time * 100.0;
+
+   netmsg_request *req = alloca( sizeof(netmsg_request) + 512 );
+   req->inetmsg_id = k_inetmsg_request;
+
+   vg_msg data;
+   vg_msg_init( &data, req->q, 512 );
+   vg_msg_wkvstr( &data, "endpoint", "setlap" );
+   vg_msg_wkvstr( &data, "mod", mod_uid );
+   vg_msg_wkvstr( &data, "route", route_uid );
+   vg_msg_wkvnum( &data, "time", k_vg_msg_i32, 1, &time_centiseconds );
+   network_send_request( req, &data, network_publish_callback, 0 );
+}
+
+static void network_request_rx_300_400( SteamNetworkingMessage_t *msg ){
+   netmsg_blank *tmp = msg->m_pData;
+
+   if( tmp->inetmsg_id == k_inetmsg_request ){
+      
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_response ){
+      netmsg_request *res = (netmsg_request *)msg->m_pData;
+
+      vg_msg *body = NULL;
+
+      vg_msg data;
+      if( res->status == k_request_status_ok ){
+         vg_msg_init( &data, res->q, msg->m_cbSize - sizeof(netmsg_request) );
+         vg_success( "Response to #%d:\n", (i32)res->id );
+         vg_msg_print( &data, data.max );
+         body = &data;
+      }
+      else {
+         vg_warn( "Server response to #%d: %d\n", (i32)res->id, res->status );
+      }
+
+      if( res->id ){
+         struct network_request *pn = 
+            vg_pool_item( &network_client.request_pool, res->id );
+         pn->callback( res, body, pn->userdata );
+         vg_pool_unwatch( &network_client.request_pool, res->id );
+      }
+   }
+}
+
+void network_send_item( enum netmsg_playeritem_type type )
+{
+   if( !network_connected() )
+      return;
+
+   netmsg_playeritem *item = 
+      alloca( sizeof(netmsg_playeritem) + ADDON_UID_MAX );
+   item->inetmsg_id = k_inetmsg_playeritem;
+   item->type_index = type;
+   item->client = 0;
+
+   if( (type == k_netmsg_playeritem_world0) ||
+       (type == k_netmsg_playeritem_world1) ){
+
+      enum world_purpose purpose = type - k_netmsg_playeritem_world0;
+      addon_reg *reg = world_static.instance_addons[ purpose ];
+
+      if( reg )
+         addon_alias_uid( &reg->alias, item->uid );
+      else 
+         item->uid[0] = '\0';
+   }
+   else{
+      u16 view_id = 0;
+      enum addon_type addon_type = k_addon_type_none;
+      if( type == k_netmsg_playeritem_board ){
+         view_id = localplayer.board_view_slot;
+         addon_type = k_addon_type_board;
+      }
+      else if( type == k_netmsg_playeritem_player ){
+         view_id = localplayer.playermodel_view_slot;
+         addon_type = k_addon_type_player;
+      }
+
+      struct addon_cache *cache = &addon_system.cache[addon_type];
+      vg_pool *pool = &cache->pool;
+
+      SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+      addon_cache_entry *entry = vg_pool_item( pool, view_id );
+      addon_alias_uid( &entry->reg_ptr->alias, item->uid );
+      SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+   }
+
+   vg_info( "send equip: [%u] %s\n", 
+            item->type_index, item->uid );
+   u32 chs = strlen(item->uid);
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, network_client.remote, 
+         item, sizeof(netmsg_playeritem)+chs+1,
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+static void network_disconnect(void){
+   SteamAPI_ISteamNetworkingSockets_CloseConnection( 
+         hSteamNetworkingSockets, network_client.remote, 0, NULL, 0 );
+   network_client.remote = 0;
+   network_client.state = k_ESteamNetworkingConnectionState_None;
+
+   for( int i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
+      netplayers.list[i].active = 0;
+   }
+}
+
+void network_status_string( vg_str *str, u32 *colour )
+{
+   if( skaterift.demo_mode ){
+      vg_strcat( str, "Offline" );
+      return;
+   }
+
+   if( steam_ready ){
+      if( network_client.user_intent == k_server_intent_offline ){
+         vg_strcat( str, "Offline" );
+      }
+      else {
+         ESteamNetworkingConnectionState state = network_client.state;
+
+         if( state == k_ESteamNetworkingConnectionState_None )
+            vg_strcat( str, "No Connection" );
+         else if( state == k_ESteamNetworkingConnectionState_Connecting )
+         {
+            vg_strcatf( str, "Connecting...\n%s", network_client.host_adress );
+
+            if( network_client.retries ){
+               vg_strcat( str, "\n(" );
+               vg_strcati32( str, network_client.retries );
+               vg_strcat( str, " retries)" );
+            }
+         }
+         else if( state == k_ESteamNetworkingConnectionState_Connected ){
+            vg_strcatf( str, "Connected to:\n%s", network_client.host_adress );
+            *colour = 0xff00a020;
+         }
+         else if( state == k_ESteamNetworkingConnectionState_ClosedByPeer )
+            vg_strcat( str, "Connection Closed" );
+         else if( state == k_ESteamNetworkingConnectionState_FindingRoute )
+            vg_strcat( str, "Finding Route" );
+         else if( state ==
+               k_ESteamNetworkingConnectionState_ProblemDetectedLocally){
+            vg_strcat( str, "Problem Detected\nLocally" );
+            *colour = 0xff0000a0;
+         }
+         else
+            vg_strcat( str, "???" );
+      }
+   }
+   else {
+      vg_strcat( str, "Steam Offline" );
+      *colour = 0xff0000a0;
+   }
+}
+
+void render_server_status_gui(void)
+{
+   vg_framebuffer_bind( g_render.fb_network_status, 1.0f );
+
+   vg_ui_set_screen( 128, 48 );
+   ui_context *ctx = &vg_ui.ctx;
+
+   /* HACK */
+       ctx->cur_vert = 0;
+       ctx->cur_indice = 0;
+   ctx->vert_start = 0;
+   ctx->indice_start = 0;
+
+   ui_rect r = { 0, 0, 128, 48 };
+   
+   char buf[128];
+   vg_str str;
+   vg_strnull( &str, buf, sizeof(buf) );
+
+   u32 bg = 0xff000000;
+   network_status_string( &str, &bg );
+
+   ui_fill( ctx, r, bg );
+   ui_text( ctx, r, buf, 1, k_ui_align_center, 0 );
+   ui_flush( ctx, k_ui_shader_colour, NULL );
+   
+   skaterift.rt_textures[ k_skaterift_rt_server_status ] =
+      g_render.fb_network_status->attachments[0].id;
+}
+
+static void on_server_connect_status( CallbackMsg_t *msg ){
+   SteamNetConnectionStatusChangedCallback_t *info = (void *)msg->m_pubParam;
+   vg_info( "  Connection status changed for %lu\n", info->m_hConn );
+   vg_info( "  %s -> %s\n", 
+         string_ESteamNetworkingConnectionState(info->m_eOldState),
+         string_ESteamNetworkingConnectionState(info->m_info.m_eState) );
+
+   if( info->m_hConn == network_client.remote ){
+      network_client.state = info->m_info.m_eState;
+
+      if( info->m_info.m_eState == 
+            k_ESteamNetworkingConnectionState_Connected ){
+         vg_success("  Connected to remote server.. authenticating\n");
+
+         /* send version info to server */
+         netmsg_version version;
+         version.inetmsg_id = k_inetmsg_version;
+         version.version = NETWORK_SKATERIFT_VERSION;
+         SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+               hSteamNetworkingSockets, network_client.remote, &version, 
+               sizeof(netmsg_version), k_nSteamNetworkingSend_Reliable, NULL );
+
+         /* TODO: We should really wait to see if the server is in auth mode
+          * first... */
+         u32 size = sizeof(netmsg_auth) + network_client.app_key_length;
+         netmsg_auth *auth = alloca(size);
+         auth->inetmsg_id = k_inetmsg_auth;
+         auth->ticket_length = network_client.app_key_length;
+         for( int i=0; i<network_client.app_key_length; i++ )
+            auth->ticket[i] = network_client.app_symmetric_key[i];
+
+         SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+               hSteamNetworkingSockets, network_client.remote, auth, size,
+               k_nSteamNetworkingSend_Reliable, NULL );
+      }
+      else if( info->m_info.m_eState == 
+            k_ESteamNetworkingConnectionState_ClosedByPeer ){
+
+         if( info->m_info.m_eEndReason == 
+               k_ESteamNetConnectionEnd_Misc_InternalError ){
+            network_client.retries = 40;
+         }
+         network_disconnect();
+      }
+      else if( info->m_info.m_eState == 
+            k_ESteamNetworkingConnectionState_ProblemDetectedLocally ){
+         network_disconnect();
+      }
+   }
+   else{
+      //vg_warn( "  Recieved signal from unknown connection\n" );
+   }
+
+   render_server_status_gui();
+}
+
+static void on_persona_state_change( CallbackMsg_t *msg ){
+   if( !network_connected() )
+      return;
+
+   PersonaStateChange_t *info = (void *)msg->m_pubParam;
+   ISteamUser *hSteamUser = SteamAPI_SteamUser();
+
+   vg_info( "User: %llu, change: %u\n", info->m_ulSteamID, 
+                                        info->m_nChangeFlags );
+
+   if( info->m_ulSteamID == SteamAPI_ISteamUser_GetSteamID(hSteamUser) ){
+      if( info->m_nChangeFlags & k_EPersonaChangeName ){
+         network_send_username();
+      }
+   }
+
+   if( info->m_nChangeFlags & k_EPersonaChangeRelationshipChanged ){
+      for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
+         struct network_player *rp = &netplayers.list[i];
+         if( rp->steamid == info->m_ulSteamID ){
+            player_remote_update_friendflags( rp );
+         }
+      }
+   }
+}
+
+void network_set_host( const char *host_str, const char *port_str )
+{
+   vg_strncpy( host_str, network_client.host_adress, 
+               sizeof(network_client.host_adress), k_strncpy_overflow_fatal );
+
+   memset( &network_client.ip, 0, sizeof(network_client.ip) );
+   network_client.ip_resolved = 0;
+
+   if( port_str )
+   {
+      vg_strncpy( port_str, network_client.host_port, 
+                  sizeof(network_client.host_port), k_strncpy_overflow_fatal );
+   }
+   else
+   {
+      vg_str str;
+      vg_strnull( &str, network_client.host_port, 
+                  sizeof(network_client.host_port) );
+      vg_strcati32( &str, NETWORK_PORT );
+   }
+
+   network_client.ip.m_port = atoi( network_client.host_port );
+}
+
+static void network_connect(void)
+{
+   VG_ASSERT( network_client.ip_resolved );
+
+   vg_info( "connecting...\n" );
+   network_client.remote = SteamAPI_ISteamNetworkingSockets_ConnectByIPAddress( 
+                  hSteamNetworkingSockets, &network_client.ip, 0, NULL );
+}
+
+static void network_sign_on_complete(void){
+   vg_success( "Sign on completed\n" );
+
+   /* send our init info */
+   network_send_username();
+   for( u32 i=0; i<k_netmsg_playeritem_max; i ++ ){
+      network_send_item(i);
+   }
+   network_send_region();
+}
+
+static void poll_remote_connection(void){
+   SteamNetworkingMessage_t *messages[32];
+   int len;
+
+   for( int i=0; i<10; i++ ){
+      len = SteamAPI_ISteamNetworkingSockets_ReceiveMessagesOnConnection(
+            hSteamNetworkingSockets, network_client.remote, 
+            messages, VG_ARRAY_LEN(messages));
+
+      if( len <= 0 )
+         return;
+
+      for( int i=0; i<len; i++ ){
+         SteamNetworkingMessage_t *msg = messages[i];
+
+         if( msg->m_cbSize < sizeof(netmsg_blank) ){
+            vg_warn( "Discarding message (too small: %d)\n", msg->m_cbSize );
+            continue;
+         }
+
+         netmsg_blank *tmp = msg->m_pData;
+
+         if( (tmp->inetmsg_id >= 200) && (tmp->inetmsg_id < 300) ){
+            player_remote_rx_200_300( msg );
+         }
+         else if( (tmp->inetmsg_id >= 300) && (tmp->inetmsg_id < 400) ){
+            network_request_rx_300_400( msg );
+         }
+         else {
+            if( tmp->inetmsg_id == k_inetmsg_version ){
+               netmsg_version *version = msg->m_pData;
+               if( version->version != NETWORK_SKATERIFT_VERSION ){
+                  network_disconnect();
+                  /* we dont want to connect to this server ever */
+                  network_client.retries = 999;
+                  network_client.last_attempt = 999999999.9;
+                  vg_error( "version mismatch with server\n" );
+               }
+               else {
+                  network_client.remote_version = version->version;
+                  network_sign_on_complete();
+               }
+            }
+         }
+
+         SteamAPI_SteamNetworkingMessage_t_Release( msg );
+      }
+   }
+}
+
+static void network_resolve_host_async( void *payload, u32 size )
+{
+   u32 *status = payload;
+   network_client.ip_resolved = *status;
+
+   char buf[256];
+   SteamAPI_SteamNetworkingIPAddr_ToString( &network_client.ip, buf, 256, 1 );
+   vg_info( "Resolved host address to: %s\n", buf );
+}
+
+static void network_resolve_host_thread( void *_ )
+{
+   vg_async_item *call = vg_async_alloc(8);
+   u32 *status = call->payload;
+   *status = 0;
+
+   if( (network_client.host_adress[0] >= '0') && 
+       (network_client.host_adress[0] <= '9') )
+   {
+      SteamAPI_SteamNetworkingIPAddr_ParseString( 
+            &network_client.ip, 
+            network_client.host_adress );
+      network_client.ip.m_port = atoi( network_client.host_port );
+      *status = 1;
+      goto end;
+   }
+
+   vg_info( "Resolving host.. %s (:%s)\n",
+             network_client.host_adress, network_client.host_port );
+
+   struct addrinfo  hints;
+   struct addrinfo  *result;
+
+   /* Obtain address(es) matching host/port. */
+
+   memset( &hints, 0, sizeof(hints) );
+   hints.ai_family = AF_INET6;
+   hints.ai_socktype = SOCK_DGRAM;
+   hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG;
+   hints.ai_protocol = 0;
+
+   int s = getaddrinfo( network_client.host_adress, network_client.host_port, 
+                        &hints, &result);
+   if( s != 0 ) 
+   {
+#ifndef _WIN32
+      vg_error( "getaddrinfo: %s\n", gai_strerror(s) );
+#endif
+
+      if( !strcmp( network_client.host_adress, "skaterift.com" ) )
+      {
+         vg_warn( "getaddrinfo failed for skaterift.com;\n "
+                  "falling back to a hardcoded IPv4\n" );
+         strcpy( network_client.host_adress, "46.101.34.155" );
+         SteamAPI_SteamNetworkingIPAddr_ParseString( 
+               &network_client.ip, 
+               network_client.host_adress );
+         network_client.ip.m_port = NETWORK_PORT;
+         *status = 1;
+      }
+
+      goto end;
+   }
+
+   struct sockaddr_in6 *inaddr = (struct sockaddr_in6 *)result->ai_addr;
+   memcpy( network_client.ip.m_ipv6, &inaddr->sin6_addr, 16 );
+   freeaddrinfo( result );
+
+   *status = 1;
+
+end: vg_async_dispatch( call, network_resolve_host_async );
+}
+
+void network_update(void)
+{
+   if( !steam_ready )
+      return;
+
+   ESteamNetworkingConnectionState state = network_client.state;
+
+   if( network_client.user_intent == k_server_intent_offline )
+   {
+      if( state != k_ESteamNetworkingConnectionState_None )
+         network_disconnect();
+
+      return;
+   }
+
+   if( state == k_ESteamNetworkingConnectionState_Connected )
+   {
+      poll_remote_connection();
+      f64 frame_delta = vg.time_real - network_client.last_frame;
+
+      if( frame_delta > NETWORK_FRAMERATE )
+      {
+         network_client.last_frame = vg.time_real;
+         remote_player_send_playerframe();
+         localplayer.sfx_buffer_count = 0;
+      }
+
+      remote_player_debug_update();
+   }
+   else 
+   {
+      if( (state == k_ESteamNetworkingConnectionState_Connecting) ||
+          (state == k_ESteamNetworkingConnectionState_FindingRoute) )
+      {
+         return;
+      }
+      else 
+      {
+         f64 waited = vg.time_real - network_client.last_attempt,
+             min_wait = 1.0;
+
+         if( network_client.retries > 5 )
+            min_wait = 60.0;
+
+         if( waited < min_wait )
+            return;
+
+         if( !network_client.ip_resolved )
+         {
+            if( vg_loader_availible() )
+            {
+               vg_loader_start( network_resolve_host_thread, NULL );
+            }
+            else return;
+         }
+         else
+            network_connect();
+         
+         network_client.retries ++;
+         network_client.last_attempt = vg.time_real;
+      }
+   }
+}
+
+void chat_send_message( const char *message )
+{
+   if( !network_connected() ){
+      return;
+   }
+
+   netmsg_chat *chat = alloca( sizeof(netmsg_chat) + NETWORK_MAX_CHAT );
+   chat->inetmsg_id = k_inetmsg_chat;
+   chat->client = 0;
+
+   u32 l = vg_strncpy( message, chat->msg, NETWORK_MAX_CHAT, 
+                       k_strncpy_always_add_null );
+
+   SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+         hSteamNetworkingSockets, network_client.remote, 
+         chat, sizeof(netmsg_chat)+l+1,
+         k_nSteamNetworkingSend_Reliable, NULL );
+}
+
+static int cmd_network_send_message( int argc, const char *argv[] ){
+   char buf[ NETWORK_MAX_CHAT ];
+   vg_str str;
+   vg_strnull( &str, buf, NETWORK_MAX_CHAT );
+
+   for( int i=0; i<argc; i ++ ){
+      vg_strcat( &str, argv[i] );
+
+      if( i < argc-1 )
+         vg_strcatch( &str, ' ' );
+   }
+
+   chat_send_message( buf );
+   return 0;
+}
+
+void network_init(void)
+{
+   vg_console_reg_var( "network_info", &network_client.network_info,
+                       k_var_dtype_i32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "auto_connect", &network_client.auto_connect,
+                       k_var_dtype_i32, VG_VAR_PERSISTENT );
+   if( steam_ready ){
+      u32 alloc_size = sizeof(struct network_request)*NETWORK_MAX_REQUESTS;
+      network_client.request_buffer = 
+         vg_linear_alloc( vg_mem.rtmemory, alloc_size );
+      memset( network_client.request_buffer, 0, alloc_size );
+
+      vg_pool *pool = &network_client.request_pool;
+      pool->buffer = network_client.request_buffer;
+      pool->count = NETWORK_MAX_REQUESTS;
+      pool->stride = sizeof( struct network_request );
+      pool->offset = offsetof( struct network_request, poolnode );
+      vg_pool_init( pool );
+
+      steam_register_callback( k_iSteamNetConnectionStatusChangedCallBack,
+                               on_server_connect_status );
+      steam_register_callback( k_iPersonaStateChange, 
+                               on_persona_state_change );
+      request_auth_ticket();
+
+      vg_console_reg_cmd( "say", cmd_network_send_message, NULL );
+   }
+}
+
+void network_end(void)
+{
+   /* TODO: Send buffered highscores that were not already */
+   if( (network_client.state == k_ESteamNetworkingConnectionState_Connected) ||
+       (network_client.state == k_ESteamNetworkingConnectionState_Connecting) )
+   {
+      SteamAPI_ISteamNetworkingSockets_CloseConnection(
+            hSteamNetworkingSockets, network_client.remote, 0, NULL, 1 );
+   }
+}
diff --git a/src/network.h b/src/network.h
new file mode 100644 (file)
index 0000000..6af51af
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ * All trademarks are property of their respective owners
+ */
+
+#pragma once
+#include "vg/vg_platform.h"
+#include "vg/vg_steam_networking.h"
+#include "vg/vg_mem_pool.h"
+#include "vg/vg_msg.h"
+#include "steam.h"
+#include "network_common.h"
+#include "network_msg.h"
+#include "addon_types.h"
+
+#define NETWORK_MAX_REQUESTS 8
+
+/* 
+ * Interface
+ */
+
+/* Call it at start; Connects us to the gameserver */
+void network_init(void);
+
+/* Run this from main loop */
+void network_update(void);
+
+/* Call it at shutdown */
+void network_end(void);
+
+/* 
+ * Can buffer up a bunch of these by calling many times, they will be
+ * sent at the next connection 
+ */
+void network_submit_highscore( u32 trackid, u16 points, u16 time );
+
+/*
+ * Game endpoints are provided with the same names to allow running without a
+ * network connection.
+ */
+
+struct network_client
+{
+   u8 app_symmetric_key[ 1024 ];
+   u32 app_key_length;
+   EServerMode auth_mode;
+
+   HSteamNetConnection remote;
+   ESteamNetworkingConnectionState state;
+   u32 remote_version;
+
+   f64 last_attempt, last_frame;
+   u32 retries;
+
+   i32 network_info;
+   i32 auto_connect;
+
+   struct network_request {
+      vg_pool_node poolnode;
+      void (*callback)( netmsg_request *res, vg_msg *body, u64 userdata );
+      f64 sendtime;
+      u64 userdata;
+   }
+   *request_buffer;
+   vg_pool request_pool;
+
+   SteamNetworkingIPAddr ip;
+   char host_port[8], host_adress[256];
+   bool ip_resolved;
+
+   enum server_intent {
+      k_server_intent_offline,
+      k_server_intent_online
+   }
+   user_intent;
+   f64 last_intent_change;
+   f32 fintent; /* yeah this shit really shouldnt be here but oh well */
+}
+extern network_client;
+
+int packet_minsize( SteamNetworkingMessage_t *msg, u32 size );
+void network_send_item( enum netmsg_playeritem_type type );
+void network_request_scoreboard( const char *mod_uid, 
+                                 const char *route_uid,
+                                 u32 week, u64 userdata );
+void network_publish_laptime( const char *mod_uid, 
+                              const char *route_uid, f64 lap_time );
+void chat_send_message( const char *message );
+void render_server_status_gui(void);
+void network_status_string( vg_str *str, u32 *colour );
+void network_send_region(void);
+void network_set_host( const char *host_str, const char *port_str );
+
+static inline int network_connected(void)
+{
+   if( network_client.remote_version != NETWORK_SKATERIFT_VERSION ) return 0;
+   return network_client.state == k_ESteamNetworkingConnectionState_Connected;
+}
diff --git a/src/network_common.h b/src/network_common.h
new file mode 100644 (file)
index 0000000..ca46e47
--- /dev/null
@@ -0,0 +1,42 @@
+#pragma once
+#include "vg/vg_platform.h"
+#include "vg/vg_string.h"
+
+#define NETWORK_USERNAME_MAX 32
+#define NETWORK_MAX_PLAYERS 20
+#define NETWORK_FRAMERATE 0.1
+#define NETWORK_BUFFERFRAMES 6
+#define NETWORK_MAX_CHAT 128
+#define NETWORK_REGION_MAX 32
+#define NETWORK_SKATERIFT_VERSION 10
+#define NETWORK_REQUEST_MAX 2048
+
+#define NETWORK_LEADERBOARD_ALLTIME 0
+#define NETWORK_LEADERBOARD_CURRENT_WEEK 1
+#define NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK 2
+#define NETWORK_PORT 27403
+#define NETWORK_PORT_STR(STR, X) STR #X
+
+#include "addon_types.h"
+
+static u32 network_msgstring( const char *src, 
+                              u32 m_cbSize, u32 base_size,
+                              char *buf, u32 buf_size ){
+   
+   u32 string_len = VG_MIN( m_cbSize - base_size, buf_size );
+   return vg_strncpy( src, buf, string_len, k_strncpy_always_add_null );
+}
+
+static u32 network_pair_index( u32 _a, u32 _b ){
+   const u32 N = NETWORK_MAX_PLAYERS;
+
+   if( !((_a != _b) && (_a<N) && (_b<N) ) )
+   {
+      vg_fatal_error( "Programming error\n" );
+   }
+
+   u32 a = VG_MIN( _a, _b ),
+       b = VG_MAX( _a, _b );
+
+   return ((N-a)*((N-a)-1))/2 - b + a;
+}
diff --git a/src/network_compression.h b/src/network_compression.h
new file mode 100644 (file)
index 0000000..f83f0f5
--- /dev/null
@@ -0,0 +1,117 @@
+#ifndef NETWORK_COMPRESSION_H
+#define NETWORK_COMPRESSION_H
+
+#include "vg/vg_platform.h"
+#include "vg/vg_m.h"
+
+typedef struct bitpack_ctx bitpack_ctx;
+struct bitpack_ctx {
+   enum bitpack_mode {
+      k_bitpack_compress,
+      k_bitpack_decompress
+   }
+   mode;
+
+   u8 *buffer;
+   u32 bytes, buffer_len;
+};
+
+static void bitpack_bytes( bitpack_ctx *ctx, u32 bytes, void *data ){
+   u8 *ext = data;
+   for( u32 i=0; i<bytes; i++ ){
+      u32 index = ctx->bytes+i;
+      if( ctx->mode == k_bitpack_compress ){
+         if( index < ctx->buffer_len )
+            ctx->buffer[index] = ext[i];
+      }
+      else{
+         if( index < ctx->buffer_len )
+            ext[i] = ctx->buffer[index];
+         else
+            ext[i] = 0x00;
+      }
+   }
+   ctx->bytes += bytes;
+}
+
+static u32 bitpack_qf32( bitpack_ctx *ctx, u32 bits, 
+                         f32 min, f32 max, f32 *v ){
+   u32 mask = (0x1 << bits) - 1;
+
+   if( ctx->mode == k_bitpack_compress ){
+      u32 a = vg_quantf( *v, bits, min, max );
+      bitpack_bytes( ctx, bits/8, &a );
+      return a;
+   }
+   else {
+      u32 a = 0;
+      bitpack_bytes( ctx, bits/8, &a );
+      *v = vg_dequantf( a, bits, min, max );
+      return a;
+   }
+}
+
+static void bitpack_qv2f( bitpack_ctx *ctx, u32 bits,
+                          f32 min, f32 max, v2f v ){
+   for( u32 i=0; i<2; i ++ )
+      bitpack_qf32( ctx, bits, min, max, v+i );
+}
+
+static void bitpack_qv3f( bitpack_ctx *ctx, u32 bits,
+                          f32 min, f32 max, v3f v ){
+   for( u32 i=0; i<3; i ++ )
+      bitpack_qf32( ctx, bits, min, max, v+i );
+}
+
+static void bitpack_qv4f( bitpack_ctx *ctx, u32 bits,
+                          f32 min, f32 max, v4f v ){
+   for( u32 i=0; i<4; i ++ )
+      bitpack_qf32( ctx, bits, min, max, v+i );
+}
+
+static void bitpack_qquat( bitpack_ctx *ctx, v4f quat ){
+   const f32 k_domain = 0.70710678118f;
+
+   if( ctx->mode == k_bitpack_compress ){
+      v4f qabs;
+      for( u32 i=0; i<4; i++ )
+         qabs[i] = fabsf(quat[i]);
+
+      u32 lxy =  qabs[1]>qabs[0],
+          lzw = (qabs[3]>qabs[2])+2,
+          l   = qabs[lzw]>qabs[lxy]? lzw: lxy;
+
+      f32 sign = vg_signf(quat[l]);
+
+      u32 smallest[3];
+      for( u32 i=0, j=0; i<4; i ++ )
+         if( i != l )
+            smallest[j ++] = vg_quantf( quat[i]*sign, 10, -k_domain, k_domain );
+      
+      u32 comp = (smallest[0]<<2) | (smallest[1]<<12) | (smallest[2]<<22) | l;
+      bitpack_bytes( ctx, 4, &comp );
+   }
+   else {
+      u32 comp;
+      bitpack_bytes( ctx, 4, &comp );
+
+      u32 smallest[3] = {(comp>>2 )&0x3ff,
+                         (comp>>12)&0x3ff,
+                         (comp>>22)&0x3ff},
+          l = comp & 0x3;
+
+      f32 m = 1.0f;
+
+      for( u32 i=0, j=0; i<4; i ++ ){
+         if( i != l ){
+            quat[i] = vg_dequantf( smallest[j ++], 10, -k_domain, k_domain );
+            m -= quat[i]*quat[i];
+         }
+      }
+
+      quat[l] = sqrtf(m);
+      q_normalize( quat );
+   }
+}
+
+#endif /* NETWORK_COMPRESSION_H */
diff --git a/src/network_msg.h b/src/network_msg.h
new file mode 100644 (file)
index 0000000..4dfeb5e
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#ifndef NETWORK_MSG_H
+#define NETWORK_MSG_H
+
+#include "world_info.h"
+#include "vg/vg_platform.h"
+;
+
+#pragma pack(push,1)
+
+typedef struct netmsg_blank netmsg_blank;
+enum{ k_inetmsg_blank = 0 };
+struct netmsg_blank{
+   u16 inetmsg_id;
+};
+
+/* send after version */
+typedef struct netmsg_auth netmsg_auth;
+enum{ k_inetmsg_auth = 1 };
+struct netmsg_auth
+{
+   u16 inetmsg_id;
+
+   u32 ticket_length;
+   u8 ticket[];
+};
+
+/* version should be sent before auth */
+typedef struct netmsg_version netmsg_version;
+enum{ k_inetmsg_version = 2 };
+struct netmsg_version{
+   u16 inetmsg_id;
+   u32 version;
+};
+
+/* server control 100 */
+
+/* player updates 200 */
+
+#define NETMSG_BOUNDARY_BIT 0x8000
+#define NETMSG_GATE_BOUNDARY_BIT 0x4000
+#define NETMSG_BOUNDARY_MASK (NETMSG_BOUNDARY_BIT|NETMSG_GATE_BOUNDARY_BIT)
+#define NETMSG_PLAYERFRAME_INSTANCE_ID 0x3
+#define NETMSG_PLAYERFRAME_HAVE_GLIDER 0x4
+#define NETMSG_PLAYERFRAME_GLIDER_ORPHAN 0x8
+
+typedef struct netmsg_playerframe netmsg_playerframe;
+enum{ k_inetmsg_playerframe = 200 };
+struct netmsg_playerframe{
+   u16 inetmsg_id;
+   f64 timestamp;
+
+   u8 client, subsystem, 
+      flags, sound_effects;
+   u16 boundary_hash; /* used for animating correctly through gates, teleport..
+                         msb is a flip flop for teleporting
+                         second msb is flip flop for gate */
+
+   u8 animdata[];
+};
+
+typedef struct netmsg_playerjoin netmsg_playerjoin;
+enum{ k_inetmsg_playerjoin = 201 };
+struct netmsg_playerjoin{
+   u16 inetmsg_id;
+   u8 index;
+   u64 steamid;
+};
+
+typedef struct netmsg_playerleave netmsg_playerleave;
+enum{ k_inetmsg_playerleave = 202 };
+struct netmsg_playerleave{
+   u16 inetmsg_id;
+   u8 index;
+};
+
+typedef struct netmsg_playerusername netmsg_playerusername;
+enum{ k_inetmsg_playerusername = 203 };
+struct netmsg_playerusername{
+   u16 inetmsg_id;
+   u8 index;
+   char name[];
+};
+
+typedef struct netmsg_playeritem netmsg_playeritem;
+enum{ k_inetmsg_playeritem = 204 };
+struct netmsg_playeritem{
+   u16 inetmsg_id;
+   u8 client;
+   u8 type_index;
+   char uid[];
+};
+enum netmsg_playeritem_type {
+   k_netmsg_playeritem_board = 0,
+   k_netmsg_playeritem_player,
+   k_netmsg_playeritem_world0,
+   k_netmsg_playeritem_world1,
+   k_netmsg_playeritem_max
+};
+
+typedef struct netmsg_chat netmsg_chat;
+enum{ k_inetmsg_chat = 205 };
+struct netmsg_chat {
+   u16 inetmsg_id;
+   u8 client;
+   char msg[];
+};
+
+typedef struct netmsg_region netmsg_region;
+enum{ k_inetmsg_region = 206 };
+struct netmsg_region {
+   u16 inetmsg_id;
+   u8 client;
+   u32 flags;
+   char loc[];
+};
+
+/* requests 300 */
+typedef struct netmsg_request netmsg_request;
+enum{ k_inetmsg_request = 300, k_inetmsg_response = 301 };
+struct netmsg_request {
+   u16 inetmsg_id;
+   u8 id, status;
+   u8 q[];
+};
+
+enum request_status {
+  k_request_status_client_error = 0,
+  k_request_status_invalid_endpoint = 1,
+  k_request_status_unauthorized = 2,
+
+  k_request_status_server_error = 100,
+  k_request_status_out_of_memory = 101,
+  k_request_status_database_error = 102,
+
+  k_request_status_ok = 200,
+  k_request_status_not_found = 201
+};
+
+#pragma pack(pop)
+#endif /* NETWORK_MSG_H */
diff --git a/src/particle.c b/src/particle.c
new file mode 100644 (file)
index 0000000..ec2960b
--- /dev/null
@@ -0,0 +1,187 @@
+#include "vg/vg_lines.h"
+#include "vg/vg_async.h"
+#include "particle.h"
+#include "shaders/particle.h"
+
+struct particle_system particles_grind = {
+   .scale = 0.02f,
+   .velocity_scale = 0.001f,
+   .width = 0.0125f
+},
+particles_env = {
+   .scale = 0.04f,
+   .velocity_scale = 0.001f,
+   .width = 0.25f
+};
+
+void particle_spawn( particle_system *sys, v3f co, v3f v,
+                     f32 lifetime, u32 colour )
+{
+   if( sys->alive == sys->max ) return;
+
+   particle *p = &sys->array[ sys->alive ++ ];
+   v3_copy( co, p->co );
+   v3_copy( v, p->v );
+   p->life = lifetime;
+   p->colour = colour;
+}
+
+void particle_spawn_cone( particle_system *sys, 
+                          v3f co, v3f dir, f32 angle, f32 speed, 
+                          f32 lifetime, u32 colour )
+{
+   if( sys->alive == sys->max ) return;
+
+   particle *p = &sys->array[ sys->alive ++ ];
+
+   v3f tx, ty;
+   v3_tangent_basis( dir, tx, ty );
+
+   v3f rand;
+   vg_rand_cone( &vg.rand, rand, angle );
+   v3_muls(          tx,  rand[0]*speed, p->v );
+   v3_muladds( p->v, ty,  rand[1]*speed, p->v );
+   v3_muladds( p->v, dir, rand[2]*speed, p->v );
+
+   p->life = lifetime;
+   p->colour = colour;
+   v3_copy( co, p->co );
+}
+
+void particle_system_update( particle_system *sys, f32 dt )
+{
+   u32 i = 0;
+iter: if( i == sys->alive ) return;
+
+   particle *p = &sys->array[i];
+   p->life -= dt;
+
+   if( p->life < 0.0f ){
+      *p = sys->array[ -- sys->alive ];
+      goto iter;
+   }
+
+   v3_muladds( p->co, p->v, dt, p->co );
+   p->v[1] += -9.8f * dt;
+
+   i ++;
+   goto iter;
+}
+
+void particle_system_debug( particle_system *sys )
+{
+   for( u32 i=0; i<sys->alive; i ++ ){
+      particle *p = &sys->array[i];
+      v3f p1;
+      v3_muladds( p->co, p->v, 0.2f, p1 );
+      vg_line( p->co, p1, p->colour );
+   }
+}
+
+struct particle_init_args {
+   particle_system *sys;
+   u16 indices[];
+};
+
+static void async_particle_init( void *payload, u32 size ){
+   struct particle_init_args *args = payload;
+   particle_system *sys = args->sys;
+
+   glGenVertexArrays( 1, &sys->vao );
+   glGenBuffers( 1, &sys->vbo );
+   glGenBuffers( 1, &sys->ebo );
+   glBindVertexArray( sys->vao );
+
+   size_t stride = sizeof(particle_vert);
+
+   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
+   glBufferData( GL_ARRAY_BUFFER, sys->max*stride*4, NULL, GL_DYNAMIC_DRAW );
+   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, sys->ebo );
+   glBufferData( GL_ELEMENT_ARRAY_BUFFER, 
+                  sys->max*sizeof(u16)*6, args->indices, GL_STATIC_DRAW );
+
+   /* 0: coordinates */
+   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
+   glEnableVertexAttribArray( 0 );
+
+   /* 3: colour */
+   glVertexAttribPointer( 1, 4, GL_UNSIGNED_BYTE, GL_TRUE, 
+         stride, (void *)offsetof(particle_vert, colour) );
+   glEnableVertexAttribArray( 1 );
+}
+
+void particle_alloc( particle_system *sys, u32 max )
+{
+   size_t stride = sizeof(particle_vert);
+
+   sys->max = max;
+   sys->array = vg_linear_alloc( vg_mem.rtmemory, max*sizeof(particle) );
+   sys->vertices = vg_linear_alloc( vg_mem.rtmemory, max*stride*4 );
+
+   vg_async_item *call = 
+      vg_async_alloc( sizeof(particle_system *) + max*sizeof(u16)*6 );
+   struct particle_init_args *init = call->payload;
+   init->sys = sys;
+
+   for( u32 i=0; i<max; i ++ ){
+      init->indices[i*6+0] = i*4;
+      init->indices[i*6+1] = i*4+1;
+      init->indices[i*6+2] = i*4+2;
+      init->indices[i*6+3] = i*4;
+      init->indices[i*6+4] = i*4+2;
+      init->indices[i*6+5] = i*4+3;
+   }
+
+   vg_async_dispatch( call, async_particle_init );
+}
+
+void particle_system_prerender( particle_system *sys )
+{
+   for( u32 i=0; i<sys->alive; i ++ ){
+      particle *p = &sys->array[i];
+      particle_vert *vs = &sys->vertices[i*4];
+
+      v3f v, right;
+      v3_copy( p->v, v );
+   
+      f32 vm = v3_length( p->v );
+      v3_muls( v, 1.0f/vm, v );
+      v3_cross( v, (v3f){0,1,0}, right );
+
+      f32 l = (sys->scale+sys->velocity_scale*vm), 
+          w = sys->width;
+
+      v3f p0, p1;
+      v3_muladds( p->co, p->v,  l, p0 );
+      v3_muladds( p->co, p->v, -l, p1 );
+      
+      v3_muladds( p0, right,  w, vs[0].co );
+      v3_muladds( p1, right,  w, vs[1].co );
+      v3_muladds( p1, right, -w, vs[2].co );
+      v3_muladds( p0, right, -w, vs[3].co );
+
+      vs[0].colour = p->colour;
+      vs[1].colour = p->colour;
+      vs[2].colour = p->colour;
+      vs[3].colour = p->colour;
+   }
+
+   glBindVertexArray( sys->vao );
+
+   size_t stride = sizeof(particle_vert);
+   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
+   glBufferSubData( GL_ARRAY_BUFFER, 0, sys->alive*stride*4, sys->vertices );
+}
+
+void particle_system_render( particle_system *sys, vg_camera *cam )
+{
+   glDisable( GL_CULL_FACE );
+   glEnable( GL_DEPTH_TEST );
+
+   shader_particle_use();
+   shader_particle_uPv( cam->mtx.pv );
+   shader_particle_uPvPrev( cam->mtx_prev.pv );
+
+       glBindVertexArray( sys->vao );
+       glDrawElements( GL_TRIANGLES, sys->alive*6, GL_UNSIGNED_SHORT, NULL );
+}
diff --git a/src/particle.h b/src/particle.h
new file mode 100644 (file)
index 0000000..6858890
--- /dev/null
@@ -0,0 +1,42 @@
+#pragma once
+#include "skaterift.h"
+
+typedef struct particle_system particle_system;
+typedef struct particle particle;
+typedef struct particle_vert particle_vert;
+
+struct particle_system {
+   struct particle {
+      v3f co, v;
+      f32 life;
+      u32 colour;
+   }
+   *array;
+
+#pragma pack(push,1)
+   struct particle_vert {
+      v3f co;
+      u32 colour;
+   }
+   *vertices;
+#pragma pack(pop)
+
+   u32 alive, max;
+   GLuint vao, vbo, ebo;
+
+   /* render settings */
+   f32 scale, velocity_scale, width;
+}
+extern particles_grind, particles_env;
+
+void particle_alloc( particle_system *sys, u32 max );
+void particle_system_update( particle_system *sys, f32 dt );
+void particle_system_debug( particle_system *sys );
+void particle_system_prerender( particle_system *sys );
+void particle_system_render( particle_system *sys, vg_camera *cam );
+
+void particle_spawn( particle_system *sys, 
+                     v3f co, v3f v, f32 lifetime, u32 colour );
+void particle_spawn_cone( particle_system *sys, 
+                          v3f co, v3f dir, f32 angle, f32 speed, 
+                          f32 lifetime, u32 colour );
diff --git a/src/physics_test.h b/src/physics_test.h
new file mode 100644 (file)
index 0000000..243de36
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#ifndef PHYSICS_TEST_H
+#define PHYSICS_TEST_H
+
+#include "rigidbody.h"
+#include "player.h"
+
+rigidbody ground = { .type = k_rb_shape_box,
+                     .bbx = {{-100.0f,-1.0f,-100.0f},{100.0f,0.0f,100.0f}},
+                     .co = {0.0f, 0.0f, 0.0f},
+                     .q = {0.0f,0.0f,0.0f,1.0f},
+                     .is_world = 1 };
+
+rigidbody blocky = 
+   {
+      .type = k_rb_shape_box,
+      .bbx = {{-2.0f,-1.0f,-3.0f},{2.0f,1.0f,2.0f}},
+      .co = {30.0f,2.0f,30.0f},
+      .q = {0.0f,0.0f,0.0f,1.0f},
+      .is_world = 1
+   };
+
+rigidbody marko = 
+{
+   .type = k_rb_shape_box,
+   .bbx = {{-0.5f,-0.5f,-0.5f},{0.5f,0.5f,0.5f}},
+   .co = {-36.0f,8.0f,-36.0f},
+   .q = {0.0f,0.0f,0.0f,1.0f},
+   .is_world = 0
+};
+
+scene epic_scene;
+
+rigidbody epic_scene_rb = 
+{
+   .type = k_rb_shape_scene,
+   .co = {0.0f,0.0f,0.0f},
+   .q = {0.0f,0.0f,0.0f,1.0f},
+   .is_world = 1,
+   .inf.scene = { .pscene = &epic_scene }
+};
+
+rigidbody funnel[4] = {
+   {
+      .type = k_rb_shape_box,
+      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
+      .co = {-10.0f,5.0f,0.0f},
+      .is_world = 1
+   },
+   {
+      .type = k_rb_shape_box,
+      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
+      .co = { 10.0f,5.0f,0.0f},
+      .is_world = 1
+   },
+   {
+      .type = k_rb_shape_box,
+      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
+      .co = { 0.0f,5.0f,10.0f},
+      .is_world = 1
+   },
+   {
+      .type = k_rb_shape_box,
+      .bbx = {{-20.0f,-1.0f,-20.0f},{20.0f,1.0f,20.0f}},
+      .co = {0.0f,5.0f,-10.0f},
+      .is_world = 1
+   }
+};
+
+rigidbody jeff1 = { .type = k_rb_shape_capsule,
+                    .inf.capsule = { .radius = 0.75f, .height = 3.0f },
+                    .co = {30.0f, 4.0f, 30.0f },
+                    .q = {1.0f,0.0f,0.0f,0.0f}
+};
+
+rigidbody ball = { .type = k_rb_shape_sphere,
+                   .inf.sphere = { .radius = 2.0f },
+                   .co = {0.0f,20.0f,2.0f},
+                   .q = {0.0f,0.0f,0.0f,1.0f}},
+
+          ball1= { .type = k_rb_shape_sphere,
+                   .inf.sphere = { .radius = 2.0f },
+                   .co = {0.1f,25.0f,0.2f},
+                   .q = {0.0f,0.0f,0.0f,1.0f}};
+
+rigidbody jeffs[16];
+
+static void reorg_jeffs(void)
+{
+   for( int i=0; i<vg_list_size(jeffs); i++ )
+   {
+      v3_copy( (v3f){ (vg_randf()-0.5f) * 10.0f,
+                      (vg_randf()-0.5f) * 10.0f + 17.0f,
+                      (vg_randf()-0.5f) * 10.0f }, jeffs[i].co );
+      v4_copy( (v4f){ vg_randf(), vg_randf(), vg_randf(), vg_randf() },
+               jeffs[i].q );
+      q_normalize( jeffs[i].q );
+      
+      jeffs[i].type = k_rb_shape_capsule;
+      jeffs[i].inf.capsule.radius = 0.75f;
+      jeffs[i].inf.capsule.height = 3.0f;
+
+      rb_init( &jeffs[i] );
+   }
+}
+
+static void physics_test_start(void)
+{
+   q_axis_angle( funnel[0].q, (v3f){1.0f,0.0f,0.0f},  0.6f );
+   q_axis_angle( funnel[1].q, (v3f){1.0f,0.0f,0.0f}, -0.6f );
+   q_axis_angle( funnel[2].q, (v3f){0.0f,0.0f,1.0f},  0.6f );
+   q_axis_angle( funnel[3].q, (v3f){0.0f,0.0f,1.0f}, -0.6f );
+
+   for( int i=0; i<4; i++ )
+      rb_init( &funnel[i] );
+
+   reorg_jeffs();
+
+   rb_init( &ground );
+   rb_init( &ball );
+   rb_init( &ball1 );
+   rb_init( &jeff1 );
+   rb_init( &blocky );
+
+   scene_init( &epic_scene );
+
+   mdl_header *mdl = mdl_load( "models/epic_scene.mdl" );
+
+   m4x3f transform;
+   m4x3_identity( transform );
+
+   for( int i=0; i<mdl->node_count; i++ )
+   {
+      mdl_node *pnode = mdl_node_from_id( mdl, i );
+
+      for( int j=0; j<pnode->submesh_count; j++ )
+      {
+         mdl_submesh *sm = mdl_node_submesh( mdl, pnode, j );
+         scene_add_submesh( &epic_scene, mdl, sm, transform );
+      }
+   }
+
+   vg_free( mdl );
+   scene_bh_create( &epic_scene );
+
+   rb_init( &epic_scene_rb );
+   rb_init( &marko );
+}
+
+static void physics_test_update(void)
+{
+   player_freecam();
+   player_camera_update();
+
+   for( int i=0; i<4; i++ )
+      rb_debug( &funnel[i], 0xff0060e0 );
+   rb_debug( &ground, 0xff00ff00 );
+   rb_debug( &ball, 0xffe00040 );
+   rb_debug( &ball1, 0xff00e050 );
+
+   rb_debug( &blocky, 0xffcccccc );
+   rb_debug( &jeff1, 0xff00ffff );
+
+   rb_debug( &epic_scene_rb, 0xffcccccc );
+   rb_debug( &marko, 0xffffcc00 );
+
+   {
+
+   rb_solver_reset();
+
+   for( int i=0; i<4; i++ )
+   {
+      rigidbody *fn = &funnel[i];
+      rb_collide( &ball, fn );
+      rb_collide( &ball1, fn );
+      rb_collide( &jeff1, fn );
+
+      for( int i=0; i<vg_list_size(jeffs); i++ )
+         rb_collide( jeffs+i, fn );
+   }
+
+   for( int i=0; i<vg_list_size(jeffs)-1; i++ )
+   {
+      for( int j=i+1; j<vg_list_size(jeffs); j++ )
+      {
+         rb_collide( jeffs+i, jeffs+j );
+      }
+   }
+
+   for( int i=0; i<vg_list_size(jeffs); i++ )
+   {
+      rb_collide( jeffs+i, &ground );
+      rb_collide( jeffs+i, &ball );
+      rb_collide( jeffs+i, &ball1 );
+      rb_collide( jeffs+i, &jeff1 );
+   }
+
+   rb_collide( &jeff1, &ground );
+   rb_collide( &jeff1, &blocky );
+   rb_collide( &jeff1, &ball );
+   rb_collide( &jeff1, &ball1 );
+
+   rb_collide( &ball, &ground );
+   rb_collide( &ball1, &ground );
+   rb_collide( &ball1, &ball );
+   rb_collide( &marko, &epic_scene_rb );
+
+   rb_presolve_contacts( rb_contact_buffer, rb_contact_count );
+   for( int i=0; i<8; i++ )
+      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
+
+
+   /* ITERATE */
+   {
+   for( int i=0; i<vg_list_size(jeffs); i++ )
+   {
+      rb_debug( &jeffs[i], (u32[]){ 0xff0000ff, 0xff00ff00, 0xff00ffff,
+                                   0xffff0000, 0xffff00ff, 0xffffff00,
+                                   }[i%6] );
+      rb_iter( jeffs+i );
+   }
+
+   rb_iter( &ball );
+   rb_iter( &ball1 );
+   rb_iter( &jeff1 );
+   rb_iter( &marko );
+   }
+   
+   /* POSITION OVERRIDE */
+   {
+   if(glfwGetKey( vg.window, GLFW_KEY_L ))
+   {
+      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, marko.co );
+      v3_zero( marko.v );
+      v3_zero( marko.w );
+   }
+   if(glfwGetKey( vg.window, GLFW_KEY_K ))
+   {
+      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, ball.co );
+      v3_zero( ball.v );
+      v3_zero( ball.w );
+   }
+   if(glfwGetKey( vg.window, GLFW_KEY_J ))
+   {
+      m4x3_mulv( player.camera, (v3f){0.0f,0.0f,-5.0f}, ball1.co );
+      v3_zero( ball1.v );
+      v3_zero( ball1.w );
+   }
+
+   if(glfwGetKey( vg.window, GLFW_KEY_H ))
+   {
+      reorg_jeffs();
+   }
+   }
+   
+   /* UPDATE TRANSFORMS */
+   for( int i=0; i<vg_list_size(jeffs); i++ )
+   {
+      rb_update_transform(jeffs+i);
+   }
+
+   rb_update_transform( &ball );
+   rb_update_transform( &ball1 );
+   rb_update_transform( &jeff1 );
+   rb_update_transform( &marko );
+
+   }
+}
+
+static void physics_test_render(void)
+{
+   m4x4f world_4x4;
+   m4x3_expand( player.camera_inverse, world_4x4 );
+
+   gpipeline.fov = 60.0f;
+   m4x4_projection( vg_pv, gpipeline.fov, 
+         (float)vg_window_x / (float)vg_window_y, 
+         0.1f, 2100.0f );
+
+   m4x4_mul( vg_pv, world_4x4, vg_pv );
+   glEnable( GL_DEPTH_TEST );
+
+   glDisable( GL_DEPTH_TEST );
+   vg_lines_drawall( (float *)vg_pv );
+}
+
+#endif /* PHYSICS_TEST_H */
diff --git a/src/player.c b/src/player.c
new file mode 100644 (file)
index 0000000..551b39b
--- /dev/null
@@ -0,0 +1,423 @@
+#include "player.h"
+#include "addon.h"
+#include "player_model.h"
+#include "input.h"
+#include "world.h"
+#include "audio.h"
+#include "player_replay.h"
+#include "network.h"
+#include "network_common.h"
+#include "world_routes.h"
+#include "ent_miniworld.h"
+#include "gui.h"
+
+#include "shaders/model_entity.h"
+#include "shaders/model_character_view.h"
+#include "shaders/model_board_view.h"
+
+#include "player_walk.h"
+#include "player_dead.h"
+#include "player_drive.h"
+#include "player_skate.h"
+#include "player_basic_info.h"
+#include "player_glide.h"
+#include <string.h>
+
+i32 k_invert_y = 0;
+struct localplayer localplayer = 
+{
+   .rb = 
+   {
+      .co = { 0,0,0 },
+      .w = { 0,0,0 },
+      .v = { 0,0,0 },
+      .q = { 0,0,0,1 },
+      .to_world = M4X3_IDENTITY,
+      .to_local = M4X3_IDENTITY
+   }
+};
+
+struct player_subsystem_interface *player_subsystems[] = 
+{
+   [k_player_subsystem_walk]  = &player_subsystem_walk,
+   [k_player_subsystem_dead]  = &player_subsystem_dead,
+   [k_player_subsystem_drive] = &player_subsystem_drive,
+   [k_player_subsystem_skate] = &player_subsystem_skate,
+   [k_player_subsystem_basic_info]=&player_subsystem_basic_info,
+   [k_player_subsystem_glide] = &player_subsystem_glide,
+};
+
+int localplayer_cmd_respawn( int argc, const char *argv[] )
+{
+   ent_spawn *rp = NULL, *r;
+   world_instance *world = world_current_instance();
+
+   if( argc == 1 ){
+      rp = world_find_spawn_by_name( world, argv[0] );
+   }
+   else if( argc == 0 ){
+      rp = world_find_closest_spawn( world, localplayer.rb.co );
+   }
+
+   if( !rp )
+      return 0;
+
+   player__spawn( rp );
+   return 1;
+}
+
+void player_init(void)
+{
+   for( u32 i=0; i<k_player_subsystem_max; i++ )
+   {
+      struct player_subsystem_interface *sys = player_subsystems[i];
+      if( sys->system_register ) sys->system_register();
+   }
+
+   vg_console_reg_cmd( "respawn", localplayer_cmd_respawn, NULL );
+   VG_VAR_F32( k_cam_damp );
+   VG_VAR_F32( k_cam_spring );
+   VG_VAR_F32( k_cam_punch );
+   VG_VAR_F32( k_cam_shake_strength );
+   VG_VAR_F32( k_cam_shake_trackspeed );
+   VG_VAR_I32( k_player_debug_info, flags=VG_VAR_PERSISTENT );
+
+#if 0
+   vg_console_reg_var( "cinema", &k_cinema, k_var_dtype_f32, 0 );
+   vg_console_reg_var( "cinema_fixed", &k_cinema_fixed, k_var_dtype_i32, 0 );
+#endif
+   vg_console_reg_var( "invert_y", &k_invert_y,
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+}
+
+void player__debugtext( ui_context *ctx, int size, const char *fmt, ... )
+{
+       char buffer[ 1024 ];
+
+   va_list args;
+   va_start( args, fmt );
+   vsnprintf( buffer, 1024, fmt, args );
+   va_end( args );
+
+   ui_text( ctx, g_player_debugger, buffer, size, k_ui_align_left, 0 );
+   g_player_debugger[1] += size*16;
+}
+
+/* 
+ * Appearence
+ */
+
+void player__use_model( u16 reg_id )
+{
+   addon_cache_unwatch( k_addon_type_player, 
+                        localplayer.playermodel_view_slot );
+   localplayer.playermodel_view_slot = 
+      addon_cache_create_viewer( k_addon_type_player, reg_id );
+}
+
+void player__bind(void)
+{
+   for( u32 i=0; i<k_player_subsystem_max; i++ )
+   {
+      struct player_subsystem_interface *sys = player_subsystems[i];
+
+      if( sys->bind ) sys->bind();
+   }
+}
+
+/*
+ * Gameloop events
+ * ----------------------------------------------------------------------------
+ */
+
+void player__pre_update(void)
+{
+   if( button_down( k_srbind_camera ) && !localplayer.immobile &&
+       (localplayer.subsystem != k_player_subsystem_dead) ){
+      if( localplayer.cam_control.camera_mode == k_cam_firstperson )
+         localplayer.cam_control.camera_mode = k_cam_thirdperson;
+      else
+         localplayer.cam_control.camera_mode = k_cam_firstperson;
+   }
+
+   if( player_subsystems[ localplayer.subsystem ]->pre_update )
+      player_subsystems[ localplayer.subsystem ]->pre_update();
+}
+
+void player__update(void)
+{
+   if( player_subsystems[ localplayer.subsystem ]->update )
+      player_subsystems[ localplayer.subsystem ]->update();
+
+   if( localplayer.glider_orphan && 
+       (skaterift.activity != k_skaterift_replay) )
+      glider_physics( (v2f){0,0} );
+}
+
+void player__post_update(void)
+{
+   struct player_subsystem_interface *sys = 
+      player_subsystems[ localplayer.subsystem ];
+
+   if( sys->post_update ) sys->post_update();
+
+   SDL_AtomicLock( &air_audio_data.sl );
+   air_audio_data.speed = v3_length( localplayer.rb.v ) * vg.time_rate;
+   SDL_AtomicUnlock( &air_audio_data.sl );
+}
+
+/*
+ * Applies gate transport to a player_interface
+ */
+void player__pass_gate( u32 id )
+{
+   world_instance *world = world_current_instance();
+   skaterift_record_frame( &player_replay.local, 1 );
+
+   /* update boundary hash (network animation) */
+   u16 index = mdl_entity_id_id(id) & ~NETMSG_BOUNDARY_MASK;
+   localplayer.boundary_hash ^= NETMSG_GATE_BOUNDARY_BIT;
+   localplayer.boundary_hash &= ~NETMSG_BOUNDARY_MASK;
+   localplayer.boundary_hash |= index;
+   
+   ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
+   world_routes_fracture( world, gate, localplayer.rb.co, localplayer.rb.v );
+
+   localplayer.gate_waiting = gate;
+   localplayer.deferred_frame_record = 1;
+
+   struct player_cam_controller *cc = &localplayer.cam_control;
+   m4x3_mulv( gate->transport, cc->tpv_lpf, cc->tpv_lpf );
+   m3x3_mulv( gate->transport, cc->cam_velocity_smooth, 
+                               cc->cam_velocity_smooth );
+
+   m4x3_mulv( gate->transport, localplayer.cam.pos, localplayer.cam.pos );
+
+   if( gate->flags & k_ent_gate_nonlocal )
+   {
+      world_default_spawn_pos( world, world->player_co );
+      world_static.active_instance = gate->target;
+      player__clean_refs();
+
+      replay_clear( &player_replay.local );
+   }
+   else 
+   {
+      world_routes_activate_entry_gate( world, gate );
+   }
+   
+   v3f v0;
+   v3_angles_vector( localplayer.angles, v0 );
+   m3x3_mulv( gate->transport, v0, v0 );
+   v3_angles( v0, localplayer.angles );
+
+   audio_lock();
+   audio_oneshot( &audio_gate_pass, 1.0f, 0.0f );
+   audio_unlock();
+}
+
+void player_apply_transport_to_cam( m4x3f transport )
+{
+   /* Pre-emptively edit the camera matrices so that the motion vectors 
+    * are correct */
+   m4x3f transport_i;
+   m4x4f transport_4;
+   m4x3_invert_affine( transport, transport_i );
+   m4x3_expand( transport_i, transport_4 );
+   m4x4_mul( g_render.cam.mtx.pv, transport_4, g_render.cam.mtx.pv );
+   m4x4_mul( g_render.cam.mtx.v,  transport_4, g_render.cam.mtx.v );
+
+   /* we want the regular transform here no the inversion */
+   m4x3_expand( transport, transport_4 );
+   m4x4_mul( world_gates.cam.mtx.pv, transport_4, world_gates.cam.mtx.pv );
+   m4x4_mul( world_gates.cam.mtx.v,  transport_4, world_gates.cam.mtx.v );
+}
+
+void player__im_gui( ui_context *ctx )
+{
+   if( !k_player_debug_info ) return;
+
+   ui_rect box = {
+      vg.window_x - 300,
+      0, 
+      300,
+      vg.window_y
+   };
+
+   ui_fill( ctx, box, (ui_colour(ctx, k_ui_bg)&0x00ffffff)|0x50000000 );
+
+   g_player_debugger[0] = box[0];
+   g_player_debugger[1] = 0;
+   g_player_debugger[2] = 300;
+   g_player_debugger[3] = 32;
+
+   player__debugtext( ctx, 2, "instance #%u", world_static.active_instance );
+
+   char buf[96];
+   for( u32 i=0; i<k_world_max; i++ )
+   {
+      if( world_static.instance_addons[ i ] )
+         addon_alias_uid( &world_static.instance_addons[ i ]->alias, buf );
+      else
+         strcpy( buf, "none" );
+
+      player__debugtext( ctx, 1, "world #%u: %s", i, buf );
+   }
+
+   player__debugtext( ctx, 2, "director" );
+   player__debugtext( ctx, 1, "activity: %s", 
+                     (const char *[]){ [k_skaterift_menu]      = "menu",
+                                       [k_skaterift_replay]    = "replay",
+                                       [k_skaterift_ent_focus] = "ent_focus",
+                                       [k_skaterift_default]   = "default",
+                     } [skaterift.activity] );
+   player__debugtext( ctx, 1, "time_rate: %.4f", skaterift.time_rate );
+
+   player__debugtext( ctx, 2, "player" );
+   player__debugtext( ctx, 1, "angles: " PRINTF_v3f( localplayer.cam.angles ) );
+
+   if( player_subsystems[ localplayer.subsystem ]->im_gui )
+      player_subsystems[ localplayer.subsystem ]->im_gui( ctx );
+
+   skaterift_replay_debug_info( ctx );
+}
+
+void player__setpos( v3f pos )
+{
+   v3_copy( pos, localplayer.rb.co );
+   v3_zero( localplayer.rb.v );
+   rb_update_matrices( &localplayer.rb );
+}
+
+void player__clean_refs(void)
+{
+   replay_clear( &player_replay.local );
+   gui_helper_clear();
+
+   world_static.challenge_target = NULL;
+   world_static.challenge_timer = 0.0f;
+   world_static.active_trigger_volume_count = 0;
+   world_static.last_use = 0.0;
+   world_entity_exit_modal();
+   world_entity_clear_focus();
+
+   localplayer.boundary_hash ^= NETMSG_BOUNDARY_BIT;
+
+   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
+      world_instance *instance = &world_static.instances[i];
+      if( instance->status == k_world_status_loaded ){
+         world_routes_clear( instance );
+      }
+   }
+}
+
+void player__reset(void)
+{
+   v3_zero( localplayer.rb.v );
+   v3_zero( localplayer.rb.w );
+   
+   f32 l = v4_length( localplayer.rb.q );
+   if( (l < 0.9f) || (l > 1.1f) )
+      q_identity( localplayer.rb.q );
+
+   rb_update_matrices( &localplayer.rb );
+
+   localplayer.subsystem = k_player_subsystem_walk;
+   player__walk_reset();
+
+   localplayer.immobile = 0;
+   localplayer.gate_waiting = NULL;
+   localplayer.have_glider = 0;
+   localplayer.glider_orphan = 0;
+   localplayer.drowned = 0;
+
+   v3_copy( localplayer.rb.co, localplayer.cam_control.tpv_lpf );
+   player__clean_refs();
+}
+
+void player__spawn( ent_spawn *rp )
+{
+   player__setpos( rp->transform.co );
+   player__reset();
+}
+
+
+void player__kill(void)
+{
+}
+
+void player__begin_holdout( v3f offset )
+{
+   memcpy( &localplayer.holdout_pose, &localplayer.pose, 
+            sizeof(localplayer.pose) );
+   v3_copy( offset, localplayer.holdout_pose.root_co );
+   localplayer.holdout_time = 1.0f;
+}
+
+void net_sfx_exchange( bitpack_ctx *ctx, struct net_sfx *sfx )
+{
+   bitpack_bytes( ctx, 1, &sfx->system );
+   bitpack_bytes( ctx, 1, &sfx->priority );
+   bitpack_bytes( ctx, 1, &sfx->id );
+   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &sfx->subframe );
+   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &sfx->volume );
+   bitpack_qv3f( ctx, 16, -1024.0f, 1024.0f, sfx->location );
+}
+
+void net_sfx_play( struct net_sfx *sfx )
+{
+   if( sfx->system < k_player_subsystem_max ){
+      struct player_subsystem_interface *sys = player_subsystems[sfx->system];
+      if( sys->sfx_oneshot ){
+         sys->sfx_oneshot( sfx->id, sfx->location, sfx->volume );
+      }
+   }
+};
+
+static struct net_sfx *find_lower_priority_sfx( struct net_sfx *buffer, u32 len, 
+                                                u32 *count, u8 priority ){
+   struct net_sfx *p_sfx = NULL;
+   if( *count < len ){
+      p_sfx = &buffer[ *count ];
+      *count = *count+1;
+   }
+   else {
+      for( u32 i=0; i<len; i++ ){
+         struct net_sfx *a = &buffer[i];
+         if( a->priority < priority ){
+            p_sfx = a;
+            break;
+         }
+      }
+   }
+
+   return p_sfx;
+}
+
+void player__networked_sfx( u8 system, u8 priority, u8 id, 
+                            v3f pos, f32 volume )
+{
+   struct net_sfx sfx,
+         *p_net = find_lower_priority_sfx( 
+               localplayer.sfx_buffer, 4, 
+               &localplayer.sfx_buffer_count, priority ),
+         *p_replay = find_lower_priority_sfx(
+               localplayer.local_sfx_buffer, 2,
+               &localplayer.local_sfx_buffer_count, priority );
+
+   sfx.id = id;
+   sfx.priority = priority;
+   sfx.volume = volume;
+   v3_copy( pos, sfx.location );
+   sfx.system = system;
+
+   /* we only care about subframe in networked sfx. local replays run at a 
+    * high enough framerate. */
+   f32 t = (vg.time_real - network_client.last_frame) / NETWORK_FRAMERATE;
+   sfx.subframe = vg_clampf( t, 0.0f, 1.0f );
+
+   if( p_net ) *p_net = sfx;
+   if( p_replay ) *p_replay = sfx;
+
+   net_sfx_play( &sfx );
+}
diff --git a/src/player.h b/src/player.h
new file mode 100644 (file)
index 0000000..ff8c2e1
--- /dev/null
@@ -0,0 +1,197 @@
+#pragma once
+#include "vg/vg_platform.h"
+
+struct player_cam_controller {
+   enum camera_mode{
+      k_cam_firstperson = 1,
+      k_cam_thirdperson = 0
+   }
+   camera_mode;
+   f32 camera_type_blend;
+
+   v3f fpv_offset,         /* expressed relative to rigidbody */
+       tpv_offset,
+       tpv_offset_extra,
+       fpv_viewpoint,      /* expressed relative to neck bone inverse final*/
+       fpv_offset_smooth,
+       fpv_viewpoint_smooth,
+       tpv_offset_smooth,
+       tpv_lpf,
+       cam_velocity_smooth;
+};
+
+#include "player_common.h"
+#include "network_compression.h"
+#include "player_effects.h"
+#include "player_api.h"
+#include "player_ragdoll.h"
+#include "player_model.h"
+#include "player_render.h"
+
+struct player_subsystem_interface
+{
+   void(*system_register)(void);
+   void(*bind)(void);
+   void(*pre_update)(void);
+   void(*update)(void);
+   void(*post_update)(void);
+   void(*im_gui)( ui_context *ctx );
+   void(*animate)(void);
+   void(*pose)( void *animator, player_pose *pose );
+   void(*effects)( void *animator, m4x3f *final_mtx, struct player_board *board,
+                   struct player_effects_data *effect_data );
+   void(*post_animate)(void);
+
+   void(*network_animator_exchange)( bitpack_ctx *ctx, void *data );
+   void(*sfx_oneshot)( u8 id, v3f pos, f32 volume );
+
+   void(*sfx_comp)(void *animator);
+   void(*sfx_kill)(void);
+
+   void *animator_data;
+   u32 animator_size;
+
+   const char *name;
+};
+
+#define PLAYER_REWIND_FRAMES 60*4
+#define RESET_MAX_TIME 45.0
+
+extern i32 k_invert_y;
+struct localplayer
+{
+   /* transform definition */
+   rigidbody rb;
+   v3f angles;
+
+   bool have_glider, glider_orphan, drowned;
+
+   /*
+    * Camera management
+    * ---------------------------
+    */
+   vg_camera cam;
+   struct player_cam_controller cam_control;
+   f32 cam_trackshake;
+
+   float cam_velocity_influence,
+         cam_velocity_coefficient,
+         cam_velocity_constant,
+         cam_velocity_coefficient_smooth,
+         cam_velocity_constant_smooth,
+         cam_velocity_influence_smooth,
+         cam_dist,
+         cam_dist_smooth;
+
+   v3f cam_land_punch, cam_land_punch_v;
+   ent_gate *gate_waiting;
+   int deferred_frame_record;
+
+   int immobile;
+
+   int rewinded_since_last_gate;
+
+   /* 
+    * Network
+    * --------------------------------------------------
+    */
+   u16 boundary_hash;
+   struct net_sfx {
+      u8 system, priority, id;
+      f32 subframe, volume;
+      v3f location;
+   }
+   sfx_buffer[4],             /* large timeframe 1/10s; for networking */
+   local_sfx_buffer[2];       /* per framerate 1/30s; for replay */
+   u32 sfx_buffer_count, 
+       local_sfx_buffer_count;
+
+   /*
+    * Animation
+    * --------------------------------------------------
+    */
+
+   struct player_ragdoll  ragdoll;
+   struct player_model    fallback_model;
+   struct player_board    fallback_board;
+
+   u16 board_view_slot, playermodel_view_slot;
+
+   player_pose            pose;
+   player_pose            holdout_pose;
+   float                  holdout_time;
+
+   m4x3f                 *final_mtx;
+
+   /*
+    * Subsystems
+    * -------------------------------------------------
+    */
+
+   enum player_subsystem subsystem, 
+                         observing_system; 
+
+   /*
+    * Rendering
+    */
+   mdl_context skeleton_meta;
+   struct skeleton skeleton;
+
+   u8 id_hip,
+      id_chest,
+      id_ik_hand_l,
+      id_ik_hand_r,
+      id_ik_elbow_l,
+      id_ik_elbow_r,
+      id_head,
+      id_foot_l,
+      id_foot_r,
+      id_ik_foot_l,
+      id_ik_foot_r,
+      id_ik_knee_l,
+      id_ik_knee_r,
+      id_wheel_l,
+      id_wheel_r,
+      id_board,
+      id_eyes,
+      id_world;
+
+   u8 skeleton_mirror[32];
+
+   struct player_effects_data effect_data;
+}
+extern localplayer;
+extern struct player_subsystem_interface *player_subsystems[];
+
+/*
+ * Gameloop tables
+ * ---------------------------------------------------------
+ */
+
+void player_init(void);
+void player__debugtext( ui_context *ctx, int size, const char *fmt, ... );
+void player__use_mesh( glmesh *mesh );
+void player__use_model( u16 reg_id );
+
+void player__bind(void);
+void player__pre_update(void);
+void player__update(void);
+void player__post_update(void);
+
+void player__pass_gate( u32 id );
+void player__im_gui( ui_context *ctx );
+void player__setpos( v3f pos );
+void player__spawn( ent_spawn *rp );
+void player__clean_refs(void);
+void player__reset(void);
+void player__kill(void);
+void player__begin_holdout( v3f offset );
+
+int localplayer_cmd_respawn( int argc, const char *argv[] );
+void player_apply_transport_to_cam( m4x3f transport );
+
+void player__clear_sfx_buffer(void);
+void player__networked_sfx( u8 system, u8 priority, u8 id, 
+                            v3f pos, f32 volume );
+void net_sfx_exchange( bitpack_ctx *ctx, struct net_sfx *sfx );
+void net_sfx_play( struct net_sfx *sfx );
diff --git a/src/player_api.h b/src/player_api.h
new file mode 100644 (file)
index 0000000..f7f4785
--- /dev/null
@@ -0,0 +1,34 @@
+#pragma once
+#include "model.h"
+
+typedef struct player_instance player_instance;
+typedef struct player_pose player_pose;
+
+struct player_pose{
+   enum player_pose_type {
+      k_player_pose_type_ik,      /* regular IK animation */
+      k_player_pose_type_fk_2,
+   }
+   type;
+
+   v3f root_co;
+   v4f root_q;
+
+   mdl_keyframe keyframes[32];
+
+   struct player_board_pose {
+      f32 lean;
+   }
+   board;
+};
+
+enum player_subsystem{
+   k_player_subsystem_walk = 0,
+   k_player_subsystem_skate = 1,
+   k_player_subsystem_dead = 2,
+   k_player_subsystem_drive = 3,
+   k_player_subsystem_basic_info = 4,
+   k_player_subsystem_glide = 5,
+   k_player_subsystem_max,
+   k_player_subsystem_invalid = 255
+};
diff --git a/src/player_basic_info.c b/src/player_basic_info.c
new file mode 100644 (file)
index 0000000..ffc7ae0
--- /dev/null
@@ -0,0 +1,34 @@
+#include "player_basic_info.h"
+#include "network_compression.h"
+
+struct player_basic_info player_basic_info;
+struct player_subsystem_interface player_subsystem_basic_info = 
+{
+   .pose = player__basic_info_pose,
+   .network_animator_exchange = player__basic_info_animator_exchange,
+   .animator_data = &player_basic_info.animator,
+   .animator_size = sizeof(player_basic_info.animator),
+   .name = "Basic Info"
+};
+
+void player__basic_info_animator_exchange(bitpack_ctx *ctx, void *data)
+{
+   struct player_basic_info_animator *animator = data;
+   /* TODO: This range needs to be standardized in a common header */
+   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
+}
+
+void player__basic_info_pose( void *_animator, player_pose *pose )
+{
+   struct player_basic_info_animator *animator = _animator;
+   v3_copy( animator->root_co, pose->root_co );
+   q_identity( pose->root_q );
+   pose->type = k_player_pose_type_fk_2;
+   pose->board.lean = 0.0f;
+
+   for( int i=0; i<localplayer.skeleton.bone_count; i ++ ){
+      v3_zero(pose->keyframes[i].co);
+      q_identity(pose->keyframes[i].q);
+      v3_fill(pose->keyframes[i].s,1.0f);
+   }
+}
diff --git a/src/player_basic_info.h b/src/player_basic_info.h
new file mode 100644 (file)
index 0000000..815be67
--- /dev/null
@@ -0,0 +1,18 @@
+#pragma once
+#include "player.h"
+#include "player_api.h"
+
+struct player_basic_info 
+{
+   struct player_basic_info_animator 
+   {
+      v3f root_co;
+   }
+   animator;
+}
+extern player_basic_info;
+extern struct player_subsystem_interface player_subsystem_basic_info;
+
+void player__basic_info_animator_exchange(bitpack_ctx *ctx, void *data);
+void player__basic_info_pose( void *_animator, player_pose *pose );
+
diff --git a/src/player_common.c b/src/player_common.c
new file mode 100644 (file)
index 0000000..1ecbae9
--- /dev/null
@@ -0,0 +1,290 @@
+#include "ent_skateshop.h"
+#include "player.h"
+#include "input.h"
+#include "menu.h"
+#include "vg/vg_perlin.h"
+
+float player_get_heading_yaw(void)
+{
+   v3f xz;
+   q_mulv( localplayer.rb.q, (v3f){ 0.0f,0.0f,1.0f }, xz );
+   return atan2f( xz[0], xz[2] );
+}
+
+static void player_camera_portal_correction(void)
+{
+   if( localplayer.gate_waiting ){
+      /* construct plane equation for reciever gate */
+      v4f plane;
+      q_mulv( localplayer.gate_waiting->q[1], (v3f){0.0f,0.0f,1.0f}, plane );
+      plane[3] = v3_dot( plane, localplayer.gate_waiting->co[1] );
+
+      f32 pol = v3_dot( localplayer.cam.pos, plane ) - plane[3];
+
+      int cleared = (pol < 0.0f) || (pol > 5.0f);
+
+      if( cleared ){
+         vg_success( "Plane cleared\n" );
+      }
+
+      m4x3f inverse;
+      m4x3_invert_affine( localplayer.gate_waiting->transport, inverse );
+
+      /* de-transform camera and player back */
+      v3f v0;
+      m4x3_mulv( inverse, localplayer.cam.pos, localplayer.cam.pos );
+      v3_angles_vector( localplayer.cam.angles, v0 );
+      m3x3_mulv( inverse, v0, v0 );
+      v3_angles( v0, localplayer.cam.angles );
+
+      skeleton_apply_transform( &localplayer.skeleton, inverse, 
+                                 localplayer.final_mtx );
+
+      /* record and re-put things again */
+      if( cleared )
+      {
+         skaterift_record_frame( &player_replay.local, 1 );
+         localplayer.deferred_frame_record = 1;
+
+         skeleton_apply_transform( &localplayer.skeleton, 
+                                    localplayer.gate_waiting->transport,
+                                    localplayer.final_mtx );
+
+         m4x3_mulv( localplayer.gate_waiting->transport, 
+                    localplayer.cam.pos, localplayer.cam.pos );
+         v3_angles_vector( localplayer.cam.angles, v0 );
+         m3x3_mulv( localplayer.gate_waiting->transport, v0, v0 );
+         v3_angles( v0, localplayer.cam.angles );
+         player_apply_transport_to_cam( localplayer.gate_waiting->transport );
+         localplayer.gate_waiting = NULL;
+      }
+   }
+}
+
+void player__cam_iterate(void)
+{
+   struct player_cam_controller *cc = &localplayer.cam_control;
+
+   if( localplayer.subsystem == k_player_subsystem_walk ){
+      v3_copy( (v3f){-0.1f,1.8f,0.0f}, cc->fpv_viewpoint );
+      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
+      v3_copy( (v3f){0.0f,1.8f,0.0f}, cc->tpv_offset );
+   }
+   else if( localplayer.subsystem == k_player_subsystem_glide ){
+      v3_copy( (v3f){-0.15f,1.75f,0.0f}, cc->fpv_viewpoint );
+      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
+      v3_copy( (v3f){0.0f,-1.0f,0.0f}, cc->tpv_offset );
+      v3_add( cc->tpv_offset_extra, cc->tpv_offset, cc->tpv_offset );
+   }
+   else{
+      v3_copy( (v3f){-0.15f,1.75f,0.0f}, cc->fpv_viewpoint );
+      v3_copy( (v3f){0.0f,0.0f,0.0f}, cc->fpv_offset );
+
+      f32 h = vg_lerpf( 0.4f, 1.4f, k_cam_height );
+      v3_copy( (v3f){0.0f,h,0.0f}, cc->tpv_offset );
+      v3_add( cc->tpv_offset_extra, cc->tpv_offset, cc->tpv_offset );
+   }
+
+   localplayer.cam_velocity_constant = 0.25f;
+   localplayer.cam_velocity_coefficient = 0.7f;
+
+   /* lerping */
+
+   if( localplayer.cam_dist_smooth == 0.0f ){
+      localplayer.cam_dist_smooth = localplayer.cam_dist;
+   }
+   else {
+      localplayer.cam_dist_smooth = vg_lerpf(
+            localplayer.cam_dist_smooth,
+            localplayer.cam_dist,
+            vg.time_frame_delta * 8.0f );
+   }
+
+   localplayer.cam_velocity_influence_smooth = vg_lerpf(
+         localplayer.cam_velocity_influence_smooth, 
+         localplayer.cam_velocity_influence,
+         vg.time_frame_delta * 8.0f );
+
+   localplayer.cam_velocity_coefficient_smooth = vg_lerpf(
+         localplayer.cam_velocity_coefficient_smooth,
+         localplayer.cam_velocity_coefficient,
+         vg.time_frame_delta * 8.0f );
+
+   localplayer.cam_velocity_constant_smooth = vg_lerpf(
+         localplayer.cam_velocity_constant_smooth,
+         localplayer.cam_velocity_constant,
+         vg.time_frame_delta * 8.0f );
+
+   enum camera_mode target_mode = cc->camera_mode;
+
+   if( localplayer.subsystem == k_player_subsystem_dead )
+      target_mode = k_cam_thirdperson;
+
+   cc->camera_type_blend = 
+      vg_lerpf( cc->camera_type_blend, 
+               (target_mode == k_cam_firstperson)? 1.0f: 0.0f,
+                5.0f * vg.time_frame_delta );
+
+   v3_lerp( cc->fpv_viewpoint_smooth, cc->fpv_viewpoint,
+            vg.time_frame_delta * 8.0f, cc->fpv_viewpoint_smooth );
+
+   v3_lerp( cc->fpv_offset_smooth, cc->fpv_offset,
+            vg.time_frame_delta * 8.0f, cc->fpv_offset_smooth );
+
+   v3_lerp( cc->tpv_offset_smooth, cc->tpv_offset,
+            vg.time_frame_delta * 8.0f, cc->tpv_offset_smooth );
+
+   /* fov -- simple blend */
+   float fov_skate = vg_lerpf( 97.0f, 135.0f, k_fov ),
+         fov_walk  = vg_lerpf( 90.0f, 110.0f, k_fov );
+
+   localplayer.cam.fov = vg_lerpf( fov_walk, fov_skate, cc->camera_type_blend );
+
+   /* 
+    * first person camera
+    */
+
+   /* position */
+   v3f fpv_pos, fpv_offset;
+   m4x3_mulv( localplayer.final_mtx[ localplayer.id_head-1 ], 
+               cc->fpv_viewpoint_smooth, fpv_pos );
+   m3x3_mulv( localplayer.rb.to_world, cc->fpv_offset_smooth, fpv_offset );
+   v3_add( fpv_offset, fpv_pos, fpv_pos );
+
+   /* angles */
+   v3f velocity_angles;
+   v3_lerp( cc->cam_velocity_smooth, localplayer.rb.v, 4.0f*vg.time_frame_delta, 
+            cc->cam_velocity_smooth );
+
+   v3_angles( cc->cam_velocity_smooth, velocity_angles );
+   velocity_angles[1] *= localplayer.cam_velocity_coefficient_smooth;
+   velocity_angles[1] += localplayer.cam_velocity_constant_smooth;
+
+   float inf_fpv = localplayer.cam_velocity_influence_smooth * 
+                     cc->camera_type_blend,
+         inf_tpv = localplayer.cam_velocity_influence_smooth *
+                     (1.0f-cc->camera_type_blend);
+
+   vg_camera_lerp_angles( localplayer.angles, velocity_angles, 
+                        inf_fpv,
+                        localplayer.angles );
+
+   /*
+    * Third person camera
+    */
+
+   /* no idea what this technique is called, it acts like clamped position based 
+    * on some derivative of where the final camera would end up ....
+    *
+    * it is done in the local basis then transformed back */
+
+   v3f future;
+   v3_muls( localplayer.rb.v, 0.4f*vg.time_frame_delta, future );
+
+   v3f camera_follow_dir = 
+      { -sinf( localplayer.angles[0] ) * cosf( localplayer.angles[1] ),
+         sinf( localplayer.angles[1] ),
+         cosf( localplayer.angles[0] ) * cosf( localplayer.angles[1] ) };
+
+   v3f v0;
+   v3_sub( camera_follow_dir, future, v0 );
+
+   v3f follow_angles;
+   v3_copy( localplayer.angles, follow_angles );
+   follow_angles[0] = atan2f( -v0[0], v0[2] );
+   follow_angles[1] = 0.3f + velocity_angles[1] * 0.2f;
+
+   float ya = atan2f( -cc->cam_velocity_smooth[1], 30.0f );
+
+   follow_angles[1] = 0.3f + ya;
+   vg_camera_lerp_angles( localplayer.angles, follow_angles,
+                        inf_tpv,
+                        localplayer.angles );
+
+   v3f pco;
+   v4f pq;
+   rb_extrapolate( &localplayer.rb, pco, pq );
+   v3_muladds( pco, localplayer.holdout_pose.root_co, 
+               localplayer.holdout_time, pco );
+   v3_lerp( cc->tpv_lpf, pco, 20.0f*vg.time_frame_delta, cc->tpv_lpf );
+
+   /* now move into world */
+   v3f tpv_pos, tpv_offset, tpv_origin;
+
+   /* TODO: whats up with CC and not CC but both sets of variables are doing
+    *       the same ideas just saved in different places?
+    */
+   /* origin */
+   q_mulv( pq, cc->tpv_offset_smooth, tpv_origin );
+   v3_add( tpv_origin, cc->tpv_lpf, tpv_origin );
+
+   /* offset */
+   v3_muls( camera_follow_dir, localplayer.cam_dist_smooth, tpv_offset );
+   v3_muladds( tpv_offset, cc->cam_velocity_smooth, -0.025f, tpv_offset );
+
+   v3_add( tpv_origin, tpv_offset, tpv_pos );
+
+#if 0
+   if( localplayer.subsystem == k_player_subsystem_walk )
+   {
+      v3f fwd, right;
+      v3_angles_vector( localplayer.angles, fwd );
+      v3_cross( fwd, (v3f){0,1.001f,0}, right );
+      right[1] = 0.0f;
+      v3_normalize( right );
+      v3_muladds( tpv_pos, right, 0.5f, tpv_pos );
+   }
+#endif
+
+   /* 
+    * Blend cameras 
+    */
+   v3_lerp( tpv_pos, fpv_pos, cc->camera_type_blend, localplayer.cam.pos );
+   v3_copy( localplayer.angles, localplayer.cam.angles );
+
+   /* Camera shake */
+   f32 speed = v3_length(localplayer.rb.v),
+       strength = k_cam_shake_strength * speed;
+   localplayer.cam_trackshake += 
+      speed*k_cam_shake_trackspeed*vg.time_frame_delta;
+
+   v2f rnd = {vg_perlin_fract_1d( localplayer.cam_trackshake, 1.0f, 4, 20 ),
+              vg_perlin_fract_1d( localplayer.cam_trackshake, 1.0f, 4, 63 ) };
+   v2_muladds( localplayer.cam.angles, rnd, strength, localplayer.cam.angles );
+
+   v3f Fd, Fs, F;
+   v3_muls( localplayer.cam_land_punch_v, -k_cam_damp, Fd );
+   v3_muls( localplayer.cam_land_punch, -k_cam_spring, Fs );
+   v3_muladds( localplayer.cam_land_punch, localplayer.cam_land_punch_v,
+               vg.time_frame_delta, localplayer.cam_land_punch );
+   v3_add( Fd, Fs, F );
+   v3_muladds( localplayer.cam_land_punch_v, F, vg.time_frame_delta,
+               localplayer.cam_land_punch_v );
+   v3_add( localplayer.cam_land_punch, localplayer.cam.pos, 
+           localplayer.cam.pos );
+
+   /* portal transitions */
+   player_camera_portal_correction();
+}
+
+void player_look( v3f angles, float speed )
+{
+   if( vg_ui.ctx.wants_mouse ) return;
+
+   angles[2] = 0.0f;
+
+   v2f mouse_input;
+   v2_copy( vg.mouse_delta, mouse_input );
+   if( k_invert_y ) mouse_input[1] *= -1.0f;
+   v2_muladds( angles, mouse_input, 0.0025f * speed, angles );
+
+   v2f jlook;
+   joystick_state( k_srjoystick_look, jlook );
+
+   angles[0] += jlook[0] * vg.time_frame_delta * 4.0f * speed;
+   float input_y = jlook[1] * vg.time_frame_delta * 4.0f;
+   if( k_invert_y ) input_y *= -1.0f;
+
+   angles[1] += input_y * speed;
+   angles[1] = vg_clampf( angles[1], -VG_PIf*0.5f, VG_PIf*0.5f );
+}
diff --git a/src/player_common.h b/src/player_common.h
new file mode 100644 (file)
index 0000000..b32faee
--- /dev/null
@@ -0,0 +1,16 @@
+#pragma once
+#include "player_api.h"
+
+static float
+   k_cam_spring            =  20.0f,
+   k_cam_damp              =  6.7f,
+   k_cam_punch             = -1.0f,
+   k_cam_shake_strength    =  0.0001f,
+   k_cam_shake_trackspeed  =  0.2f;
+
+static i32 k_player_debug_info = 0;
+static ui_rect g_player_debugger;
+
+void player_look( v3f angles, float speed );
+void player__cam_iterate(void);
+f32 player_get_heading_yaw(void);
diff --git a/src/player_dead.c b/src/player_dead.c
new file mode 100644 (file)
index 0000000..43b6211
--- /dev/null
@@ -0,0 +1,205 @@
+#include "skaterift.h"
+#include "player_dead.h"
+#include "gui.h"
+
+struct player_dead player_dead;
+struct player_subsystem_interface player_subsystem_dead = {
+   .update = player__dead_update,
+   .post_update = player__dead_post_update,
+   .animate = player__dead_animate,
+   .pose = player__dead_pose,
+   .post_animate = player__dead_post_animate,
+   .im_gui = player__dead_im_gui,
+   .bind = player__dead_bind,
+
+   .animator_data = &player_dead.animator,
+   .animator_size = sizeof(player_dead.animator),
+   .network_animator_exchange = player__dead_animator_exchange,
+   .name = "Dead"
+};
+
+void player__dead_update(void)
+{
+   player_ragdoll_iter( &localplayer.ragdoll );
+
+   world_instance *world = world_current_instance();
+   world_water_player_safe( world, 0.2f );
+}
+
+void player__dead_post_update(void){
+   struct ragdoll_part *part = 
+      &localplayer.ragdoll.parts[ localplayer.id_hip-1 ];
+   struct player_dead *d = &player_dead;
+
+   v3f ext_co;
+   v4f ext_q;
+   rb_extrapolate( &part->rb, ext_co, ext_q );
+
+   v3_lerp( d->co_lpf, ext_co, vg.time_frame_delta*4.0f, d->co_lpf );
+   v3_lerp( d->v_lpf,  part->rb.v,  vg.time_frame_delta*4.0f, d->v_lpf );
+   v3_lerp( d->w_lpf,  part->rb.w,  vg.time_frame_delta*4.0f, d->w_lpf );
+   
+   v3_copy( d->co_lpf, localplayer.rb.co );
+   v3_zero( localplayer.rb.v );
+   v3_zero( localplayer.rb.w );
+
+   if( (skaterift.activity == k_skaterift_default) && 
+         button_down(k_srbind_dead_respawn) ){
+      ent_spawn *spawn = world_find_closest_spawn( 
+            world_current_instance(), localplayer.rb.co );
+
+      if( spawn ){
+         v3_copy( spawn->transform.co, localplayer.rb.co );
+         player__reset();
+         srinput.state = k_input_state_resume;
+      }
+      else {
+         vg_error( "No spawns!\n" );
+      }
+   }
+}
+
+void player__dead_animate(void){
+   struct player_dead *d = &player_dead;
+   struct player_dead_animator *animator = &d->animator;
+   struct player_ragdoll *rd = &localplayer.ragdoll;
+   struct skeleton *sk = &localplayer.skeleton;
+
+   m4x3f transforms[ 32 ];
+
+   /* root transform */
+   q_m3x3( localplayer.rb.q, transforms[0] );
+   v3_copy( localplayer.rb.co, transforms[0][3] );
+
+   v4_copy( localplayer.rb.q, animator->transforms[0].q );
+   v3_copy( localplayer.rb.co, animator->transforms[0].co );
+
+   /* colliders with bones transforms */
+   for( int i=0; i<rd->part_count; i++ ){
+      struct ragdoll_part *part = &rd->parts[i];
+
+      m4x3f mtx;
+
+      v4f q_int;
+      v3f co_int;
+
+      float substep = vg.time_fixed_extrapolate;
+      v3_lerp( part->prev_co, part->rb.co, substep, co_int );
+      q_nlerp( part->prev_q, part->rb.q, substep, q_int );
+      v4_copy( part->rb.q, q_int );
+
+      q_m3x3( q_int, mtx );
+      v3_copy( co_int, mtx[3] );
+
+      m4x3_mul( mtx, part->inv_collider_mtx, transforms[part->bone_id] );
+   }
+
+   /* bones without colliders transforms */
+   for( u32 i=1; i<sk->bone_count; i++ ){
+      struct skeleton_bone *sb = &sk->bones[i];
+
+      if( sb->parent && !sb->collider ){
+         v3f delta;
+         v3_sub( sk->bones[i].co, sk->bones[sb->parent].co, delta );
+
+         m4x3f posemtx;
+         m3x3_identity( posemtx );
+         v3_copy( delta, posemtx[3] );
+
+         /* final matrix */
+         m4x3_mul( transforms[sb->parent], posemtx, transforms[i] );
+      }
+   }
+
+   /* measurements */
+   for( u32 i=1; i<sk->bone_count; i++ ){
+      struct skeleton_bone *sb = &sk->bones[i];
+
+      v3_zero( animator->transforms[i].co );
+      q_identity( animator->transforms[i].q );
+
+      m4x3f parent, inverse, local;
+      m3x3_identity( parent );
+      v3_sub( sk->bones[i].co, sk->bones[sb->parent].co, parent[3] );
+      m4x3_mul( transforms[ sb->parent ], parent, parent );
+      m4x3_invert_affine( parent, inverse );
+
+      v3f _s;
+      m4x3_mul( inverse, transforms[i], local );
+      m4x3_decompose( local, animator->transforms[i].co, 
+                             animator->transforms[i].q, _s );
+   }
+}
+
+void player__dead_pose( void *_animator, player_pose *pose )
+{
+   struct player_dead_animator *animator = _animator;
+   struct player_ragdoll *rd = &localplayer.ragdoll;
+   struct skeleton *sk = &localplayer.skeleton;
+
+   pose->type = k_player_pose_type_fk_2;
+   pose->board.lean = 0.0f;
+
+   v3_copy( animator->transforms[0].co, pose->root_co );
+   v4_copy( animator->transforms[0].q, pose->root_q );
+
+   for( u32 i=1; i<sk->bone_count; i++ ){
+      v3_copy( animator->transforms[i].co, pose->keyframes[i-1].co );
+      v4_copy( animator->transforms[i].q, pose->keyframes[i-1].q );
+      v3_fill( pose->keyframes[i-1].s, 1.0f );
+   }
+}
+
+void player__dead_post_animate(void)
+{
+   localplayer.cam_velocity_influence = 1.0f;
+}
+
+void player__dead_im_gui( ui_context *ctx )
+{
+}
+
+void player__dead_transition( enum player_die_type type )
+{
+   if( localplayer.subsystem == k_player_subsystem_dead )
+      return;
+
+   localplayer.subsystem = k_player_subsystem_dead;
+   copy_localplayer_to_ragdoll( &localplayer.ragdoll, type );
+
+   struct ragdoll_part *part = 
+      &localplayer.ragdoll.parts[ localplayer.id_hip-1 ];
+   v3_copy( part->rb.co, player_dead.co_lpf );
+   v3_copy( part->rb.v,  player_dead.v_lpf );
+   v3_copy( part->rb.w,  player_dead.w_lpf );
+
+   gui_helper_clear();
+   vg_str str;
+
+   struct gui_helper *h;
+   if( (h = gui_new_helper(input_button_list[k_srbind_reset], &str) )){
+      vg_strcat( &str, "Rewind" );
+
+      if( world_static.active_instance == k_world_purpose_hub )
+         h->greyed = 1;
+   }
+
+   if( gui_new_helper(input_button_list[k_srbind_dead_respawn], &str ))
+      vg_strcat( &str, "Spawn" );
+}
+
+void player__dead_animator_exchange( bitpack_ctx *ctx, void *data )
+{
+   struct player_dead_animator *animator = data;
+
+   for( u32 i=0; i<localplayer.skeleton.bone_count; i ++ ){
+      bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->transforms[i].co );
+      bitpack_qquat( ctx, animator->transforms[i].q );
+   }
+}
+
+void player__dead_bind(void)
+{
+   struct skeleton *sk = &localplayer.skeleton;
+   player_dead.anim_bail = skeleton_get_anim( sk, "pose_bail_ball" );
+}
diff --git a/src/player_dead.h b/src/player_dead.h
new file mode 100644 (file)
index 0000000..93b0cc3
--- /dev/null
@@ -0,0 +1,32 @@
+#pragma once
+#include "player.h"
+#include "player_api.h"
+
+struct player_dead
+{
+   v3f co_lpf, v_lpf, w_lpf;
+
+   struct player_dead_animator{
+      struct {
+         v3f co;
+         v4f q;
+      }
+      transforms[ 32 ];
+   }
+   animator;
+
+   struct skeleton_anim *anim_bail;
+}
+extern player_dead;
+extern struct player_subsystem_interface player_subsystem_dead;
+
+void player__dead_update      (void);
+void player__dead_post_update (void);
+void player__dead_animate     (void);
+void player__dead_pose        (void *animator, player_pose *pose);
+void player__dead_post_animate(void);
+void player__dead_im_gui      ( ui_context *ctx );
+void player__dead_bind        (void);
+void player__dead_transition  ( enum player_die_type type );
+void player__dead_animator_exchange( bitpack_ctx *ctx, void *data );
+
diff --git a/src/player_drive.c b/src/player_drive.c
new file mode 100644 (file)
index 0000000..0462037
--- /dev/null
@@ -0,0 +1,87 @@
+#include "player_drive.h"
+#include "input.h"
+
+struct player_drive player_drive;
+struct player_subsystem_interface player_subsystem_drive = 
+{
+   .pre_update = player__drive_pre_update,
+   .update = player__drive_update,
+   .post_update = player__drive_post_update,
+   .animate = player__drive_animate,
+   .pose = player__drive_pose,
+   .post_animate = player__drive_post_animate,
+   .im_gui = player__drive_im_gui,
+   .bind = player__drive_bind,
+
+   .animator_data = NULL,
+   .animator_size = 0,
+   .name = "Drive"
+};
+
+void player__drive_pre_update(void)
+{
+   drivable_vehicle *vehc = player_drive.vehicle;
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   vehc->steer = vg_lerpf( vehc->steer, steer[0] * 0.4f, 
+                           vg.time_fixed_delta * 8.0f );
+   vehc->drive = steer[1];
+}
+
+void player__drive_update(void){}
+
+void player__drive_post_update(void)
+{
+   v3_copy( player_drive.vehicle->rb.co,localplayer.rb.co );
+   v3_copy( player_drive.vehicle->rb.v, localplayer.rb.v );
+   v4_copy( player_drive.vehicle->rb.q, localplayer.rb.q );
+   v3_copy( player_drive.vehicle->rb.w, localplayer.rb.w );
+}
+
+void player__drive_animate(void){}
+
+void player__drive_pose( void *animator, player_pose *pose )
+{
+   struct skeleton *sk = &localplayer.skeleton;
+
+   skeleton_sample_anim( sk, player_drive.anim_drive, 0.0f, pose->keyframes );
+   v3_copy( localplayer.rb.co, pose->root_co );
+   v4_copy( localplayer.rb.q, pose->root_q );
+}
+
+void player__drive_post_animate(void)
+{
+   if( localplayer.cam_control.camera_mode == k_cam_firstperson )
+      localplayer.cam_velocity_influence = 0.0f;
+   else
+      localplayer.cam_velocity_influence = 1.0f;
+
+   rigidbody *rb = &gzoomer.rb;
+   float yaw = atan2f( -rb->to_world[2][0], rb->to_world[2][2] ),
+       pitch = atan2f
+               ( 
+                   -rb->to_world[2][1], 
+                   sqrtf
+                   (
+                     rb->to_world[2][0]*rb->to_world[2][0] + 
+                     rb->to_world[2][2]*rb->to_world[2][2]
+                   )
+               );
+
+   localplayer.angles[0] = yaw;
+   localplayer.angles[1] = pitch;
+}
+
+void player__drive_im_gui( ui_context *ctx )
+{
+   player__debugtext( ctx, 1, "Nothing here" );
+}
+
+void player__drive_bind(void)
+{
+   struct skeleton *sk = &localplayer.skeleton;
+   player_drive.vehicle = &gzoomer;
+   player_drive.anim_drive = skeleton_get_anim( sk, "idle_cycle+y" );
+}
diff --git a/src/player_drive.h b/src/player_drive.h
new file mode 100644 (file)
index 0000000..9a5649d
--- /dev/null
@@ -0,0 +1,21 @@
+#pragma once
+#include "player.h"
+#include "vehicle.h"
+
+struct player_drive 
+{
+   drivable_vehicle *vehicle;
+   struct skeleton_anim *anim_drive;
+}
+extern player_drive;
+extern struct player_subsystem_interface player_subsystem_drive;
+
+void player__drive_pre_update(void);
+void player__drive_update(void);
+void player__drive_post_update(void);
+void player__drive_animate(void);
+void player__drive_pose( void *animator, player_pose *pose );
+
+void player__drive_post_animate(void);
+void player__drive_im_gui( ui_context *ctx );
+void player__drive_bind(void);
diff --git a/src/player_effects.c b/src/player_effects.c
new file mode 100644 (file)
index 0000000..981c232
--- /dev/null
@@ -0,0 +1,37 @@
+#include "player.h"
+#include "player_effects.h"
+#include "player_render.h"
+#include "particle.h"
+
+void effect_blink_apply( effect_blink *ef, player_pose *pose, f32 dt )
+{
+   if( ef->t < 0.0f ){
+      ef->t = (1.0f-powf(vg_randf64(&vg.rand),4.0f))*4.0f;
+      ef->l = 0.08f;
+   }
+
+   pose->keyframes[ localplayer.id_eyes-1 ].s[1] = ef->l > 0.0f? 0.2f: 1.0f;
+
+   ef->t -= dt;
+   ef->l -= dt;
+}
+
+void effect_spark_apply( effect_spark *ef, v3f co, v3f v, f32 dt )
+{
+   if( !ef->colour ) return;
+
+   if( ef->t < 0.0f ){
+      ef->t += 0.05f+vg_randf64(&vg.rand)*0.1f;
+
+      v3f dir;
+      v3_copy( v, dir );
+      dir[1] += 1.0f;
+      f32 l = v3_length(dir);
+      v3_muls( dir, 1.0f/l, dir );
+
+      particle_spawn_cone( &particles_grind, co, dir, VG_PIf/2.0f, l, 
+                           4.0f, ef->colour );
+   }
+   else
+      ef->t -= dt;
+}
diff --git a/src/player_effects.h b/src/player_effects.h
new file mode 100644 (file)
index 0000000..f148dbc
--- /dev/null
@@ -0,0 +1,26 @@
+#pragma once
+#include "vg/vg_platform.h"
+#include "player_render.h"
+
+typedef struct effect_blink effect_blink;
+typedef struct effect_spark effect_spark;
+
+struct effect_blink 
+{
+   f32 t, l;
+};
+
+struct effect_spark 
+{
+   u32 colour;
+   f32 t;
+};
+
+void effect_blink_apply( effect_blink *ef, player_pose *pose, f32 dt );
+void effect_spark_apply( effect_spark *ef, v3f co, v3f v, f32 dt );
+
+struct player_effects_data 
+{
+   effect_blink blink;
+   effect_spark spark, sand;
+};
diff --git a/src/player_glide.c b/src/player_glide.c
new file mode 100644 (file)
index 0000000..d9977ce
--- /dev/null
@@ -0,0 +1,706 @@
+#include "player_glide.h"
+#include "vg/vg_rigidbody.h"
+#include "scene_rigidbody.h"
+#include "shaders/model_board_view.h"
+#include "shaders/model_entity.h"
+#include "input.h"
+#include "skaterift.h"
+
+#include "player_dead.h"
+#include "player_skate.h"
+
+trail_system trails_glider[] = {
+   {
+      .width = 0.035f,
+      .lifetime = 5.0f,
+      .min_dist = 0.5f
+   },
+   {
+      .width = 0.035f,
+      .lifetime = 5.0f,
+      .min_dist = 0.5f
+   },
+};
+
+struct player_glide player_glide =
+{
+   .parts = {
+      {
+         .co    = { 1.0f, 0.5f, -1.0f },
+         .euler = { VG_TAUf*0.25f,  VG_TAUf*0.125f, 0.0f },
+         .shape = k_rb_shape_capsule,
+         .inf   = { .h = 2.82842712475f, .r = 0.25f },
+      },
+      {
+         .co    = { -1.0f, 0.5f, -1.0f },
+         .euler = { VG_TAUf*0.25f, -VG_TAUf*0.125f, 0.0f },
+         .shape = k_rb_shape_capsule,
+         .inf   = { .h = 2.82842712475f, .r = 0.25f },
+      },
+      {
+         .co    = {  0.0f, 0.5f, 1.0f },
+         .euler = { VG_TAUf*0.25f, VG_TAUf*0.25f, 0.0f },
+         .shape = k_rb_shape_capsule,
+         .inf   = { .h = 6.0f, .r = 0.25f },
+      },
+      {
+         .co    = {  0.0f, -0.5f, 0.0f },
+         .euler = { VG_TAUf*0.25f, VG_TAUf*0.25f, 0.0f },
+         .shape = k_rb_shape_capsule,
+         .inf   = { .h = 2.0f, .r = 0.25f },
+         .is_damage = 1,
+      },
+   }
+};
+
+struct player_subsystem_interface player_subsystem_glide = 
+{
+   .pre_update = player_glide_pre_update,
+   .update = player_glide_update,
+   .post_update = player_glide_post_update,
+   .animate = player_glide_animate,
+   .pose = player_glide_pose,
+   .post_animate = player_glide_post_animate,
+   .network_animator_exchange = player_glide_animator_exchange,
+   .im_gui = player_glide_im_gui,
+   .bind = player_glide_bind,
+
+   .animator_data = &player_glide.animator,
+   .animator_size = sizeof(player_glide.animator),
+   .name = "Glide"
+};
+
+static f32 k_glide_steer = 2.0f,
+           k_glide_cl = 0.04f,
+           k_glide_cs = 0.02f,
+           k_glide_drag = 0.0001f,
+           k_glide_slip_yaw = 0.1f,
+           k_glide_lift_pitch = 0.0f,
+           k_glide_wing_orient = -0.1f,
+           k_glide_balance = 1.0f;
+
+static i32 k_glide_pause = 0;
+
+void player_glide_pre_update(void)
+{
+   if( button_down(k_srbind_use) ){
+      localplayer.subsystem = k_player_subsystem_skate;
+      localplayer.glider_orphan = 1;
+
+      player_skate.state.activity = k_skate_activity_air;
+      player_skate.state.activity_prev = k_skate_activity_air;
+
+      q_mulv( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, player_skate.state.cog );
+      v3_add( player_skate.state.cog, localplayer.rb.co, 
+              player_skate.state.cog );
+      v3_copy( localplayer.rb.v, player_skate.state.cog_v );
+
+      player__begin_holdout( (v3f){0.0f,0.0f,0.0f} );
+      player__skate_reset_animator();
+      player__skate_clear_mechanics();
+      v3_copy( (v3f){0.0f,0.0f,0.0f}, player_skate.state.trick_euler );
+
+      player__approximate_best_trajectory();
+   }
+}
+
+static void massless_accel( rigidbody *rb, v3f delta, v3f impulse ){
+   /* linear */
+   v3_muladds( rb->v, impulse, vg.time_fixed_delta, rb->v );
+   
+   /* Angular velocity */
+   v3f wa;
+   v3_cross( delta, impulse, wa );
+   v3_muladds( rb->w, wa, vg.time_fixed_delta, rb->w );
+}
+
+static void calculate_lift( v3f vl, f32 aoa_bias, 
+                            v3f axis, v3f back, f32 power,
+                            v3f out_force ){
+   v3f up;
+   v3_cross( back, axis, up );
+
+   v3f wind;
+   v3_muladds( vl, axis, -v3_dot(axis,vl), wind );
+   
+   f32 windv2 = v3_length2(wind),
+       aoa    = atan2f( v3_dot( up, wind ), v3_dot( back, wind ) ) + aoa_bias,
+       cl     = aoa / VG_PIf,
+       L      = windv2 * cl * power;
+
+   v3f lift_dir;
+   v3_normalize( wind );
+   v3_cross( wind, axis, lift_dir );
+
+   /* this is where induced drag (from the flappy things) would go */
+   
+   v3_muls( lift_dir, L, out_force );
+}
+
+static void calculate_drag( v3f vl, f32 cd, v3f out_force ){
+   f32 v2 = v3_length2( vl );
+   v3f dir;
+   v3_copy( vl, dir );
+   v3_normalize( dir );
+   v3_muls( vl, -cd*v2, out_force );
+}
+
+/*
+ * Returns true if the bottom sphere is hit 
+ */
+bool glider_physics( v2f steer )
+{
+   rigidbody *rb = &player_glide.rb;
+
+   /* lift */
+   v3f vl, wl;
+   m3x3_mulv( rb->to_local, rb->v, vl );
+   m3x3_mulv( rb->to_local, rb->w, wl );
+   
+   v3f F, Flift, Fslip, Fdrag, FslipW, FliftW;
+
+   calculate_lift( vl, steer[1]*k_glide_steer, 
+                  (v3f){1,0,0},
+                  (v3f){0,sinf(k_glide_wing_orient),cosf(k_glide_wing_orient)},
+                  k_glide_cl, Flift );
+   v3_copy( Flift, player_glide.info_lift );
+   v3_cross( (v3f){0,0,0}, Flift, FliftW );
+   
+   calculate_lift( vl, 0.0f,
+                  (v3f){0,1,0},(v3f){0,0,1},
+                  k_glide_cs, Fslip );
+   v3_copy( Fslip, player_glide.info_slip );
+   v3_cross( (v3f){0,k_glide_lift_pitch,k_glide_slip_yaw}, Fslip, FslipW );
+
+   calculate_drag( vl, k_glide_drag, Fdrag );
+   v3_copy( Fdrag, player_glide.info_drag );
+
+   v3f balance = {0.0f,-k_glide_balance,0.0f};
+   m3x3_mulv( rb->to_local, balance, balance );
+
+   v3f Fw = {
+       steer[1]*k_glide_steer - balance[2],
+       0.0f,
+      -steer[0]*k_glide_steer + balance[0],
+   };
+
+   if( player_glide.ticker ){
+      player_glide.ticker --;
+      return 0;
+   }
+   player_glide.ticker += k_glide_pause;
+
+   /* apply forces */
+   v3_add( Flift, Fslip, F );
+   v3_add( F, Fdrag, F );
+
+   m3x3_mulv( rb->to_world, F, F );
+   v3_muladds( rb->v, F, vg.time_fixed_delta, rb->v );
+
+   v3_add( Fw, FslipW, Fw );
+   v3_add( Fw, FliftW, Fw );
+   m3x3_mulv( rb->to_world, Fw, Fw );
+   v3_muladds( rb->w, Fw, vg.time_fixed_delta, rb->w );
+
+
+   /* 
+    * collisions & constraints
+    */
+   world_instance *world = world_current_instance();
+   rb_solver_reset();
+
+   bool bottom_hit = 0;
+
+   rigidbody _null = {0};
+   _null.inv_mass = 0.0f;
+   m3x3_zero( _null.iI );
+   for( u32 i=0; i < VG_ARRAY_LEN(player_glide.parts); i ++ ){
+      m4x3f mmdl;
+      m4x3_mul( rb->to_world, player_glide.parts[i].mdl, mmdl );
+
+      if( player_glide.parts[i].shape == k_rb_shape_capsule ){
+         vg_line_capsule( mmdl,
+                          player_glide.parts[i].inf.r,
+                          player_glide.parts[i].inf.h,
+                          VG__BLACK );
+      }
+      else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
+         vg_line_sphere( mmdl, player_glide.parts[i].r, 0 );
+      }
+
+      if( rb_global_has_space() ){
+         rb_ct *buf = rb_global_buffer();
+
+         u32 l = 0;
+         
+         if( player_glide.parts[i].shape == k_rb_shape_capsule ){
+            l = rb_capsule__scene( mmdl, &player_glide.parts[i].inf,
+                                   NULL, world->geo_bh, buf, 
+                                   k_material_flag_ghosts );
+         }
+         else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
+            l = rb_sphere__scene( mmdl, player_glide.parts[i].r,
+                                  NULL, world->geo_bh, buf,
+                                  k_material_flag_ghosts );
+         }
+
+         if( player_glide.parts[i].is_damage && l ){
+            bottom_hit = 1;
+         }
+
+         for( u32 j=0; j<l; j ++ ){
+            buf[j].rba = rb;
+            buf[j].rbb = &_null;
+         }
+
+         rb_contact_count += l;
+      }
+   }
+
+   rb_presolve_contacts( rb_contact_buffer, 
+                         vg.time_fixed_delta, rb_contact_count );
+   for( u32 i=0; i<10; i ++ )
+      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
+   
+   rb_iter( rb );
+   rb_update_matrices( rb );
+
+   return bottom_hit;
+}
+
+void player_glide_update(void)
+{
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   if( glider_physics( steer ) )
+   {
+      vg_info( "player fell off due to glider hitting ground\n" );
+      player__dead_transition( k_player_die_type_generic );
+      localplayer.glider_orphan = 1;
+   }
+
+   if( !world_water_player_safe( world_current_instance(), 1.0f ) )
+      return;
+}
+
+void player_glide_post_update(void)
+{
+   v3_copy( player_glide.rb.co, localplayer.rb.co );
+   v4_copy( player_glide.rb.q, localplayer.rb.q );
+   v3_copy( player_glide.rb.v, localplayer.rb.v );
+   v3_copy( player_glide.rb.w, localplayer.rb.w );
+   rb_update_matrices( &localplayer.rb );
+}
+
+void player_glide_animate(void)
+{
+   struct player_glide *g = &player_glide;
+   struct player_glide_animator *animator = &g->animator;
+   rb_extrapolate( &localplayer.rb, animator->root_co, animator->root_q );
+}
+
+void player_glide_pose( void *_animator, player_pose *pose )
+{
+   struct skeleton *sk = &localplayer.skeleton;
+   struct player_glide_animator *animator = _animator;
+   pose->type = k_player_pose_type_ik;
+   pose->board.lean = 0.0f;
+
+   skeleton_sample_anim( sk, player_glide.anim_glide, 0.0f, pose->keyframes );
+
+   v3f temp;
+   q_mulv( animator->root_q, (v3f){0,-0.5f,0}, temp );
+   v3_add( animator->root_co, temp, pose->root_co );
+
+   v4_copy( animator->root_q, pose->root_q );
+}
+
+void player_glide_post_animate(void)
+{
+   if( localplayer.cam_control.camera_mode == k_cam_firstperson )
+      localplayer.cam_velocity_influence = 0.0f;
+   else
+      localplayer.cam_velocity_influence = 0.0f;
+
+   v3f fwd;
+   v3_muls( localplayer.rb.to_world[2], -1.0f, fwd );
+   v3_angles( fwd, localplayer.angles );
+
+   localplayer.cam_dist = 2.0f + v3_length( localplayer.rb.v )*0.2f;
+}
+
+void player_glide_animator_exchange( bitpack_ctx *ctx, void *data )
+{
+   struct player_glide_animator *animator = data;
+
+   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
+   bitpack_qquat( ctx, animator->root_q );
+}
+
+void player_glide_remote_animator_exchange( bitpack_ctx *ctx, void *data )
+{
+   struct remote_glider_animator *animator = data;
+
+   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
+   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->s );
+   bitpack_qquat( ctx, animator->root_q );
+}
+
+void player_glide_im_gui( ui_context *ctx )
+{
+   player__debugtext( ctx, 1, " lift: %.2f %.2f %.2f", 
+                           player_glide.info_lift[0],
+                           player_glide.info_lift[1],
+                           player_glide.info_lift[2] );
+   player__debugtext( ctx, 1, " slip: %.2f %.2f %.2f", 
+                           player_glide.info_slip[0],
+                           player_glide.info_slip[1],
+                           player_glide.info_slip[2] );
+   player__debugtext( ctx, 1, " drag: %.2f %.2f %.2f", 
+                           player_glide.info_drag[0],
+                           player_glide.info_drag[1],
+                           player_glide.info_drag[2] );
+}
+
+void player_glide_equip_glider(void)
+{
+   if( !localplayer.have_glider ){
+      localplayer.have_glider = 1;
+      localplayer.glider_orphan = 0;
+      player_glide.t = -1.0f;
+   }
+}
+
+static int ccmd_player_glider_spawn( int argc, const char *argv[] ){
+   if( vg_console.cheats ){
+      player_glide_equip_glider();
+   }
+   else {
+      vg_error( "Can't spawn without cheats enabled.\n" );
+   }
+   return 0;
+}
+
+void player_glide_bind(void)
+{
+   u32 mask = VG_VAR_CHEAT|VG_VAR_PERSISTENT;
+   VG_VAR_F32( k_glide_steer, flags=mask );
+   VG_VAR_F32( k_glide_cl, flags=mask );
+   VG_VAR_F32( k_glide_cs, flags=mask );
+   VG_VAR_F32( k_glide_drag, flags=mask );
+   VG_VAR_F32( k_glide_slip_yaw, flags=mask );
+   VG_VAR_F32( k_glide_lift_pitch, flags=mask );
+   VG_VAR_I32( k_glide_pause, flags=mask );
+   VG_VAR_F32( k_glide_balance, flags=mask );
+   VG_VAR_F32( k_glide_wing_orient, flags=mask );
+
+   vg_console_reg_cmd( "spawn_glider", ccmd_player_glider_spawn, NULL );
+
+   f32 mass = 0.0f,k_density = 8.0f;
+   m3x3f I;
+   m3x3_zero( I );
+
+   for( u32 i=0; i<VG_ARRAY_LEN(player_glide.parts); i ++ ){
+      /* create part transform matrix */
+      v4f qp, qy, qr, q;
+      q_axis_angle( qp, (v3f){1,0,0}, player_glide.parts[i].euler[0] );
+      q_axis_angle( qy, (v3f){0,1,0}, player_glide.parts[i].euler[1] );
+      q_axis_angle( qr, (v3f){0,0,1}, player_glide.parts[i].euler[2] );
+
+      q_mul( qr, qy, q );
+      q_mul( q, qp, q );
+
+      q_m3x3( q, player_glide.parts[i].mdl );
+      v3_copy( player_glide.parts[i].co, player_glide.parts[i].mdl[3] );
+
+      /* add it to inertia model */
+
+      if( player_glide.parts[i].shape == k_rb_shape_capsule ){
+         f32 r  = player_glide.parts[i].inf.r,
+             h  = player_glide.parts[i].inf.h,
+             pv = vg_capsule_volume( r, h ),
+             pm = pv * k_density;
+
+         mass += pm;
+
+         m3x3f pI;
+         vg_capsule_inertia( r, h, pm, pI );
+         vg_rotate_inertia( pI, player_glide.parts[i].mdl );
+         vg_translate_inertia( pI, pm, player_glide.parts[i].co );
+         m3x3_add( I, pI, I );
+      }
+      else if( player_glide.parts[i].shape == k_rb_shape_sphere ){
+         f32 r  = player_glide.parts[i].r,
+             pv = vg_sphere_volume( r ),
+             pm = pv * k_density;
+
+         mass += pm;
+         m3x3f pI;
+         vg_sphere_inertia( r, pm, pI );
+         vg_translate_inertia( pI, pm, player_glide.parts[i].co );
+         m3x3_add( I, pI, I );
+      }
+   }
+
+   /* set inverses */
+   m3x3_inv( I, player_glide.rb.iI );
+   player_glide.rb.inv_mass = 1.0f / mass;
+
+   /* resources */
+   struct skeleton *sk = &localplayer.skeleton;
+   player_glide.anim_glide = skeleton_get_anim( sk, "glide_pose" );
+
+   void *alloc = vg_mem.rtmemory;
+   mdl_context *mdl = &player_glide.glider;
+
+   mdl_open( mdl, "models/glider.mdl", alloc );
+   mdl_load_metadata_block( mdl, alloc );
+   mdl_async_full_load_std( mdl );
+
+   /* load trail positions */
+   mdl_array_ptr markers;
+   MDL_LOAD_ARRAY( mdl, &markers, ent_marker, vg_mem.scratch );
+   mdl_close( mdl );
+
+   for( u32 i=0; i<mdl_arrcount( &markers ); i ++ )
+   {
+      ent_marker *marker = mdl_arritm( &markers, i );
+      v3_copy( marker->transform.co, 
+               player_glide.trail_positions[ player_glide.trail_count ++ ] );
+
+      if( player_glide.trail_count == VG_ARRAY_LEN(trails_glider) )
+         break;
+   }
+
+   /* allocate effects */
+   for( u32 i=0; i<VG_ARRAY_LEN(trails_glider); i ++ )
+   {
+      trail_alloc( &trails_glider[i], 200 );
+   }
+}
+
+void player_glide_transition(void)
+{
+   localplayer.subsystem = k_player_subsystem_glide;
+   localplayer.have_glider = 0;
+   world_static.challenge_target = NULL;
+   world_static.challenge_timer = 0.0f;
+   world_static.focused_entity = 0;
+   world_static.last_use = 0.0;
+   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
+      world_instance *instance = &world_static.instances[i];
+      if( instance->status == k_world_status_loaded ){
+         world_routes_clear( instance );
+      }
+   }
+
+   v3_copy( localplayer.rb.co, player_glide.rb.co );
+
+   f32 dir = v3_dot( localplayer.rb.v, localplayer.rb.to_world[2] );
+
+   if( dir > 0.0f ){
+      v4f qyaw;
+      q_axis_angle( qyaw, (v3f){0,1,0}, VG_TAUf*0.5f );
+      q_mul( qyaw, localplayer.rb.q, player_glide.rb.q );
+      q_normalize( player_glide.rb.q );
+   }
+   else 
+      v4_copy( localplayer.rb.q,  player_glide.rb.q );
+
+   v3_copy( localplayer.rb.v,  player_glide.rb.v );
+   v3_copy( localplayer.rb.w,  player_glide.rb.w );
+   rb_update_matrices( &player_glide.rb );
+
+   player__begin_holdout( (v3f){0,0,0} );
+}
+
+void render_glider_model( vg_camera *cam, world_instance *world,
+                          m4x3f mmdl, enum board_shader shader )
+{
+   u32 current_mat = 0xffffffff;
+   glActiveTexture( GL_TEXTURE0 );
+
+   mdl_context *mdl = &player_glide.glider;
+   mesh_bind( &player_glide.glider.mesh );
+
+   for( u32 i=0; i<mdl_arrcount(&mdl->meshs); i ++ )
+   {
+      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
+
+      m4x3f mmmdl;
+      mdl_transform_m4x3( &mesh->transform, mmmdl );
+      m4x3_mul( mmdl, mmmdl, mmmdl );
+
+      if( shader == k_board_shader_player )
+         shader_model_board_view_uMdl( mmmdl );
+      else if( shader == k_board_shader_entity )
+      {
+         m4x4f m4mmmdl;
+         m4x3_expand( mmmdl, m4mmmdl );
+         m4x4_mul( cam->mtx_prev.pv, m4mmmdl, m4mmmdl );
+
+         shader_model_entity_uMdl( mmmdl );
+         shader_model_entity_uPvmPrev( m4mmmdl );
+      }
+
+      for( u32 j=0; j<mesh->submesh_count; j ++ )
+      {
+         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, mesh->submesh_start+j );
+         if( !sm->material_id ) 
+         {
+            vg_error( "Invalid material ID 0\n" );
+            continue;
+         }
+
+         if( sm->material_id != current_mat )
+         {
+            mdl_material *mat = mdl_arritm( &mdl->materials,sm->material_id-1 );
+            GLuint tex = vg.tex_missing;
+
+            if( mat->shader == k_shader_standard )
+            {
+               struct shader_props_standard *props = mat->props.compiled;
+
+               u32 index = props->tex_diffuse-1;
+               mdl_texture *ptex = mdl_arritm( &mdl->textures, index );
+               tex = ptex->glname;
+            }
+
+            glBindTexture( GL_TEXTURE_2D, tex );
+            current_mat = sm->material_id;
+         }
+
+         mdl_draw_submesh( sm );
+      }
+   }
+}
+
+/*
+ * TODO: more flexible way to call
+ *      - this depends on the current state, but we need to pass a struct in
+ *        that can hold that information instead so we can save it into 
+ *        the replay
+ */
+void player_glide_render( vg_camera *cam, world_instance *world,
+                          player_pose *pose )
+{
+   if( !((localplayer.subsystem == k_player_subsystem_glide) ||
+         (localplayer.observing_system == k_player_subsystem_glide) ||
+          localplayer.have_glider ||
+          localplayer.glider_orphan) )
+      return;
+
+   shader_model_board_view_use();
+   shader_model_board_view_uTexMain( 0 );
+   shader_model_board_view_uCamera( cam->transform[3] );
+   shader_model_board_view_uPv( cam->mtx.pv );
+   shader_model_board_view_uDepthMode(1);
+   depth_compare_bind(
+      shader_model_board_view_uTexSceneDepth,
+      shader_model_board_view_uInverseRatioDepth,
+      shader_model_board_view_uInverseRatioMain,
+      cam );
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_board_view );
+
+   mdl_keyframe kf_res;
+   if( localplayer.glider_orphan ){
+      rb_extrapolate( &player_glide.rb, kf_res.co, kf_res.q );
+      v3_fill( kf_res.s, 1.0f );
+
+      v3f temp;
+      q_mulv( kf_res.q, (v3f){0,-0.5f,0}, temp );
+      v3_add( temp, kf_res.co, kf_res.co );
+   }
+   else {
+      f32 target;
+      if( localplayer.subsystem == k_player_subsystem_glide ) target = 1.0f;
+      else target = 0.0f;
+
+      /* TODO: TEMP */
+      if( skaterift.activity != k_skaterift_replay )
+         vg_slewf( &player_glide.t, target, vg.time_frame_delta * 4.0f );
+
+      mdl_keyframe kf_backpack;
+
+      struct skeleton *sk = &localplayer.skeleton;
+      m4x3_mulv( localplayer.final_mtx[localplayer.id_chest ],
+            sk->bones[localplayer.id_chest].co, 
+            kf_backpack.co );
+
+      v4f qyaw, qpitch, qchest, q;
+      q_axis_angle( qyaw,   (v3f){0,1,0}, VG_TAUf*0.25f );
+      q_axis_angle( qpitch, (v3f){1,0,0}, VG_TAUf*0.25f );
+      m3x3_q( localplayer.final_mtx[ localplayer.id_chest ], qchest );
+
+      q_mul( qyaw, qpitch, q );
+      q_mul( qchest, q, kf_backpack.q );
+      q_normalize( kf_backpack.q );
+
+      f32 scale;
+      if( player_glide.t <= 0.0f ){
+         f32 st  = player_glide.t + 1.0f,
+             sst = vg_smoothstepf(st),
+             isst= 1.0f - sst;
+         scale = vg_lerpf( 0.0f, 0.2f, sst );
+
+         v4f qspin;
+         q_axis_angle( qspin, (v3f){0,0,1}, VG_TAUf * isst * 0.5f );
+         q_mul( kf_backpack.q, qspin, kf_backpack.q );
+         kf_backpack.co[1] += isst * 1.0f;
+         v3_muladds( kf_backpack.co, 
+                     localplayer.final_mtx[ localplayer.id_chest ][0],
+                     isst * 0.25f,
+                     kf_backpack.co );
+      }
+      else{
+         scale = vg_lerpf( 0.2f, 1.0f, vg_smoothstepf(player_glide.t) );
+      }
+
+
+      v3_fill( kf_backpack.s, scale );
+
+      v3_copy( pose->root_co, kf_res.co );
+      v4_copy( pose->root_q, kf_res.q );
+      v3_fill( kf_res.s, scale );
+
+      f32 blend = vg_smoothstepf( vg_maxf( 0, player_glide.t ) );
+      keyframe_lerp( &kf_backpack, &kf_res, blend, &kf_res );
+   }
+      
+   m4x3f mmdl;
+   q_m3x3( kf_res.q, mmdl );
+   m3x3_scale( mmdl, kf_res.s );
+   v3_copy( kf_res.co, mmdl[3] );
+
+   render_glider_model( cam, world, mmdl, k_board_shader_player );
+
+   /* totally FUCKED */
+   v4_copy( kf_res.q, player_glide.remote_animator.root_q );
+   v3_copy( kf_res.co, player_glide.remote_animator.root_co );
+   player_glide.remote_animator.s = kf_res.s[0];
+}
+
+void player_glide_render_effects( vg_camera *cam )
+{
+   v3f co, temp;
+   v4f q;
+   rb_extrapolate( &player_glide.rb, co, q );
+   q_mulv( q, (v3f){0,-0.5f,0}, temp );
+   v3_add( temp, co, co );
+
+   f32 alpha = vg_maxf( (fabsf(player_glide.info_lift[2])-1.0f), 0.0f ) /18.0f;
+
+   for( u32 i=0; i<player_glide.trail_count; i ++ ){
+      v3f vvert;
+      q_mulv( q, player_glide.trail_positions[i], vvert );
+      v3_add( co, vvert, vvert );
+      
+      trail_system_update( &trails_glider[i], vg.time_delta, vvert,
+                           localplayer.rb.to_world[1], alpha );
+                           
+      trail_system_prerender( &trails_glider[i] );
+      trail_system_render( &trails_glider[i], &g_render.cam );
+   }
+}
diff --git a/src/player_glide.h b/src/player_glide.h
new file mode 100644 (file)
index 0000000..03c8260
--- /dev/null
@@ -0,0 +1,77 @@
+#pragma once
+#include "player.h"
+#include "player_render.h"
+#include "trail.h"
+
+struct player_glide 
+{
+   struct skeleton_anim *anim_glide;
+
+   struct player_glide_animator 
+   {
+      v3f root_co;
+      v4f root_q;
+   }
+   animator;
+
+   /* this sucks */
+   struct remote_glider_animator 
+   {
+      v3f root_co;
+      v4f root_q;
+      f32 s;
+   }
+   remote_animator;
+
+   v3f info_lift,
+       info_slip,
+       info_drag;
+
+   u32 ticker;
+
+   rigidbody rb;
+
+   f32 t;
+
+   struct {
+      v3f co, euler;
+      m4x3f mdl;
+      
+      union {
+         rb_capsule inf;
+         f32 r;
+      };
+
+      enum rb_shape shape;
+      bool is_damage;
+   }
+   parts[4];
+
+   u32 trail_count;
+   v3f trail_positions[2];
+
+   mdl_context glider;
+}
+extern player_glide;
+extern struct player_subsystem_interface player_subsystem_glide;
+
+void player_glide_pre_update(void);
+void player_glide_update(void);
+void player_glide_post_update(void);
+void player_glide_animate(void);
+void player_glide_pose( void *animator, player_pose *pose );
+
+void player_glide_post_animate(void);
+void player_glide_im_gui( ui_context *ctx );
+void player_glide_bind(void);
+void player_glide_transition(void);
+bool glider_physics( v2f steer );
+void player_glide_animator_exchange( bitpack_ctx *ctx, void *data );
+void player_glide_render( vg_camera *cam, world_instance *world,
+                          player_pose *pose );
+void render_glider_model( vg_camera *cam, world_instance *world,
+                          m4x3f mmdl, enum board_shader shader );
+void player_glide_remote_animator_exchange( bitpack_ctx *ctx, void *data );
+void player_glide_equip_glider(void);
+void player_glide_render_effects( vg_camera *cam );
+
diff --git a/src/player_model.h b/src/player_model.h
new file mode 100644 (file)
index 0000000..8d5dd80
--- /dev/null
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+#include "model.h"
+#include "skeleton.h"
+#include "player_ragdoll.h"
+
+#include "shaders/model_character_view.h"
diff --git a/src/player_ragdoll.c b/src/player_ragdoll.c
new file mode 100644 (file)
index 0000000..1527d10
--- /dev/null
@@ -0,0 +1,630 @@
+#pragma once
+#include "vg/vg_rigidbody.h"
+#include "vg/vg_rigidbody_collision.h"
+#include "vg/vg_rigidbody_constraints.h"
+#include "scene_rigidbody.h"
+
+#include "player.h"
+#include "player_dead.h"
+#include "audio.h"
+
+static float k_ragdoll_floatyiness = 20.0f,
+             k_ragdoll_floatydrag  = 1.0f,
+             k_ragdoll_limit_scale = 1.0f,
+             k_ragdoll_spring = 127.0f,
+             k_ragdoll_dampening = 15.0f,
+             k_ragdoll_correction = 0.5f,
+             k_ragdoll_angular_drag = 0.08f,
+             k_ragdoll_active_threshold = 5.0f;
+
+static int   k_ragdoll_div = 1,
+             ragdoll_frame = 0,
+             k_ragdoll_debug_collider = 1,
+             k_ragdoll_debug_constraints = 0;
+
+static int dev_ragdoll_saveload(int argc, const char *argv[]){
+   if( argc != 2 ){
+      vg_info( "Usage: ragdoll load/save filepath\n" );
+      return 1;
+   }
+
+   if( !strcmp(argv[0],"save") ){
+      FILE *fp = fopen( argv[1], "wb" );
+      if( !fp ){
+         vg_error( "Failed to open file\n" );
+         return 1;
+      }
+      fwrite( &localplayer.ragdoll.parts, 
+               sizeof(localplayer.ragdoll.parts), 1, fp );
+      fclose( fp );
+   }
+   else if( !strcmp(argv[0],"load") ){
+      FILE *fp = fopen( argv[1], "rb" );
+      if( !fp ){
+         vg_error( "Failed to open file\n" );
+         return 1;
+      }
+
+      fread( &localplayer.ragdoll.parts, 
+              sizeof(localplayer.ragdoll.parts), 1, fp );
+      fclose( fp );
+   }
+   else {
+      vg_error( "Unknown command: %s (options are: save,load)\n", argv[0] );
+      return 1;
+   }
+   
+   return 0;
+}
+
+void player_ragdoll_init(void)
+{
+   VG_VAR_F32( k_ragdoll_active_threshold );
+   VG_VAR_F32( k_ragdoll_angular_drag );
+   VG_VAR_F32( k_ragdoll_correction );
+   VG_VAR_F32( k_ragdoll_limit_scale );
+   VG_VAR_F32( k_ragdoll_spring );
+   VG_VAR_F32( k_ragdoll_dampening );
+   VG_VAR_I32( k_ragdoll_div );
+   VG_VAR_I32( k_ragdoll_debug_collider );
+   VG_VAR_I32( k_ragdoll_debug_constraints );
+   vg_console_reg_cmd( "ragdoll", dev_ragdoll_saveload, NULL );
+}
+
+void player_init_ragdoll_bone_collider( struct skeleton_bone *bone,
+                                        struct ragdoll_part *rp )
+{
+   f32 k_density = 8.0f,
+       k_inertia_scale = 2.0f;
+
+   m4x3_identity( rp->collider_mtx );
+
+   rp->type = bone->collider;
+   if( bone->collider == k_bone_collider_box ){
+      v3f delta;
+      v3_sub( bone->hitbox[1], bone->hitbox[0], delta );
+      v3_muls( delta, 0.5f, delta );
+      v3_add( bone->hitbox[0], delta, rp->collider_mtx[3] );
+
+      v3_muls( delta, -1.0f, rp->inf.box[0] );
+      v3_copy( delta,        rp->inf.box[1] );
+
+      rp->colour = 0xffcccccc;
+
+      rb_setbody_box( &rp->rb, rp->inf.box, k_density, k_inertia_scale );
+   }
+   else if( bone->collider == k_bone_collider_capsule ){
+      v3f v0, v1, tx, ty;
+      v3_sub( bone->hitbox[1], bone->hitbox[0], v0 );
+
+      int major_axis = 0;
+      float largest = -1.0f;
+
+      for( int i=0; i<3; i ++ ){
+         if( fabsf( v0[i] ) > largest ){
+            largest = fabsf( v0[i] );
+            major_axis = i;
+         }
+      }
+      
+      v3_zero( v1 );
+      v1[ major_axis ] = 1.0f;
+      v3_tangent_basis( v1, tx, ty );
+      
+      rp->inf.capsule.r = (fabsf(v3_dot(tx,v0)) + fabsf(v3_dot(ty,v0))) * 0.25f;
+      rp->inf.capsule.h = fabsf(v0[ major_axis ]);
+
+      /* orientation */
+      v3_muls( tx, -1.0f, rp->collider_mtx[0] );
+      v3_muls( v1, -1.0f, rp->collider_mtx[1] );
+      v3_muls( ty, -1.0f, rp->collider_mtx[2] );
+      v3_add( bone->hitbox[0], bone->hitbox[1], rp->collider_mtx[3] );
+      v3_muls( rp->collider_mtx[3], 0.5f, rp->collider_mtx[3] );
+
+      rp->colour = 0xff000000 | (0xff << (major_axis*8));
+      
+      rb_setbody_capsule( &rp->rb, rp->inf.capsule.r, rp->inf.capsule.h, 
+                           k_density, k_inertia_scale );
+   }
+   else{
+      vg_warn( "type: %u\n", bone->collider );
+      vg_fatal_error( "Invalid bone collider type" );
+   }
+
+   m4x3_invert_affine( rp->collider_mtx, rp->inv_collider_mtx );
+
+   /* Position collider into rest */
+   m3x3_q( rp->collider_mtx, rp->rb.q );
+   v3_add( rp->collider_mtx[3], bone->co, rp->rb.co );
+   v3_zero( rp->rb.v );
+   v3_zero( rp->rb.w );
+   rb_update_matrices( &rp->rb );
+}
+
+/*
+ * Get parent index in the ragdoll
+ */
+u32 ragdoll_bone_parent( struct player_ragdoll *rd, u32 bone_id )
+{
+   for( u32 j=0; j<rd->part_count; j++ )
+      if( rd->parts[ j ].bone_id == bone_id )
+         return j;
+
+   vg_fatal_error( "Referenced parent bone does not have a rigidbody" );
+   return 0;
+}
+
+/*
+ * Setup ragdoll colliders from skeleton
+ */
+void setup_ragdoll_from_skeleton( struct skeleton *sk,
+                                  struct player_ragdoll *rd )
+{
+   rd->part_count = 0;
+
+   if( !sk->collider_count )
+      return;
+
+   rd->position_constraints_count = 0;
+   rd->cone_constraints_count = 0;
+
+   for( u32 i=1; i<sk->bone_count; i ++ ){
+      struct skeleton_bone *bone = &sk->bones[i];
+
+      /*
+       * Bones with colliders
+       */
+      if( !(bone->collider) )
+         continue;
+
+      if( rd->part_count > VG_ARRAY_LEN(rd->parts) )
+         vg_fatal_error( "Playermodel has too many colliders" );
+
+      u32 part_id = rd->part_count;
+      rd->part_count ++;
+
+      struct ragdoll_part *rp = &rd->parts[ part_id ];
+      rp->bone_id = i;
+      rp->parent = 0xffffffff;
+
+      player_init_ragdoll_bone_collider( bone, rp );
+      
+      /*
+       * Bones with collider and parent
+       */
+      if( !bone->parent )
+         continue;
+
+      rp->parent = ragdoll_bone_parent( rd, bone->parent );
+      
+      if( bone->orig_bone->flags & k_bone_flag_cone_constraint ){
+         u32 conid = rd->position_constraints_count;
+         rd->position_constraints_count ++;
+
+         struct rb_constr_pos *c = &rd->position_constraints[ conid ];
+
+         struct skeleton_bone *bj = &sk->bones[rp->bone_id];
+         struct ragdoll_part  *pp = &rd->parts[rp->parent];
+         struct skeleton_bone *bp = &sk->bones[pp->bone_id];
+         
+         rd->constraint_associations[conid][0] = rp->parent;
+         rd->constraint_associations[conid][1] = part_id;
+
+         /* Convention: rba -- parent, rbb -- child */
+         c->rba = &pp->rb;
+         c->rbb = &rp->rb;
+
+         v3f delta;
+         v3_sub( bj->co, bp->co, delta );
+         m4x3_mulv( rp->inv_collider_mtx, (v3f){0.0f,0.0f,0.0f}, c->lcb );
+         m4x3_mulv( pp->inv_collider_mtx, delta, c->lca );
+
+
+         mdl_bone *inf = bone->orig_bone;
+
+         struct rb_constr_swingtwist *a = 
+            &rd->cone_constraints[ rd->cone_constraints_count ++ ];
+
+         a->rba = &pp->rb;
+         a->rbb = &rp->rb;
+         a->conet = cosf( inf->conet )-0.0001f;
+         
+         /* Store constraint in local space vectors */
+         m3x3_mulv( c->rba->to_local, inf->conevx, a->conevx );
+         m3x3_mulv( c->rba->to_local, inf->conevy, a->conevy );
+         m3x3_mulv( c->rbb->to_local, inf->coneva, a->coneva );
+         v3_copy( c->lca, a->view_offset );
+         
+         v3_cross( inf->coneva, inf->conevy, a->conevxb );
+         m3x3_mulv( c->rbb->to_local, a->conevxb, a->conevxb );
+
+         v3_normalize( a->conevxb );
+         v3_normalize( a->conevx );
+         v3_normalize( a->conevy );
+         v3_normalize( a->coneva );
+
+         a->conevx[3] = v3_length( inf->conevx );
+         a->conevy[3] = v3_length( inf->conevy );
+
+         rp->use_limits = 1;
+      }
+   }
+}
+
+/*
+ * Make avatar copy the ragdoll
+ */
+void copy_ragdoll_pose_to_localplayer( struct player_ragdoll *rd )
+{
+   for( int i=0; i<rd->part_count; i++ ){
+      struct ragdoll_part *part = &rd->parts[i];
+
+      m4x3f mtx;
+
+      v4f q_int;
+      v3f co_int;
+
+      float substep = vg.time_fixed_extrapolate;
+      v3_lerp( part->prev_co, part->rb.co, substep, co_int );
+      q_nlerp( part->prev_q, part->rb.q, substep, q_int );
+
+      q_m3x3( q_int, mtx );
+      v3_copy( co_int, mtx[3] );
+
+      m4x3_mul( mtx, part->inv_collider_mtx, 
+                localplayer.final_mtx[part->bone_id] );
+   }
+
+   for( u32 i=1; i<localplayer.skeleton.bone_count; i++ ){
+      struct skeleton_bone *sb = &localplayer.skeleton.bones[i];
+
+      if( sb->parent && !sb->collider ){
+         v3f delta;
+         v3_sub( localplayer.skeleton.bones[i].co, 
+                 localplayer.skeleton.bones[sb->parent].co, delta );
+
+         m4x3f posemtx;
+         m3x3_identity( posemtx );
+         v3_copy( delta, posemtx[3] );
+
+         /* final matrix */
+         m4x3_mul( localplayer.final_mtx[sb->parent], posemtx, 
+                   localplayer.final_mtx[i] );
+      }
+   }
+
+   skeleton_apply_inverses( &localplayer.skeleton, localplayer.final_mtx );
+}
+
+/*
+ * Make the ragdoll copy the player model
+ */
+void copy_localplayer_to_ragdoll( struct player_ragdoll *rd, 
+                                  enum player_die_type type )
+{
+   v3f centroid;
+
+   v3f *bone_mtx = localplayer.final_mtx[localplayer.id_hip];
+   m4x3_mulv( bone_mtx, 
+              localplayer.skeleton.bones[localplayer.id_hip].co, centroid );
+
+   for( int i=0; i<rd->part_count; i++ ){
+      struct ragdoll_part *part = &rd->parts[i];
+
+      v3f pos, offset;
+      u32 bone = part->bone_id;
+
+      v3f *bone_mtx = localplayer.final_mtx[bone];
+
+      m4x3_mulv( bone_mtx, localplayer.skeleton.bones[bone].co, pos );
+      m3x3_mulv( bone_mtx, part->collider_mtx[3], offset );
+      v3_add( pos, offset, part->rb.co );
+
+      m3x3f r;
+      m3x3_mul( bone_mtx, part->collider_mtx, r );
+      m3x3_q( r, part->rb.q );
+
+      v3f ra, v;
+      v3_sub( part->rb.co, centroid, ra );
+      v3_cross( localplayer.rb.w, ra, v );
+      v3_add( localplayer.rb.v, v, part->rb.v );
+
+      if( type == k_player_die_type_feet ){
+         if( (bone == localplayer.id_foot_l) || 
+             (bone == localplayer.id_foot_r) ){
+            v3_zero( part->rb.v );
+         }
+      }
+
+      v3_copy( localplayer.rb.w, part->rb.w );
+
+      v3_copy( part->rb.co, part->prev_co );
+      v4_copy( part->rb.q, part->prev_q );
+      
+      rb_update_matrices( &part->rb );
+   }
+}
+
+/*
+ * Ragdoll physics step
+ */
+void player_ragdoll_iter( struct player_ragdoll *rd )
+{
+   world_instance *world = world_current_instance();
+
+   int run_sim = 0;
+   ragdoll_frame ++;
+
+   if( ragdoll_frame >= k_ragdoll_div ){
+      ragdoll_frame = 0;
+      run_sim = 1;
+   }
+
+   rb_solver_reset();
+   
+   float contact_velocities[256];
+
+   rigidbody _null = {0};
+   _null.inv_mass = 0.0f;
+   m3x3_zero( _null.iI );
+
+   for( int i=0; i<rd->part_count; i ++ ){
+      v4_copy( rd->parts[i].rb.q, rd->parts[i].prev_q );
+      v3_copy( rd->parts[i].rb.co, rd->parts[i].prev_co );
+
+      if( rb_global_has_space() ){
+         rb_ct *buf = rb_global_buffer();
+
+         int l;
+         
+         if( rd->parts[i].type == k_bone_collider_capsule ){
+            l = rb_capsule__scene( rd->parts[i].rb.to_world,
+                                   &rd->parts[i].inf.capsule,
+                                   NULL, world->geo_bh, buf,
+                                   k_material_flag_ghosts );
+         }
+         else if( rd->parts[i].type == k_bone_collider_box ){
+            l = rb_box__scene( rd->parts[i].rb.to_world,
+                               rd->parts[i].inf.box,
+                               NULL, world->geo_bh, buf,
+                               k_material_flag_ghosts );
+         }
+         else continue;
+
+         for( int j=0; j<l; j++ ){
+            buf[j].rba = &rd->parts[i].rb;
+            buf[j].rbb = &_null;
+         }
+
+         rb_contact_count += l;
+      }
+   }
+
+   /* 
+    * self-collision
+    */
+   for( int i=0; i<rd->part_count-1; i ++ ){
+      for( int j=i+1; j<rd->part_count; j ++ ){
+         if( rd->parts[j].parent != i ){
+            if( !rb_global_has_space() )
+               break;
+
+            if( rd->parts[j].type != k_bone_collider_capsule )
+               continue;
+
+            if( rd->parts[i].type != k_bone_collider_capsule )
+               continue;
+
+            rb_ct *buf = rb_global_buffer();
+
+            int l = rb_capsule__capsule( rd->parts[i].rb.to_world,
+                                         &rd->parts[i].inf.capsule,
+                                         rd->parts[j].rb.to_world,
+                                         &rd->parts[j].inf.capsule,
+                                         buf );
+
+            for( int k=0; k<l; k++ ){
+               buf[k].rba = &rd->parts[i].rb;
+               buf[k].rbb = &rd->parts[j].rb;
+            }
+
+            rb_contact_count += l;
+         }
+      }
+   }
+
+   if( localplayer.drowned )
+   {
+      for( int j=0; j<rd->part_count; j++ )
+      {
+         struct ragdoll_part *pj = &rd->parts[j];
+
+         if( run_sim )
+         {
+            rb_effect_simple_bouyency( &pj->rb, world->water.plane,
+                                        k_ragdoll_floatyiness,
+                                        k_ragdoll_floatydrag );
+         }
+      }
+   }
+
+   /*
+    * PRESOLVE
+    */
+   for( u32 i=0; i<rb_contact_count; i++ ){
+      rb_ct *ct = &rb_contact_buffer[i];
+
+      v3f rv, ra, rb;
+      v3_sub( ct->co, ct->rba->co, ra );
+      v3_sub( ct->co, ct->rbb->co, rb );
+      rb_rcv( ct->rba, ct->rbb, ra, rb, rv );
+      float     vn = v3_dot( rv, ct->n );
+
+      contact_velocities[i] = vn;
+   }
+
+   rb_presolve_contacts( rb_contact_buffer, vg.time_fixed_delta, 
+                         rb_contact_count );
+   rb_presolve_swingtwist_constraints( rd->cone_constraints,
+                                       rd->cone_constraints_count );
+
+   /* 
+    * DEBUG
+    */
+   if( k_ragdoll_debug_collider ){
+      for( u32 i=0; i<rd->part_count; i ++ ){
+         struct ragdoll_part *rp = &rd->parts[i];
+
+         if( rp->type == k_bone_collider_capsule ){
+            vg_line_capsule( rp->rb.to_world, 
+                             rp->inf.capsule.r, rp->inf.capsule.h, rp->colour );
+         }
+         else if( rp->type == k_bone_collider_box ){
+            vg_line_boxf_transformed( rp->rb.to_world, 
+                                      rp->inf.box, rp->colour );
+         }
+      }
+   }
+
+   if( k_ragdoll_debug_constraints ){
+      rb_debug_position_constraints( rd->position_constraints, 
+                                     rd->position_constraints_count );
+
+      rb_debug_swingtwist_constraints( rd->cone_constraints,
+                                       rd->cone_constraints_count );
+   }
+
+   /*
+    * SOLVE CONSTRAINTS  & Integrate
+    */
+   if( run_sim ){
+      /* the solver is not very quickly converging so... */
+      for( int i=0; i<40; i++ ){
+         if( i<20 ){
+            rb_solve_contacts( rb_contact_buffer, rb_contact_count );
+            rb_solve_swingtwist_constraints( rd->cone_constraints, 
+                                             rd->cone_constraints_count );
+            rb_postsolve_swingtwist_constraints( rd->cone_constraints, 
+                                                 rd->cone_constraints_count );
+         }
+         rb_solve_position_constraints( rd->position_constraints, 
+                                        rd->position_constraints_count );
+      }
+
+      rb_correct_position_constraints( rd->position_constraints,
+                                       rd->position_constraints_count, 
+                                       k_ragdoll_correction * 0.5f );
+      rb_correct_swingtwist_constraints( rd->cone_constraints, 
+                                         rd->cone_constraints_count,
+                                         k_ragdoll_correction * 0.25f );
+
+      for( int i=0; i<rd->part_count; i++ ){
+         rb_iter( &rd->parts[i].rb );
+
+         v3f w;
+         v3_copy( rd->parts[i].rb.w, w );
+         if( v3_length2( w ) > 0.00001f ){
+            v3_normalize( w );
+            v3_muladds( rd->parts[i].rb.w, w, -k_ragdoll_angular_drag,
+                        rd->parts[i].rb.w );
+         }
+      }
+
+      for( int i=0; i<rd->part_count; i++ )
+         rb_update_matrices( &rd->parts[i].rb );
+   }
+
+   rb_ct *stress = NULL;
+   float max_stress = 1.0f;
+
+   for( u32 i=0; i<rb_contact_count; i++ ){
+      rb_ct *ct = &rb_contact_buffer[i];
+
+      v3f rv, ra, rb;
+      v3_sub( ct->co, ct->rba->co, ra );
+      v3_sub( ct->co, ct->rbb->co, rb );
+      rb_rcv( ct->rba, ct->rbb, ra, rb, rv );
+      float vn = v3_dot( rv, ct->n );
+
+      float s = fabsf(vn - contact_velocities[i]);
+      if( s > max_stress ){
+         stress = ct;
+         max_stress = s;
+      }
+   }
+
+   static u32 temp_filter = 0;
+
+   /* 
+    * motorized joints 
+    */
+   if( run_sim && 
+         (v3_length2(player_dead.v_lpf)>(k_ragdoll_active_threshold*
+                                         k_ragdoll_active_threshold)) ){
+      mdl_keyframe anim[32];
+      skeleton_sample_anim( &localplayer.skeleton, player_dead.anim_bail,
+                            0.0f, anim );
+
+      for( u32 i=0; i<rd->cone_constraints_count; i ++ ){
+         rb_constr_swingtwist *st = &rd->cone_constraints[i];
+         rb_constr_pos *pc = &rd->position_constraints[i];
+
+         v3f va, vap;
+
+         m3x3_mulv( st->rbb->to_world, st->coneva, va );
+
+         /* calculate va as seen in rest position, from the perspective of the
+          * parent object, mapped to pose world space using the parents 
+          * transform. thats our target */
+
+         u32 id_p = rd->constraint_associations[i][0],
+             id_a = rd->constraint_associations[i][1];
+
+         struct ragdoll_part *pa = &rd->parts[ id_a ],
+                             *pp = &rd->parts[ id_p ];
+
+         mdl_keyframe *kf = &anim[ pa->bone_id-1 ];
+         m3x3_mulv( pa->collider_mtx, st->coneva, vap );
+         q_mulv( kf->q, vap, vap );
+
+         /* This could be a transfer function */
+         m3x3_mulv( pp->inv_collider_mtx, vap, vap );
+         m3x3_mulv( st->rba->to_world, vap, vap );
+
+         f32 d = v3_dot( vap, va ),
+             a = acosf( vg_clampf( d, -1.0f, 1.0f ) );
+
+         v3f axis;
+         v3_cross( vap, va, axis );
+
+         f32 Fs = -a * k_ragdoll_spring,
+             Fd = -v3_dot( st->rbb->w, axis ) * k_ragdoll_dampening,
+             F  = Fs+Fd;
+
+         v3f torque;
+         v3_muls( axis, F, torque );
+         v3_muladds( st->rbb->w, torque, vg.time_fixed_delta, st->rbb->w );
+
+         /* apply a adjustment to keep velocity at joint 0 */
+#if 0
+         v3f wcb, vcb;
+         m3x3_mulv( st->rbb->to_world, pc->lcb, wcb );
+         v3_cross( torque, wcb, vcb );
+         v3_muladds( st->rbb->v, vcb, vg.time_fixed_delta, st->rbb->v );
+#endif
+      }
+   }
+
+   if( temp_filter ){
+      temp_filter --;
+      return;
+   }
+
+   if( stress ){
+      temp_filter = 20;
+      audio_lock();
+      audio_oneshot_3d( &audio_hits[vg_randu32(&vg.rand)%5], 
+                        stress->co, 20.0f, 1.0f );
+      audio_unlock();
+   }
+}
diff --git a/src/player_ragdoll.h b/src/player_ragdoll.h
new file mode 100644 (file)
index 0000000..08ab5e7
--- /dev/null
@@ -0,0 +1,71 @@
+#pragma once
+
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
+ *
+ * Ragdoll system
+ */
+
+#include "player_api.h"
+#include "skeleton.h"
+#include "vg/vg_rigidbody.h"
+#include "vg/vg_rigidbody_constraints.h"
+
+struct player_ragdoll{
+   struct ragdoll_part{
+      u32 bone_id;
+      
+      /* Collider transform relative to bone */
+      m4x3f collider_mtx,
+            inv_collider_mtx;
+
+      v4f prev_q;
+      v3f prev_co;
+
+      u32 use_limits;
+      v3f limits[2];
+
+      u32 parent;
+      u32 colour;
+
+      rigidbody rb;
+      enum bone_collider type;
+
+      union {
+         rb_capsule capsule;
+         boxf box;
+      }
+      inf;
+   }
+   parts[32];
+   u32 part_count;
+
+   rb_constr_pos  position_constraints[32];
+   u32            position_constraints_count;
+
+   rb_constr_swingtwist cone_constraints[32];
+   u32                  cone_constraints_count;
+
+   /* TODO: Fix duplicated data */
+   u32 constraint_associations[32][2];
+   int shoes[2];
+};
+
+enum player_die_type {
+   k_player_die_type_generic,
+   k_player_die_type_head,
+   k_player_die_type_feet
+};
+
+void player_ragdoll_init(void);
+void player_init_ragdoll_bone_collider( struct skeleton_bone *bone,
+                                           struct ragdoll_part *rp );
+u32 ragdoll_bone_parent( struct player_ragdoll *rd, u32 bone_id );
+void setup_ragdoll_from_skeleton( struct skeleton *sk,
+                                  struct player_ragdoll *rd );
+void copy_ragdoll_pose_to_localplayer( struct player_ragdoll *rd );
+void copy_localplayer_to_ragdoll( struct player_ragdoll *rd, 
+                                  enum player_die_type type );
+                                   
+void player_debug_ragdoll(void);
+void player_ragdoll_iter( struct player_ragdoll *rd );
diff --git a/src/player_remote.c b/src/player_remote.c
new file mode 100644 (file)
index 0000000..6fdf04f
--- /dev/null
@@ -0,0 +1,1159 @@
+#include "player_remote.h"
+#include "skeleton.h"
+#include "player_render.h"
+#include "player_api.h"
+#include "network_common.h"
+#include "addon.h"
+#include "font.h"
+#include "gui.h"
+#include "ent_miniworld.h"
+#include "ent_region.h"
+#include "shaders/model_entity.h"
+#include "vg/vg_steam_friends.h"
+
+struct global_netplayers netplayers;
+
+static i32 k_show_own_name = 0;
+
+static void player_remote_clear( struct network_player *player )
+{
+   addon_cache_unwatch( k_addon_type_player, player->playermodel_view_slot );
+   addon_cache_unwatch( k_addon_type_board, player->board_view_slot );
+
+   memset( player, 0, sizeof(*player) );
+   strcpy( player->username, "unknown" );
+   player->subsystem = k_player_subsystem_invalid;
+}
+
+/* 
+ * re-attatches addon_reg pointers on the remote client if we have the same
+ * world loaded.
+ */
+static void relink_remote_player_worlds( u32 client_id ){
+   struct network_player *player = &netplayers.list[client_id];
+
+   addon_alias q[2];
+   addon_uid_to_alias( player->items[k_netmsg_playeritem_world0], &q[0] );
+   addon_uid_to_alias( player->items[k_netmsg_playeritem_world1], &q[1] );
+
+   /*
+    * currently in 10.23, the hub world will always be the same.
+    * this might but probably wont change in the future
+    */
+   for( u32 i=0; i<k_world_max; i++ ){
+      addon_reg *reg = world_static.instance_addons[ i ];
+
+      player->world_match[i] = 0;
+
+      if( reg ){
+         if( addon_alias_eq( &q[i], &world_static.instance_addons[i]->alias ) )
+            player->world_match[i] = 1;
+      }
+   }
+}
+
+/* 
+ * re-attatches addon_reg pointers on the remote client if we have the mod 
+ * installed locally.
+ *
+ * Run if local worlds change
+ */
+void relink_all_remote_player_worlds(void)
+{
+   for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
+      struct network_player *player = &netplayers.list[i];
+      if( player->active )
+         relink_remote_player_worlds(i);
+   }
+}
+
+void player_remote_update_friendflags( struct network_player *remote )
+{
+   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
+   remote->isfriend = SteamAPI_ISteamFriends_HasFriend( hSteamFriends,
+                        remote->steamid, k_EFriendFlagImmediate );
+   remote->isblocked = SteamAPI_ISteamFriends_HasFriend( hSteamFriends,
+                        remote->steamid, k_EFriendFlagBlocked );
+}
+
+void player_remote_rx_200_300( SteamNetworkingMessage_t *msg )
+{
+   netmsg_blank *tmp = msg->m_pData;
+
+   if( tmp->inetmsg_id == k_inetmsg_playerjoin ){
+      netmsg_playerjoin *playerjoin = msg->m_pData;
+      if( !packet_minsize( msg, sizeof(*playerjoin) )) return;
+
+      if( playerjoin->index < VG_ARRAY_LEN(netplayers.list) ){
+         struct network_player *player = &netplayers.list[ playerjoin->index ];
+         player_remote_clear( player );
+         player->active = 1;
+         player->steamid = playerjoin->steamid;
+         player_remote_update_friendflags( player );
+
+         /* TODO: interpret the uids */
+         player->board_view_slot = 0;
+         player->playermodel_view_slot = 0;
+
+         struct interp_buffer *buf = &netplayers.interp_data[playerjoin->index];
+         buf->t = -99999999.9;
+         for( u32 i=0; i<VG_ARRAY_LEN(buf->frames); i ++ ){
+            buf->frames[i].active = 0;
+         }
+
+         vg_info( "#%u joined friend: %d, blocked: %d\n", 
+                  playerjoin->index, player->isfriend, player->isblocked );
+      }
+      else {
+         vg_error( "inetmsg_playerjoin: player index out of range\n" );
+      }
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playerleave ){
+      netmsg_playerleave *playerleave = msg->m_pData;
+      if( !packet_minsize( msg, sizeof(*playerleave) )) return;
+      
+      if( playerleave->index < VG_ARRAY_LEN(netplayers.list) ){
+         struct network_player *player = &netplayers.list[ playerleave->index ];
+         player_remote_clear( player );
+         player->active = 0;
+         vg_info( "player leave (%d)\n", playerleave->index );
+      }
+      else {
+         vg_error( "inetmsg_playerleave: player index out of range\n" );
+      }
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playerusername ){
+      netmsg_playerusername *update = msg->m_pData;
+      if( !packet_minsize( msg, sizeof(*update) )) return;
+
+      if( update->index < VG_ARRAY_LEN(netplayers.list) ){
+         struct network_player *player = &netplayers.list[ update->index ];
+
+         network_msgstring( update->name, msg->m_cbSize, sizeof(*update),
+                            player->username, sizeof(player->username) );
+
+         vg_info( "#%u changed username to: %s\n", 
+                  update->index, player->username );
+      }
+      else {
+         vg_error( "inetmsg_playerleave: player index out of range\n" );
+      }
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playerframe ){
+      u32 datasize = msg->m_cbSize - sizeof(netmsg_playerframe);
+      
+      if( datasize > sizeof(union interp_animdata) ){
+         vg_error( "Player frame data exceeds animdata size\n" );
+         return;
+      }
+      
+      netmsg_playerframe *frame = msg->m_pData;
+
+      if( frame->client >= VG_ARRAY_LEN(netplayers.list) ){
+         vg_error( "inetmsg_playerframe: player index out of range\n" );
+         return;
+      }
+
+      if( frame->subsystem >= k_player_subsystem_max ){
+         vg_error( "inetmsg_playerframe: subsystem out of range\n" );
+         return;
+      }
+
+      struct interp_buffer *ib = &netplayers.interp_data[ frame->client ];
+      struct interp_frame *dest = NULL;
+
+      f64 min_time = INFINITY;
+      for( u32 i=0; i<VG_ARRAY_LEN(ib->frames); i++ ){
+         struct interp_frame *ifr = &ib->frames[i];
+
+         if( !ifr->active ){
+            dest = ifr;
+            break;
+         }
+
+         if( ifr->timestamp < min_time ){
+            min_time = ifr->timestamp;
+            dest = ifr;
+         }
+      }
+
+      dest->active = 1;
+      dest->subsystem = frame->subsystem;
+      dest->flags = frame->flags;
+
+      bitpack_ctx ctx = {
+         .mode = k_bitpack_decompress,
+         .buffer = frame->animdata,
+         .buffer_len = datasize,
+         .bytes = 0,
+      };
+      
+      /* animation 
+       * -------------------------------------------------------------*/
+
+      dest->timestamp = frame->timestamp;
+      dest->boundary_hash = frame->boundary_hash;
+
+      struct network_player *player = &netplayers.list[ frame->client ];
+      struct player_subsystem_interface *sys = 
+         player_subsystems[ frame->subsystem ];
+
+      memset( &dest->data, 0, sys->animator_size );
+      if( sys->network_animator_exchange )
+         sys->network_animator_exchange( &ctx, &dest->data );
+      else 
+         bitpack_bytes( &ctx, sys->animator_size, sys->animator_data );
+
+      /* sfx
+       * -------------------------------------------------------------*/
+      
+      for( u32 i=0; i<frame->sound_effects; i ++ ){
+         struct net_sfx sfx;
+         net_sfx_exchange( &ctx, &sfx );
+
+         f64 t = (frame->timestamp - NETWORK_FRAMERATE) + 
+                 (sfx.subframe*NETWORK_FRAMERATE);
+
+         f32 remaining = t - ib->t;
+
+         if( remaining <= 0.0f )
+            net_sfx_play( &sfx );
+         else{
+            struct net_sfx *dst = NULL;
+
+            for( u32 j=0; j<NETWORK_SFX_QUEUE_LENGTH; j ++ ){
+               struct net_sfx *sj = &netplayers.sfx_queue[j];
+               if( sj->system == k_player_subsystem_invalid ){
+                  dst = sj;
+                  break;
+               }
+
+               if( sj->priority < sfx.priority )
+                  dst = sj;
+            }
+
+            *dst = sfx;
+            dst->subframe = remaining;
+         }
+      }
+
+      /* glider
+       * -------------------------------------------------------------*/
+
+      memset( &dest->data_glider, 0, sizeof(struct remote_glider_animator) );
+      if( dest->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
+                         NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
+         player_glide_remote_animator_exchange( &ctx, &dest->data_glider );
+      }
+
+      player->subsystem = frame->subsystem;
+      player->down_bytes += msg->m_cbSize;
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_playeritem ){
+      netmsg_playeritem *item = msg->m_pData;
+      if( !packet_minsize( msg, sizeof(*item)+1 )) return;
+
+      if( item->client >= VG_ARRAY_LEN(netplayers.list) ){
+         vg_error( "inetmsg_playerframe: player index out of range\n" );
+         return;
+      }
+
+      if( item->type_index >= k_netmsg_playeritem_max ){
+         vg_warn( "Client #%d invalid equip type %u\n", 
+                  (i32)item->client, (u32)item->type_index );
+         return;
+      }
+
+      vg_info( "Client #%d equiped: [%s] %s\n", 
+               item->client,
+               (const char *[]){[k_netmsg_playeritem_board]="board",
+                                [k_netmsg_playeritem_player]="player",
+                                [k_netmsg_playeritem_world0]="world0",
+                                [k_netmsg_playeritem_world1]="world1"
+               }[item->type_index], item->uid );
+
+      struct network_player *player = &netplayers.list[ item->client ];
+      char *uid = player->items[ item->type_index ];
+
+      network_msgstring( item->uid, msg->m_cbSize, sizeof(*item),
+                         uid, ADDON_UID_MAX );
+
+      if( item->type_index == k_netmsg_playeritem_board ){
+         addon_cache_unwatch( k_addon_type_board, player->board_view_slot );
+         player->board_view_slot = 
+            addon_cache_create_viewer_from_uid( k_addon_type_board, uid );
+      }
+      else if( item->type_index == k_netmsg_playeritem_player ){
+         addon_cache_unwatch( k_addon_type_player, 
+                              player->playermodel_view_slot );
+         player->playermodel_view_slot =
+            addon_cache_create_viewer_from_uid( k_addon_type_player, uid );
+      }
+      else if( (item->type_index == k_netmsg_playeritem_world0) ||
+               (item->type_index == k_netmsg_playeritem_world1) ){
+         relink_remote_player_worlds( item->client );
+      }
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_chat ){
+      netmsg_chat *chat = msg->m_pData;
+      
+      struct network_player *player = &netplayers.list[ chat->client ];
+      network_msgstring( chat->msg, msg->m_cbSize, sizeof(netmsg_chat),
+                         player->chat, NETWORK_MAX_CHAT );
+      player->chat_time = vg.time_real;
+      vg_info( "[%d]: %s\n", chat->client, player->chat );
+   }
+   else if( tmp->inetmsg_id == k_inetmsg_region ){
+      netmsg_region *region = msg->m_pData;
+      struct network_player *player = &netplayers.list[ region->client ];
+
+      u32 l = network_msgstring( 
+               region->loc, msg->m_cbSize, sizeof(netmsg_region),
+               player->region, NETWORK_REGION_MAX );
+      player->region_flags = region->flags;
+
+      if( l )
+         player->region_flags |= k_ent_region_flag_hasname;
+
+      player->effect_data.spark.colour = region_spark_colour(region->flags);
+   }
+}
+
+/*
+ * Write localplayer pose to network
+ */
+void remote_player_send_playerframe(void)
+{
+   u8 sysid = localplayer.subsystem;
+   if( sysid >= k_player_subsystem_max ) return;
+
+   struct player_subsystem_interface *sys = player_subsystems[sysid];
+
+   if( sys->animator_size ){
+      u32 max_buf_size = sys->animator_size + sizeof(localplayer.sfx_buffer),
+          base_size = sizeof(struct netmsg_playerframe),
+          max_packet = base_size + max_buf_size;
+
+      netmsg_playerframe *frame = alloca( max_packet );
+      frame->inetmsg_id = k_inetmsg_playerframe;
+      frame->client = 0xff;
+      frame->subsystem = localplayer.subsystem;
+      frame->flags = world_static.active_instance;
+
+      bitpack_ctx ctx = {
+         .mode = k_bitpack_compress,
+         .buffer = frame->animdata,
+         .buffer_len = max_buf_size,
+         .bytes = 0
+      };
+
+      /* animation 
+       * -----------------------------------------------*/
+
+      frame->timestamp = vg.time_real;
+      frame->boundary_hash = localplayer.boundary_hash;
+      if( sys->network_animator_exchange )
+         sys->network_animator_exchange( &ctx, sys->animator_data );
+      else
+         bitpack_bytes( &ctx, sys->animator_size, sys->animator_data );
+
+      /* sfx
+       * ---------------------------------------------*/
+
+      frame->sound_effects = localplayer.sfx_buffer_count;
+      for( u32 i=0; i<localplayer.sfx_buffer_count; i ++ )
+         net_sfx_exchange( &ctx, &localplayer.sfx_buffer[i] );
+
+      /* glider
+       * -------------------------------------------------------------*/
+
+      if( localplayer.have_glider || 
+            (localplayer.subsystem == k_player_subsystem_glide) ) {
+         frame->flags |= NETMSG_PLAYERFRAME_HAVE_GLIDER;
+      }
+
+      if( localplayer.glider_orphan )
+         frame->flags |= NETMSG_PLAYERFRAME_GLIDER_ORPHAN;
+
+      if( frame->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
+                          NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
+         player_glide_remote_animator_exchange( &ctx,    
+                                                &player_glide.remote_animator );
+      }
+
+      /* ------- */
+
+      u32 wire_size = base_size + ctx.bytes;
+      netplayers.up_bytes += wire_size;
+
+      SteamAPI_ISteamNetworkingSockets_SendMessageToConnection(
+            hSteamNetworkingSockets, network_client.remote, 
+            frame, wire_size,
+            k_nSteamNetworkingSend_Unreliable, NULL );
+   }
+}
+
+/*
+ * Updates network traffic stats
+ */
+void remote_player_debug_update(void)
+{
+   if( (vg.time_real - netplayers.last_data_measurement) > 1.0 ){
+      netplayers.last_data_measurement = vg.time_real;
+      u32 total_down = 0;
+
+      for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ ){
+         struct network_player *player = &netplayers.list[i];
+         if( player->active ){
+            total_down += player->down_bytes;
+            player->down_kbs = ((f32)player->down_bytes)/1024.0f;
+            player->down_bytes = 0;
+         }
+      }
+
+      netplayers.down_kbs = ((f32)total_down)/1024.0f;
+      netplayers.up_kbs = ((f32)netplayers.up_bytes)/1024.0f;
+      netplayers.up_bytes = 0;
+   }
+}
+
+/*
+ * Debugging information
+ */
+void remote_player_network_imgui( ui_context *ctx, m4x4f pv )
+{
+   if( network_client.user_intent == k_server_intent_online )
+   {
+      if( !(steam_ready &&
+         (network_client.state == k_ESteamNetworkingConnectionState_Connected)))
+      {
+         char buf[128];
+         vg_str str;
+         vg_strnull( &str, buf, sizeof(buf) );
+         u32 fg = 0;
+         network_status_string( &str, &fg );
+         ui_text( ctx, (ui_rect){ vg.window_x - 200, 0, 200, 48 }, buf, 1, 
+                  k_ui_align_middle_center, fg );
+      }
+   }
+
+   if( !network_client.network_info ) 
+      return;
+
+   ui_rect panel = { (vg.window_x / 2) - 200, 0, 400, 600 };
+   ui_fill( ctx, panel, (ui_colour(ctx, k_ui_bg)&0x00ffffff)|0x50000000 );
+
+   ctx->font = &vgf_default_title;
+   ui_info( ctx, panel, "Network" );
+   ctx->font = &vgf_default_large;
+   ui_info( ctx, panel, "Status" );
+   ctx->font = &vgf_default_small;
+
+   char buf[512];
+   const char *netstatus = "PROGRAMMING ERROR";
+
+   struct { enum ESteamNetworkingConnectionState state; const char *str; }
+   states[] = {
+          { k_ESteamNetworkingConnectionState_None, "None" },
+          { k_ESteamNetworkingConnectionState_Connecting, 
+         (const char *[]){"Connecting -",
+                          "Connecting /",
+                          "Connecting |",
+                          "Connecting \\",
+                          }[(u32)(vg.time_real/0.25) & 0x3 ] },
+          { k_ESteamNetworkingConnectionState_FindingRoute, "Finding Route" },
+          { k_ESteamNetworkingConnectionState_Connected, "Connected" },
+          { k_ESteamNetworkingConnectionState_ClosedByPeer, "Closed by peer" },
+          { k_ESteamNetworkingConnectionState_ProblemDetectedLocally, 
+         "Problem Detected Locally" },
+          { k_ESteamNetworkingConnectionState_FinWait, "Fin Wait" },
+          { k_ESteamNetworkingConnectionState_Linger, "Linger" },
+          { k_ESteamNetworkingConnectionState_Dead, "Dead" }
+   };
+   for( u32 i=0; i<VG_ARRAY_LEN(states); i ++ )
+   {
+      if( states[i].state == network_client.state )
+      {
+         netstatus = states[i].str;
+         break;
+      }
+   }
+   snprintf( buf, 512, "Network: %s", netstatus );
+   ui_info( ctx, panel, buf );
+   ui_info( ctx, panel, "---------------------" );
+
+   if( network_client.state == k_ESteamNetworkingConnectionState_Connected )
+   {
+      ui_info( ctx, panel, "#-1: localplayer" );
+      
+      snprintf( buf, 512, "U%.3f/D%.3fkbs", 
+                netplayers.up_kbs, netplayers.down_kbs );
+      ui_info( ctx, panel, buf );
+
+      for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i++ )
+      {
+         struct network_player *player = &netplayers.list[i];
+         if( player->active )
+         {
+            const char *sysname = "invalid";
+
+            if( player->subsystem < k_player_subsystem_max )
+            {
+               sysname = player_subsystems[ player->subsystem ]->name;
+            }
+            snprintf( buf, 512, "#%u: %s [%s] D%.1fkbs", 
+                      i, player->username, sysname, player->down_kbs );
+            ui_info( ctx, panel, buf );
+         }
+      }
+   }
+   else 
+   {
+      ui_info( ctx, panel, "offline" );
+   }
+}
+
+static void remote_player_effect( struct network_player *player, 
+                                  player_pose *final_pose ){
+   /* effects */
+}
+
+/*
+ * write the remote players final_mtx 
+ */
+static void pose_remote_player( u32 index, 
+                                struct interp_frame *f0,
+                                struct interp_frame *f1 ){
+
+   struct network_player *player = &netplayers.list[ index ];
+
+   struct interp_buffer *buf = &netplayers.interp_data[ index ];
+   struct skeleton *sk = &localplayer.skeleton;
+   m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*index ];
+   struct player_board_pose *board_pose = &netplayers.board_poses[index];
+
+   struct player_subsystem_interface *sys0 = player_subsystems[f0->subsystem],
+                                     *sys1 = NULL;
+
+   struct player_board *board = 
+      addon_cache_item_if_loaded( k_addon_type_board, player->board_view_slot );
+
+   player_pose pose0, pose1, posed;
+   sys0->pose( &f0->data, &pose0 );
+
+   u8 instance_id = 0;
+
+   f32 t = 0.0f;
+
+   if( f1 ){
+      t = (buf->t - f0->timestamp) / (f1->timestamp - f0->timestamp);
+      t = vg_clampf( t, 0.0f, 1.0f );
+
+      sys1 = player_subsystems[f1->subsystem];
+      sys1->pose( &f1->data, &pose1 );
+
+      u16 bounds = f0->boundary_hash^f1->boundary_hash;
+
+      if( bounds & NETMSG_BOUNDARY_BIT )
+         t = 1.0f;
+
+      if( bounds & NETMSG_GATE_BOUNDARY_BIT ){
+         /* TODO: Extra work retransforming the root_co, instance_id.. etc */
+         t = 1.0f;
+      }
+
+      instance_id = f1->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
+      lerp_player_pose( &pose0, &pose1, t, &posed );
+      effect_blink_apply( &player->effect_data.blink, &posed, vg.time_delta );
+
+      apply_full_skeleton_pose( sk, &posed, final_mtx );
+
+      if( t < 0.5f ){
+         if( sys0->effects ) 
+            sys0->effects( &f0->data, final_mtx, board, &player->effect_data );
+      }
+      else{
+         if( sys1->effects ) 
+            sys1->effects( &f1->data, final_mtx, board, &player->effect_data );
+      }
+
+      memcpy( board_pose, &posed.board, sizeof(*board_pose) );
+   }
+   else {
+      instance_id = f0->flags & NETMSG_PLAYERFRAME_INSTANCE_ID;
+      effect_blink_apply( &player->effect_data.blink, &pose0, vg.time_delta );
+      apply_full_skeleton_pose( sk, &pose0, final_mtx );
+      if( sys0->effects ) 
+         sys0->effects( &f0->data, final_mtx, board, &player->effect_data );
+      memcpy( board_pose, &pose0.board, sizeof(*board_pose) );
+   }
+
+   if( f0->flags & (NETMSG_PLAYERFRAME_HAVE_GLIDER|
+                    NETMSG_PLAYERFRAME_GLIDER_ORPHAN) ){
+      player->render_glider = 1;
+
+      v3f co;
+      v4f q;
+      f32 s;
+
+      if( f1 ){
+         v3_lerp( f0->data_glider.root_co, f1->data_glider.root_co, t, co );
+         q_nlerp( f0->data_glider.root_q,  f1->data_glider.root_q,  t, q );
+         s = vg_lerpf( f0->data_glider.s, f1->data_glider.s, t );
+      }
+      else {
+         v3_copy( f0->data_glider.root_co, co );
+         v4_copy( f0->data_glider.root_q, q );
+         s = f0->data_glider.s;
+      }
+
+      v3f *mtx = netplayers.glider_mtx[ index ];
+      q_m3x3( q, mtx );
+      m3x3_scalef( mtx, s );
+      v3_copy( co, mtx[3] );
+   }
+   else
+      player->render_glider = 0;
+
+   if( player->world_match[ instance_id ] )
+      player->active_world = &world_static.instances[ instance_id ];
+}
+
+/* 
+ * animate remote player and store in final_mtx
+ */
+void animate_remote_player( u32 index )
+{
+   /*
+    * Trys to keep the cursor inside the buffer
+    */
+   f64 min_time = -999999999.9,
+       max_time =  999999999.9,
+       abs_max_time = -999999999.9;
+
+   struct interp_frame *minframe = NULL,
+                       *maxframe = NULL,
+                       *abs_max_frame = NULL;
+
+   struct interp_buffer *buf = &netplayers.interp_data[index];
+   for( u32 i=0; i<VG_ARRAY_LEN(buf->frames); i ++ ){
+      struct interp_frame *ifr = &buf->frames[i];
+
+      if( ifr->active ){
+         if( (ifr->timestamp > min_time) && (ifr->timestamp < buf->t) ){
+            min_time = ifr->timestamp;
+            minframe = ifr;
+         }
+
+         if( (ifr->timestamp < max_time) && (ifr->timestamp > buf->t) ){
+            max_time = ifr->timestamp;
+            maxframe = ifr;
+         }
+
+         if( ifr->timestamp > abs_max_time ){
+            abs_max_time = ifr->timestamp;
+            abs_max_frame = ifr;
+         }
+      }
+   }
+
+   struct network_player *player = &netplayers.list[ index ];
+   if( player->active != 2 )
+      player->active_world = NULL;
+   
+   if( minframe && maxframe ){
+      pose_remote_player( index, minframe, maxframe );
+      buf->t += vg.time_frame_delta;
+   }
+   else {
+      buf->t = abs_max_time - 0.25;
+
+      if( abs_max_frame )
+         pose_remote_player( index, abs_max_frame, NULL );
+      else 
+         return;
+   }
+}
+
+/*
+ * Update full final_mtx for all remote players
+ */
+void animate_remote_players(void)
+{
+   for( u32 i=0; i<VG_ARRAY_LEN(netplayers.list); i ++ ){
+      struct network_player *player = &netplayers.list[i];
+      if( !player->active ) continue;
+
+      animate_remote_player( i );
+   }
+}
+
+/*
+ * Draw remote players
+ */
+void render_remote_players( world_instance *world, vg_camera *cam )
+{
+   u32 draw_list[ NETWORK_MAX_PLAYERS ],
+       draw_list_count = 0,
+       gliders = 0;
+
+   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
+      struct network_player *player = &netplayers.list[i];
+      if( !player->active || player->isblocked ) continue;
+      if( player->active_world != world ) continue;
+
+#if 0
+      if( !player->isfriend && 
+            (world-world_static.instances == k_world_purpose_hub)) continue;
+#endif
+
+      draw_list[draw_list_count ++] = i;
+
+      if( player->render_glider )
+         gliders ++;
+   }
+
+   struct skeleton *sk = &localplayer.skeleton;
+
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+
+   for( u32 j=0; j<draw_list_count; j ++ ){
+      u32 index = draw_list[j];
+
+      struct network_player *player = &netplayers.list[index];
+      m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*index ];
+
+      struct player_model *model = 
+         addon_cache_item_if_loaded( k_addon_type_player, 
+                                     player->playermodel_view_slot );
+
+      if( !model ) model = &localplayer.fallback_model;
+      render_playermodel( cam, world, 0, model, sk, final_mtx );
+
+      struct player_board *board = 
+         addon_cache_item_if_loaded( k_addon_type_board,
+                                     player->board_view_slot );
+      render_board( cam, world, board, final_mtx[localplayer.id_board],
+                     &netplayers.board_poses[ index ], k_board_shader_player );
+   }
+
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+
+   if( !gliders )
+      return;
+
+   /* TODO: we really, really only need to do all this once. at some point
+    *       PLEASE figure out a good place to do this once per frame!
+    */
+   
+   shader_model_entity_use();
+   shader_model_entity_uTexMain( 0 );
+   shader_model_entity_uCamera( cam->transform[3] );
+   shader_model_entity_uPv( cam->mtx.pv );
+   
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
+
+   for( u32 j=0; j<draw_list_count; j ++ ){
+      u32 index = draw_list[j];
+
+      struct network_player *player = &netplayers.list[index];
+      if( !player->render_glider ) continue;
+
+      if( player->render_glider ){
+         v3f *glider_mtx = netplayers.glider_mtx[ index ];
+         render_glider_model( cam, world, glider_mtx, k_board_shader_entity );
+      }
+   }
+}
+
+static int remote_players_randomize( int argc, const char *argv[] ){
+   for( int i=0; i<NETWORK_MAX_PLAYERS; i ++ ){
+      struct network_player *player = &netplayers.list[i];
+
+      player->active = (vg_randu32(&vg.rand) & 0x1)? 2: 0;
+      player->isfriend = vg_randu32(&vg.rand) & vg_randu32(&vg.rand) & 0x1;
+      player->isblocked = vg_randu32(&vg.rand) & 
+                          vg_randu32(&vg.rand) & 
+                          vg_randu32(&vg.rand) & 0x1;
+      player->world_match[ 0 ] = vg_randu32(&vg.rand) & 0x1;
+      player->world_match[ 1 ] = 0;
+
+      if( player->world_match[0] )
+         player->active_world = &world_static.instances[0];
+      else
+         player->active_world = NULL;
+
+      for( int i=0; i<sizeof(player->username)-1; i ++ ){
+         player->username[i] = 'a' + (vg_randu32(&vg.rand) % 30);
+         player->username[i+1] = '\0';
+
+         if( (vg_randu32(&vg.rand) % 8) == 3 )
+            break;
+      }
+
+      for( int i=0; i<3; i ++ ){
+         player->medals[i] = vg_randu32(&vg.rand) % 3;
+      }
+
+      v3f pos;
+
+      vg_rand_sphere( &vg.rand, pos );
+      v3_muladds( localplayer.rb.co, pos, 100.0f,
+                  netplayers.final_mtx[ i*localplayer.skeleton.bone_count][3] );
+   }
+
+   return 0;
+}
+
+void remote_players_init(void)
+{
+   vg_console_reg_cmd( "add_test_players", remote_players_randomize, NULL );
+   vg_console_reg_var( "k_show_own_name", &k_show_own_name, 
+                       k_var_dtype_i32, 0 );
+   for( u32 i=0; i<NETWORK_SFX_QUEUE_LENGTH; i ++ ){
+      netplayers.sfx_queue[i].system = k_player_subsystem_invalid;
+   }
+}
+
+void remote_sfx_pre_update(void)
+{
+   for( u32 i=0; i<NETWORK_SFX_QUEUE_LENGTH; i ++ ){
+      struct net_sfx *si = &netplayers.sfx_queue[i];
+
+      if( si->system != k_player_subsystem_invalid ){
+         si->subframe -= vg.time_frame_delta;
+         if( si->subframe <= 0.0f ){
+            net_sfx_play( si );
+            si->system = k_player_subsystem_invalid;
+         }
+      }
+   }
+}
+
+/*
+ * animator->root_co of remote player
+ */
+static void remote_player_position( int id, v3f out_co ){
+   struct skeleton *sk = &localplayer.skeleton;
+   m4x3f *final_mtx = &netplayers.final_mtx[ sk->bone_count*id ];
+   v3_copy( final_mtx[0][3], out_co );
+}
+
+enum remote_player_gui_type {
+   k_remote_player_gui_type_stranger,
+   k_remote_player_gui_type_friend,
+   k_remote_player_gui_type_you,
+};
+
+/*
+ * Given players' root_co, get the screen point where we should draw tag info.
+ */
+static int player_tag_position( m4x4f pv, v3f root_co, ui_point out_point ){
+   v4f wpos;
+   v3_copy( root_co, wpos );
+   wpos[1] += 2.0f;
+   wpos[3] = 1.0f;
+
+   m4x4_mulv( pv, wpos, wpos );
+
+   if( wpos[3] > 0.0f ){
+      v2_muls( wpos, (1.0f/wpos[3]) * 0.5f, wpos );
+      v2_add( wpos, (v2f){ 0.5f, 0.5f }, wpos );
+
+      float k_max = 32000.0f;
+      
+      out_point[0] = vg_clampf(wpos[0] * vg.window_x, -k_max, k_max );
+      out_point[1] = vg_clampf((1.0f-wpos[1]) * vg.window_y, -k_max, k_max );
+      return 1;
+   }
+   else
+      return 0;
+}
+
+/*
+ * Draw chat box relative to the root tag position on the screen
+ */
+static void chat_box( ui_context *ctx,
+                      ui_point tag_root, f64 time, const char *message )
+{
+   if( (vg.time_real - time) > 15.0 ) 
+      return;
+
+   ui_rect wr;
+   wr[2] = ui_text_line_width( ctx, message ) + 8;
+   wr[3] = ctx->font->ch + 2;
+   wr[0] = tag_root[0]-(wr[2]/2);
+   wr[1] = tag_root[1] - wr[3] - 8;
+
+   ui_fill( ctx, wr, ui_opacity( ui_colour(ctx, k_ui_bg), 0.23f ) );
+   ui_text( ctx, wr, message, 1, k_ui_align_middle_center, 0 );
+}
+
+/*
+ * Draw full imgui for remote player
+ */
+static void remote_player_nametag( ui_context *ctx, ui_point tag_root, 
+                                   struct network_player *player )
+{
+   ctx->font = &vgf_default_large;
+
+   ui_rect wr;
+   wr[2] = VG_MAX( ui_text_line_width( ctx, player->username ), 140 ) + 8;
+   wr[3] = 32;
+   wr[0] = tag_root[0]-(wr[2]/2);
+   wr[1] = tag_root[1]-(wr[3]/2);
+
+   ui_fill( ctx, wr, ui_opacity( ui_colour(ctx, k_ui_bg), 0.23f ) );
+   ui_text( ctx, wr, player->username, 1, k_ui_align_middle_center, 0 );
+   ctx->font = &vgf_default_small;
+
+   /* medals */
+   int cols = 0;
+   for( int i=0; i<3; i ++ )
+      if( player->medals[i] ) 
+         cols ++;
+
+   char buf[32];
+   vg_str str;
+
+   if( cols )
+   {
+      f32 w = (f32)wr[2] / (f32)cols;
+      cols = 0;
+
+      for( int i=0; i<3; i ++ )
+      {
+         if( player->medals[i] )
+         {
+            ui_rect col = { wr[0] + (f32)cols*w, wr[1] + wr[3], 
+                            w, ctx->font->ch };
+
+            vg_strnull( &str, buf, 32 );
+#if 0
+            vg_strcatch( &str, (char)k_SRglyph_vg_circle );
+#endif
+            vg_strcati32( &str, player->medals[i] );
+
+            ui_text( ctx, col, buf, 1, k_ui_align_middle_center, 
+                     ui_colour( ctx, (enum ui_scheme_colour[]){ 
+                        k_ui_yellow, k_ui_gray, k_ui_orange }[i] ) );
+            
+            cols ++;
+         }
+      }
+   }
+}
+
+static void remote_player_world_gui( ui_context *ctx, m4x4f pv, v3f root_co, 
+                                     struct network_player *player  ){
+   ui_point tag_root;
+   if( !player_tag_position( pv, root_co, tag_root ) )
+      return;
+
+   if( player )
+   {
+      remote_player_nametag( ctx, tag_root, player );
+      chat_box( ctx, tag_root, player->chat_time, player->chat );
+   }
+   else 
+   {
+      if( netplayers.chatting )
+         chat_box( ctx, tag_root, vg.time_real, netplayers.chat_buffer );
+      else
+      {
+         chat_box( ctx, tag_root, 
+                   netplayers.chat_time, netplayers.chat_message );
+      }
+   }
+}
+
+static void remote_player_gui_info( ui_context *ctx, ui_rect box, 
+                                    const char *username,
+                                    const char *activity, 
+                                    enum remote_player_gui_type type,
+                                    int in_world ){
+
+   f32 opacity = in_world? 0.6f: 0.3f;
+
+   if( type == k_remote_player_gui_type_you )
+      ui_fill( ctx, box, ui_opacity( 0xff555555, opacity ) );
+   else
+      ui_fill( ctx, box, ui_opacity( 0xff000000, opacity ) );
+
+   if( type == k_remote_player_gui_type_friend )
+      ui_outline( ctx, box, -1, ui_opacity( 0xff00c4f0, opacity ), 0 );
+
+   ui_rect top, bottom;
+   ui_split_ratio( box, k_ui_axis_h, 0.6666f, 1, top, bottom );
+
+   u32 fg;
+   
+   if( type == k_remote_player_gui_type_friend )
+      fg = ui_colour( ctx, k_ui_yellow + (in_world? k_ui_brighter: 0) );
+   else
+      fg = ui_colour( ctx, in_world? k_ui_fg: k_ui_fg+4 );
+
+   ctx->font = &vgf_default_large;
+   ui_text( ctx, top, username, 1, k_ui_align_middle_center, fg );
+   ctx->font = &vgf_default_small;
+   ui_text( ctx, bottom, activity, 1, k_ui_align_middle_center, fg );
+}
+
+void remote_players_imgui_lobby( ui_context *ctx )
+{
+   if( network_client.user_intent == k_server_intent_online ){
+      if( !(steam_ready &&
+         (network_client.state == k_ESteamNetworkingConnectionState_Connected)))
+      {
+         return;
+      }
+   }
+
+   ui_px y = 50, width = 200, height = 42, gap = 2,
+         x = vg.window_x - width;
+
+   ctx->font = &vgf_default_large;
+   ui_text( ctx, (ui_rect){ x, 0, width, height }, 
+            "In World", 1, k_ui_align_middle_center, 0 );
+   ctx->font = &vgf_default_small;
+
+
+   ui_rect us = { x, y, width, height };
+   /* FIXME: your location */
+   remote_player_gui_info( ctx, us, steam_username_at_startup, "you",
+                           k_remote_player_gui_type_you, 1 );
+   y += height + gap;
+
+   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i ++ )
+   {
+      struct network_player *player = &netplayers.list[i];
+      if( !player->active || player->isblocked ) continue;
+
+      int in_same_world = player->active_world == world_current_instance();
+      if( !player->isfriend && !in_same_world )
+         continue;
+
+      const char *location = in_same_world? "": "another world";
+      if( player->region_flags & k_ent_region_flag_hasname ){
+         location = player->region;
+      }
+      
+      ui_rect box = { x, y, width, height };
+      remote_player_gui_info( ctx, box, player->username, location,
+                              player->isfriend, in_same_world );
+      y += height + gap;
+   }
+}
+
+void remote_players_imgui_world( ui_context *ctx, 
+                                 world_instance *world, m4x4f pv,
+                                 f32 max_dist, int geo_cull )
+{
+   ui_flush( ctx, k_ui_shader_colour, NULL );
+
+   for( u32 i=0; i<NETWORK_MAX_PLAYERS; i++ )
+   {
+      struct network_player *player = &netplayers.list[i];
+      if( player->active )
+      {
+         v3f co;
+         remote_player_position( i, co );
+
+         if( !player->active_world )
+            continue;
+         if( !player->isfriend && 
+               (world-world_static.instances == k_world_purpose_hub)) continue;
+
+         /* their in our active subworld */
+         if( player->active_world != world )
+         {
+            m4x3_mulv( global_miniworld.mmdl, co, co );
+            co[1] -= 2.0f; /* HACK lol */
+         }
+
+         f32 d2 = v3_dist2( co, localplayer.rb.co );
+
+         if( d2 > (max_dist*max_dist) )
+            continue;
+
+         f32 dist = sqrtf(d2);
+         f32 opacity = 0.95f * sqrtf(((max_dist-dist)/max_dist));
+
+         if( geo_cull ){
+            ray_hit hit;
+            hit.dist = dist;
+
+            v3f dir;
+            v3_sub( co, g_render.cam.pos, dir );
+            v3_normalize( dir );
+
+            if( ray_world( world, g_render.cam.pos, dir, &hit, 
+                           k_material_flag_ghosts ) ){
+               opacity *= 0.5f;
+            }
+         }
+
+         player->opacity = vg_lerpf( player->opacity, opacity,
+                                     vg.time_frame_delta * 2.0f );
+         
+         remote_player_world_gui( ctx, pv, co, player );
+
+         vg_ui.colour[3] = player->opacity;
+         ui_flush( ctx, k_ui_shader_colour, NULL );
+      }
+   }
+
+   vg_ui.colour[3] = 1.0f;
+   remote_player_world_gui( ctx, pv, localplayer.rb.co, NULL );
+   ui_flush( ctx, k_ui_shader_colour, NULL );
+}
+
+static void chat_escape( ui_context *ctx )
+{
+   netplayers.chatting = -1;
+}
+
+static void chat_enter( ui_context *ctx, char *buf, u32 len ){
+   vg_strncpy( buf, netplayers.chat_message, NETWORK_MAX_CHAT, 
+               k_strncpy_always_add_null );
+   netplayers.chatting = -1;
+   netplayers.chat_time = vg.time_real;
+   chat_send_message( buf );
+}
+
+void remote_players_chat_imgui( ui_context *ctx )
+{
+   if( netplayers.chatting == 1 )
+   {
+      ui_rect box = { 0, 0, 400, 40 },
+              window = { 0, 0, vg.window_x, vg.window_y };
+      ui_rect_center( window, box );
+
+      struct ui_textbox_callbacks callbacks = 
+      {
+         .enter = chat_enter,
+         .escape = chat_escape
+      };
+
+      ui_textbox( ctx, box, NULL, 
+                  netplayers.chat_buffer, NETWORK_MAX_CHAT, 1,
+                  UI_TEXTBOX_AUTOFOCUS, &callbacks );
+   }
+   else 
+   {
+      if( netplayers.chatting == -1 )
+      {
+         netplayers.chatting = 0;
+         srinput.state = k_input_state_resume;
+      }
+      else 
+      {
+         if( (skaterift.activity == k_skaterift_default) && 
+               button_down( k_srbind_chat ) ){
+            netplayers.chatting = 1;
+            netplayers.chat_buffer[0] = '\0';
+            srinput.state = k_input_state_pause;
+         }
+      }
+   }
+}
diff --git a/src/player_remote.h b/src/player_remote.h
new file mode 100644 (file)
index 0000000..3e4b67e
--- /dev/null
@@ -0,0 +1,107 @@
+#pragma once
+#include "player.h"
+#include "network.h"
+#include "network_common.h"
+#include "player_render.h"
+#include "player_effects.h"
+#include "player_api.h"
+
+#include "player_skate.h"
+#include "player_walk.h"
+#include "player_dead.h"
+#include "player_basic_info.h"
+#include "player_glide.h"
+
+#define NETWORK_SFX_QUEUE_LENGTH 12
+
+struct global_netplayers
+{
+   struct network_player {
+      int active, isfriend, isblocked;
+      u64 steamid;
+      u16 board_view_slot, playermodel_view_slot;
+      enum player_subsystem subsystem;
+
+      /* this is set IF they exist in a world that we have loaded */
+      world_instance *active_world;
+      int world_match[ k_world_max ];
+      u32 location_pstr; /* TODO: valid if active_world set. */
+
+      char username[ NETWORK_USERNAME_MAX ];
+      char items[k_netmsg_playeritem_max][ADDON_UID_MAX];
+      char chat[ NETWORK_MAX_CHAT ];
+      char region[ NETWORK_REGION_MAX ];
+      u32 region_flags;
+      f64 chat_time;
+
+      /* ui */
+      u32 medals[3];
+      f32 opacity;
+
+      u32 down_bytes;
+      f32 down_kbs;
+      
+      struct player_effects_data effect_data;
+      bool render_glider;
+   }
+   list[ NETWORK_MAX_PLAYERS ];
+
+   struct interp_buffer {
+      /* collect the most recent 6 frames of animation data */
+      struct interp_frame {
+         int active;
+         f64 timestamp;
+         enum player_subsystem subsystem;
+
+         u8 flags;
+         u16 boundary_hash;
+
+         union interp_animdata {
+            /* these aren't accessed directly, just used to take the 
+             * max(sizeof) all systems */
+            struct player_skate_animator __skate;
+            struct player_walk_animator __walk;
+            struct player_dead_animator __dead;
+            struct player_basic_info_animator __basic;
+         } 
+         data;
+
+         struct remote_glider_animator data_glider;
+      }
+      frames[ NETWORK_BUFFERFRAMES ];
+
+      f64 t;
+   }
+   interp_data[ NETWORK_MAX_PLAYERS ];
+
+   struct net_sfx sfx_queue[ NETWORK_SFX_QUEUE_LENGTH ];
+
+   m4x3f *final_mtx,
+         *glider_mtx;
+   struct player_board_pose board_poses[ NETWORK_MAX_PLAYERS ];
+
+   u32 up_bytes;
+   f32 up_kbs, down_kbs;
+   f64 last_data_measurement;
+
+   int chatting;
+   char chat_buffer[ NETWORK_MAX_CHAT ], chat_message[ NETWORK_MAX_CHAT ];
+   f64 chat_time;
+}
+extern netplayers;
+
+void player_remote_rx_200_300( SteamNetworkingMessage_t *msg );
+void remote_player_debug_update(void);
+void remote_player_send_playerframe(void);
+void animate_remote_player( u32 index );
+void animate_remote_players(void);
+void render_remote_players( world_instance *world, vg_camera *cam );
+void relink_all_remote_player_worlds(void);
+void player_remote_update_friendflags( struct network_player *remote );
+void remote_players_init(void);
+void remote_sfx_pre_update(void);
+void remote_player_network_imgui( ui_context *ctx, m4x4f pv );
+void remote_players_imgui_world( ui_context *ctx, world_instance *world, 
+                                 m4x4f pv, f32 max_dist, int geo_cull );
+void remote_players_imgui_lobby( ui_context *ctx );
+void remote_players_chat_imgui( ui_context *ctx );
diff --git a/src/player_render.c b/src/player_render.c
new file mode 100644 (file)
index 0000000..15dec10
--- /dev/null
@@ -0,0 +1,629 @@
+#include "player.h"
+#include "player_render.h"
+#include "vg/vg_camera.h"
+#include "player_model.h"
+#include "ent_skateshop.h"
+#include "audio.h"
+#include "input.h"
+
+#include "shaders/model_character_view.h"
+#include "shaders/model_board_view.h"
+#include "shaders/model_entity.h"
+#include "shaders/model_board_view.h"
+#include "depth_compare.h"
+
+#include "network.h"
+#include "player_remote.h"
+#include "player_glide.h"
+
+void player_load_animation_reference( const char *path )
+{
+   mdl_context *meta = &localplayer.skeleton_meta;
+   mdl_open( meta, path, vg_mem.rtmemory );
+   mdl_load_metadata_block( meta, vg_mem.rtmemory );
+   mdl_load_animation_block( meta, vg_mem.rtmemory );
+   mdl_close( meta );
+
+   struct skeleton *sk = &localplayer.skeleton;
+   skeleton_setup( sk, vg_mem.rtmemory, meta );
+
+   localplayer.id_world      = skeleton_bone_id( sk, "world" );
+   localplayer.id_hip        = skeleton_bone_id( sk, "hips" );
+   localplayer.id_chest      = skeleton_bone_id( sk, "chest" );
+   localplayer.id_ik_hand_l  = skeleton_bone_id( sk, "hand.IK.L" );
+   localplayer.id_ik_hand_r  = skeleton_bone_id( sk, "hand.IK.R" );
+   localplayer.id_ik_elbow_l = skeleton_bone_id( sk, "elbow.L" );
+   localplayer.id_ik_elbow_r = skeleton_bone_id( sk, "elbow.R" );
+   localplayer.id_head       = skeleton_bone_id( sk, "head" );
+   localplayer.id_foot_l  = skeleton_bone_id( sk, "foot.L" );
+   localplayer.id_foot_r  = skeleton_bone_id( sk, "foot.R" );
+   localplayer.id_ik_foot_l  = skeleton_bone_id( sk, "foot.IK.L" );
+   localplayer.id_ik_foot_r  = skeleton_bone_id( sk, "foot.IK.R" );
+   localplayer.id_board      = skeleton_bone_id( sk, "board" );
+   localplayer.id_wheel_l    = skeleton_bone_id( sk, "wheel.L" );
+   localplayer.id_wheel_r    = skeleton_bone_id( sk, "wheel.R" );
+   localplayer.id_ik_knee_l  = skeleton_bone_id( sk, "knee.L" );
+   localplayer.id_ik_knee_r  = skeleton_bone_id( sk, "knee.R" );
+   localplayer.id_eyes       = skeleton_bone_id( sk, "eyes" );
+
+   for( i32 i=0; i<sk->bone_count; i ++ ){
+      localplayer.skeleton_mirror[i] = 0;
+   }
+
+   for( i32 i=1; i<sk->bone_count-1; i ++ ){
+      struct skeleton_bone *si = &sk->bones[i];
+
+      char tmp[64];
+      vg_str str;
+      vg_strnull( &str, tmp, 64 );
+      vg_strcat( &str, si->name );
+
+      char *L = vg_strch( &str, 'L' );
+      if( !L ) continue;
+      u32 len = L-tmp;
+
+      for( i32 j=i+1; j<sk->bone_count; j ++ ){
+         struct skeleton_bone *sj = &sk->bones[j];
+
+         if( !strncmp( si->name, sj->name, len ) ){
+            if( sj->name[len] == 'R' ){
+               localplayer.skeleton_mirror[i] = j;
+               localplayer.skeleton_mirror[j] = i;
+               break;
+            }
+         }
+      }
+   }
+
+   setup_ragdoll_from_skeleton( sk, &localplayer.ragdoll );
+
+   /* allocate matrix buffers for localplayer and remote players */
+   u32 mtx_size = sizeof(m4x3f)*sk->bone_count;
+   localplayer.final_mtx = vg_linear_alloc( vg_mem.rtmemory, mtx_size );
+   netplayers.final_mtx = vg_linear_alloc( vg_mem.rtmemory, 
+                                           mtx_size*NETWORK_MAX_PLAYERS );
+   netplayers.glider_mtx = vg_linear_alloc( vg_mem.rtmemory,
+                                            sizeof(m4x3f)*NETWORK_MAX_PLAYERS );
+}
+
+/* TODO: Standard model load */
+
+void dynamic_model_load( mdl_context *ctx,
+                         struct dynamic_model_1texture *mdl, 
+                         const char *path, u32 *fixup_table )
+{
+   if( !mdl_arrcount( &ctx->textures ) )
+      vg_fatal_error( "No texture in model" );
+
+   mdl_texture *tex0 = mdl_arritm( &ctx->textures, 0 );
+   void *data = vg_linear_alloc( vg_mem.scratch, tex0->file.pack_size );
+   mdl_fread_pack_file( ctx, &tex0->file, data );
+
+   vg_tex2d_load_qoi_async( data, tex0->file.pack_size,
+                            VG_TEX2D_NEAREST|VG_TEX2D_CLAMP,
+                            &mdl->texture );
+
+   mdl_async_load_glmesh( ctx, &mdl->mesh, fixup_table );
+}
+
+void dynamic_model_unload( struct dynamic_model_1texture *mdl )
+{
+   mesh_free( &mdl->mesh );
+   glDeleteTextures( 1, &mdl->texture );
+}
+
+/* TODO: allow error handling */
+void player_board_load( struct player_board *board, const char *path )
+{
+   vg_linear_clear( vg_mem.scratch );
+
+   mdl_context ctx;
+   mdl_open( &ctx, path, vg_mem.scratch );
+   mdl_load_metadata_block( &ctx, vg_mem.scratch );
+
+   dynamic_model_load( &ctx, &board->mdl, path, NULL );
+
+   mdl_array_ptr markers;
+   MDL_LOAD_ARRAY( &ctx, &markers, ent_marker, vg_mem.scratch );
+
+   /* TODO: you get put into a new section, the above is standard mdl loads. */
+   for( int i=0; i<4; i++ )
+      board->wheels[i].indice_count = 0;
+   for( int i=0; i<2; i++ )
+      board->trucks[i].indice_count = 0;
+   board->board.indice_count = 0;
+
+   for( u32 i=0; i<mdl_arrcount(&ctx.meshs); i++ ){
+      mdl_mesh *mesh = mdl_arritm( &ctx.meshs, i );
+
+      if( mdl_entity_id_type( mesh->entity_id ) != k_ent_marker )
+         continue;
+
+      u32 index = mdl_entity_id_id( mesh->entity_id );
+      ent_marker *marker = mdl_arritm( &markers, index );
+
+      mdl_submesh *sm0 = mdl_arritm( &ctx.submeshs, mesh->submesh_start );
+      
+      const char *alias = mdl_pstr( &ctx, marker->pstr_alias );
+      u32 lr = marker->transform.co[0] > 0.0f? 1: 0,
+          fb = marker->transform.co[2] > 0.0f? 0: 1;
+
+      if( !strcmp( alias, "wheel" ) ){
+         u32 id = fb<<1 | lr;
+         board->wheels[ id ] = *sm0;
+         v3_copy( marker->transform.co, board->wheel_positions[ id ] );
+      }
+      else if( !strcmp( alias, "board" ) ){
+         board->board = *sm0;
+         v3_copy( marker->transform.co, board->board_position );
+      }
+      else if( !strcmp( alias, "truck" ) ){
+         board->trucks[ fb ] = *sm0;
+         v3_copy( marker->transform.co, board->truck_positions[ fb ] );
+      }
+   }
+
+   mdl_close( &ctx );
+}
+
+void player_board_unload( struct player_board *board )
+{
+   dynamic_model_unload( &board->mdl );
+}
+
+void player_model_load( struct player_model *board, const char *path)
+{
+   vg_linear_clear( vg_mem.scratch );
+
+   mdl_context ctx;
+   mdl_open( &ctx, path, vg_mem.scratch );
+   mdl_load_metadata_block( &ctx, vg_mem.scratch );
+
+   if( !ctx.armatures.count )
+      vg_fatal_error( "No armature in playermodel\n" );
+
+   mdl_armature *armature = mdl_arritm( &ctx.armatures, 0 );
+
+   u32 fixup_table[ armature->bone_count+1 ];
+   for( u32 i=0; i<armature->bone_count+1; i ++ )
+      fixup_table[i] = 0;
+
+   for( u32 i=1; i<localplayer.skeleton.bone_count; i ++ ){
+      struct skeleton_bone *sb = &localplayer.skeleton.bones[i];
+      u32 hash = vg_strdjb2( sb->name );
+
+      for( u32 j=1; j<armature->bone_count; j ++ ){
+         mdl_bone *bone = mdl_arritm( &ctx.bones, armature->bone_start+j );
+
+         if( mdl_pstreq( &ctx, bone->pstr_name, sb->name, hash ) ){
+            fixup_table[j+1] = i;
+            break;
+         }
+      }
+   }
+
+   dynamic_model_load( &ctx, &board->mdl, path, fixup_table );
+   mdl_close( &ctx );
+}
+
+void player_model_unload( struct player_model *board )
+{
+   dynamic_model_unload( &board->mdl );
+}
+
+void apply_full_skeleton_pose( struct skeleton *sk, player_pose *pose,
+                               m4x3f *final_mtx ){
+   m4x3f transform;
+   q_m3x3( pose->root_q, transform );
+   v3_copy( pose->root_co, transform[3] );
+   
+   if( pose->type == k_player_pose_type_ik ){
+      skeleton_apply_pose( sk, pose->keyframes, 
+                           k_anim_apply_defer_ik, final_mtx );
+      skeleton_apply_ik_pass( sk, final_mtx );
+      skeleton_apply_pose( sk, pose->keyframes, 
+                           k_anim_apply_deffered_only, final_mtx );
+      skeleton_apply_inverses( sk, final_mtx );
+      skeleton_apply_transform( sk, transform, final_mtx );
+   }
+   else if( pose->type == k_player_pose_type_fk_2 ){
+      skeleton_apply_pose( sk, pose->keyframes, 
+                           k_anim_apply_always, final_mtx );
+      skeleton_apply_inverses( sk, final_mtx );
+      skeleton_apply_transform( sk, transform, final_mtx );
+   }
+}
+
+void player__animate(void)
+{
+   struct player_subsystem_interface *sys = 
+      player_subsystems[localplayer.subsystem];
+
+   struct player_board *board = 
+      addon_cache_item_if_loaded( k_addon_type_board, 
+                                  localplayer.board_view_slot );
+
+   sys->animate();
+
+   player_pose *pose = &localplayer.pose;
+   sys->pose( sys->animator_data, pose );
+
+   struct skeleton *sk = &localplayer.skeleton;
+
+   if( localplayer.holdout_time > 0.0f ){
+      skeleton_lerp_pose( sk, 
+                          pose->keyframes,localplayer.holdout_pose.keyframes, 
+                          localplayer.holdout_time, pose->keyframes );
+
+      v3_muladds( pose->root_co, localplayer.holdout_pose.root_co, 
+                  localplayer.holdout_time, pose->root_co );
+      q_nlerp( pose->root_q, localplayer.holdout_pose.root_q, 
+               localplayer.holdout_time, pose->root_q );
+
+      localplayer.holdout_time -= vg.time_frame_delta / 0.25f;
+   }
+
+   effect_blink_apply( &localplayer.effect_data.blink,
+                       &localplayer.pose, vg.time_delta );
+   apply_full_skeleton_pose( sk, &localplayer.pose, localplayer.final_mtx );
+   
+   if( sys->effects ){
+      sys->effects( sys->animator_data, localplayer.final_mtx, board,
+                    &localplayer.effect_data );
+   }
+
+   skeleton_debug( sk, localplayer.final_mtx );
+
+   if( sys->post_animate )
+      sys->post_animate();
+
+   player__observe_system( localplayer.subsystem );
+   if( sys->sfx_comp )
+      sys->sfx_comp( sys->animator_data );
+
+   player__cam_iterate();
+}
+
+static void player_copy_frame_animator( replay_frame *frame ){
+   struct player_subsystem_interface *sys = 
+      player_subsystems[localplayer.subsystem];
+
+   if( sys->animator_size ){
+      void *src = replay_frame_data( frame, k_replay_framedata_animator );
+      memcpy( sys->animator_data, src, sys->animator_size );
+   }
+}
+
+void lerp_player_pose( player_pose *pose0, player_pose *pose1, f32 t,
+                       player_pose *posed ){
+   struct skeleton *sk = &localplayer.skeleton;
+   
+   v3_lerp( pose0->root_co, pose1->root_co, t, posed->root_co );
+   q_nlerp( pose0->root_q,  pose1->root_q,  t, posed->root_q );
+   posed->type = pose0->type;
+   posed->board.lean = vg_lerpf( pose0->board.lean, pose1->board.lean, t );
+
+   if( pose0->type != pose1->type ){
+      /* it would be nice to apply IK pass in-keyframes. TOO BAD! */
+      skeleton_copy_pose( sk, pose0->keyframes, posed->keyframes );
+   }
+   else {
+      skeleton_lerp_pose( sk, pose0->keyframes, pose1->keyframes, t, 
+                          posed->keyframes );
+   }
+}
+
+void player__observe_system( enum player_subsystem id )
+{
+   if( id != localplayer.observing_system ){
+      struct player_subsystem_interface *sysm1 = 
+         player_subsystems[ localplayer.observing_system ];
+
+      if( sysm1->sfx_kill ) sysm1->sfx_kill();
+      localplayer.observing_system = id;
+   }
+}
+
+void player__animate_from_replay( replay_buffer *replay )
+{
+   replay_frame *frame = replay->cursor_frame,
+                *next = NULL;
+   if( frame ){
+      next = frame->r;
+
+      struct player_subsystem_interface 
+         *sys0 = player_subsystems[frame->system];
+      void *a0 = replay_frame_data( frame, k_replay_framedata_animator );
+
+      struct replay_glider_data 
+         *g0 = replay_frame_data( frame, k_replay_framedata_glider ),
+         *g1;
+
+      f32 t = 0.0f;
+
+      if( next ){
+         t = replay_subframe_time( replay );
+
+         player_pose pose0, pose1;
+
+         struct player_subsystem_interface 
+            *sys1 = player_subsystems[next->system];
+         void *a1 = replay_frame_data( next, k_replay_framedata_animator );
+
+         sys0->pose( a0, &pose0 );
+         sys1->pose( a1, &pose1 );
+
+         lerp_player_pose( &pose0, &pose1, t, &localplayer.pose );
+         g1 = replay_frame_data( next,  k_replay_framedata_glider );
+      }
+      else{
+         sys0->pose( a0, &localplayer.pose );
+         g1 = NULL;
+      }
+
+      player__observe_system( frame->system );
+      if( sys0->sfx_comp ) 
+         sys0->sfx_comp( a0 );
+
+      if( g0 ){
+         if( g0->glider_orphan ){
+            if( g1 ){
+               v3_lerp( g0->co, g1->co, t, player_glide.rb.co );
+               q_nlerp( g0->q,  g1->q,  t, player_glide.rb.q );
+            }
+            else {
+               v3_copy( g0->co, player_glide.rb.co );
+               v4_copy( g0->q,  player_glide.rb.q );
+            }
+
+            rb_update_matrices( &player_glide.rb );
+         }
+
+         if( g1 )
+            player_glide.t = vg_lerpf( g0->t, g1->t, t );
+         else
+            player_glide.t = g0->t;
+
+         localplayer.have_glider   = g0->have_glider;
+         localplayer.glider_orphan = g0->glider_orphan;
+      }
+      else /* no glider data in g1, or edge case we dont care about */ {
+         localplayer.have_glider = 0;
+         localplayer.glider_orphan = 0;
+         player_glide.t = 0.0f;
+      }
+   }
+   else return;
+
+   apply_full_skeleton_pose( &localplayer.skeleton, &localplayer.pose,
+                             localplayer.final_mtx );
+}
+
+void player__pre_render(void)
+{
+   /* shadowing/ao info */
+   struct player_board *board = 
+      addon_cache_item_if_loaded( k_addon_type_board,
+                                  localplayer.board_view_slot );
+   v3f vp0, vp1;
+   if( board ){
+      v3_copy((v3f){0.0f,0.1f, board->truck_positions[0][2]}, vp0 );
+      v3_copy((v3f){0.0f,0.1f, board->truck_positions[1][2]}, vp1 );
+   }
+   else{
+      v3_zero( vp0 );
+      v3_zero( vp1 );
+   }
+
+   struct ub_world_lighting *ubo = &world_current_instance()->ub_lighting;
+   v3f *board_mtx = localplayer.final_mtx[ localplayer.id_board ];
+   m4x3_mulv( board_mtx, vp0, ubo->g_board_0 );
+   m4x3_mulv( board_mtx, vp1, ubo->g_board_1 );
+}
+
+void render_board( vg_camera *cam, world_instance *world,
+                   struct player_board *board, m4x3f root,
+                   struct player_board_pose *pose,
+                   enum board_shader shader )
+{
+   if( !board ) 
+      board = &localplayer.fallback_board;
+
+   /* TODO: 
+    *  adding depth compare to this shader
+    */
+
+   v3f inverse;
+
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, board->mdl.texture );
+
+   if( shader == k_board_shader_player )
+   {
+      shader_model_board_view_use();
+      shader_model_board_view_uTexMain( 0 );
+      shader_model_board_view_uCamera( cam->transform[3] );
+      shader_model_board_view_uPv( cam->mtx.pv );
+
+      shader_model_board_view_uDepthMode(1);
+      depth_compare_bind(
+         shader_model_board_view_uTexSceneDepth,
+         shader_model_board_view_uInverseRatioDepth,
+         shader_model_board_view_uInverseRatioMain,
+         cam );
+
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_board_view );
+   }
+   else if( shader == k_board_shader_entity )
+   {
+      shader_model_entity_use();
+      shader_model_entity_uTexMain( 0 );
+      shader_model_entity_uCamera( cam->transform[3] );
+      shader_model_entity_uPv( cam->mtx.pv );
+      
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
+   }
+
+   mesh_bind( &board->mdl.mesh );
+
+   m4x4f m4mdl;
+
+   if( board->board.indice_count ){
+      m4x3f mlocal;
+      m3x3_identity( mlocal );
+
+      mdl_keyframe kf;
+      v3_zero( kf.co );
+      q_identity( kf.q );
+      v3_zero( kf.s );
+
+      v4f qroll;
+      q_axis_angle( qroll, (v3f){0.0f,0.0f,1.0f}, pose->lean * 0.6f );
+      keyframe_rotate_around( &kf, (v3f){0.0f,0.11f,0.0f}, 
+                              (v3f){0.0f,0.0f,0.0f}, qroll );
+
+      v3_add( board->board_position, kf.co, mlocal[3] );
+      q_m3x3( kf.q, mlocal );
+
+      m4x3_mul( root, mlocal, mlocal );
+
+      if( shader == k_board_shader_entity ){
+         /* TODO: provide a way to supply previous mdl mtx? */
+         m4x3_expand( mlocal, m4mdl );
+         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
+         shader_model_entity_uPvmPrev( m4mdl );
+         shader_model_entity_uMdl( mlocal );
+      }
+      else
+         shader_model_board_view_uMdl( mlocal );
+
+      mdl_draw_submesh( &board->board );
+   }
+
+   for( int i=0; i<2; i++ ){
+      if( !board->trucks[i].indice_count )
+         continue;
+
+      m4x3f mlocal;
+      m3x3_identity( mlocal );
+      v3_copy( board->truck_positions[i], mlocal[3] );
+      m4x3_mul( root, mlocal, mlocal );
+
+      if( shader == k_board_shader_entity ){
+         m4x3_expand( mlocal, m4mdl );
+         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
+         shader_model_entity_uPvmPrev( m4mdl );
+         shader_model_entity_uMdl( mlocal );
+      }
+      else
+         shader_model_board_view_uMdl( mlocal );
+
+      mdl_draw_submesh( &board->trucks[i] );
+   }
+
+   for( int i=0; i<4; i++ ){
+      if( !board->wheels[i].indice_count )
+         continue;
+
+      m4x3f mlocal;
+      m3x3_identity( mlocal );
+      v3_copy( board->wheel_positions[i], mlocal[3] );
+      m4x3_mul( root, mlocal, mlocal );
+
+      if( shader == k_board_shader_entity ){
+         m4x3_expand( mlocal, m4mdl );
+         m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
+         shader_model_entity_uPvmPrev( m4mdl );
+         shader_model_entity_uMdl( mlocal );
+      }
+      else
+         shader_model_board_view_uMdl( mlocal );
+
+      mdl_draw_submesh( &board->wheels[i] );
+   }
+}
+
+void render_playermodel( vg_camera *cam, world_instance *world,
+                         int depth_compare,
+                         struct player_model *model,
+                         struct skeleton *skeleton,
+                         m4x3f *final_mtx )
+{
+   if( !model ) return;
+   
+   shader_model_character_view_use();
+
+       glActiveTexture( GL_TEXTURE0 );
+       glBindTexture( GL_TEXTURE_2D, model->mdl.texture );
+   shader_model_character_view_uTexMain( 0 );
+   shader_model_character_view_uCamera( cam->transform[3] );
+   shader_model_character_view_uPv( cam->mtx.pv );
+   shader_model_character_view_uDepthMode( depth_compare );
+   if( depth_compare )
+   {
+      depth_compare_bind(
+         shader_model_character_view_uTexSceneDepth,
+         shader_model_character_view_uInverseRatioDepth,
+         shader_model_character_view_uInverseRatioMain,
+         cam );
+   }
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_character_view );
+
+   glUniformMatrix4x3fv( _uniform_model_character_view_uTransforms,
+                         skeleton->bone_count,
+                         0,
+                         (const GLfloat *)final_mtx );
+   
+   mesh_bind( &model->mdl.mesh );
+   mesh_draw( &model->mdl.mesh );
+}
+
+void player__render( vg_camera *cam )
+{
+   world_instance *world = world_current_instance();
+   SDL_AtomicLock( &addon_system.sl_cache_using_resources );
+
+   struct player_model *model = 
+      addon_cache_item_if_loaded( k_addon_type_player, 
+                                  localplayer.playermodel_view_slot );
+
+   if( !model ) model = &localplayer.fallback_model;
+   render_playermodel( cam, world, 1, model, &localplayer.skeleton,
+                       localplayer.final_mtx );
+
+   struct player_board *board = 
+      addon_cache_item_if_loaded( k_addon_type_board,
+                                  localplayer.board_view_slot );
+
+   render_board( cam, world, board, localplayer.final_mtx[localplayer.id_board],
+                  &localplayer.pose.board, k_board_shader_player );
+
+   SDL_AtomicUnlock( &addon_system.sl_cache_using_resources );
+
+   glEnable( GL_CULL_FACE );
+   player_glide_render( cam, world, &localplayer.pose );
+   glDisable( GL_CULL_FACE );
+}
+
+void player_mirror_pose( mdl_keyframe pose[32], mdl_keyframe mirrored[32] )
+{
+   mdl_keyframe temp[32];
+
+   struct skeleton *sk = &localplayer.skeleton;
+   for( u32 i=1; i<sk->bone_count; i ++ ){
+      mdl_keyframe *dest = &temp[i-1];
+      u8 mapping = localplayer.skeleton_mirror[i];
+
+      if( mapping ) *dest = pose[mapping-1]; /* R */
+      else          *dest = pose[i-1];       /* L */
+
+      dest->co[2] *= -1.0f;
+      dest->q[0] *= -1.0f;
+      dest->q[1] *= -1.0f;
+   }
+
+   for( u32 i=0; i<sk->bone_count-1; i ++ ){
+      mirrored[i] = temp[i];
+   }
+}
diff --git a/src/player_render.h b/src/player_render.h
new file mode 100644 (file)
index 0000000..cfc48e7
--- /dev/null
@@ -0,0 +1,82 @@
+#pragma once
+#include "model.h"
+#include "skeleton.h"
+#include "vg/vg_camera.h"
+#include "world.h"
+#include "player_render.h"
+#include "player_api.h"
+#include "player_replay.h"
+
+enum eboard_truck{
+   k_board_truck_back = 0,
+   k_board_truck_front = 1
+};
+
+enum eboard_wheel{
+   k_board_wheel_fl = 0,
+   k_board_wheel_fr = 1,
+   k_board_wheel_bl = 2,
+   k_board_wheel_br = 3,
+};
+
+/* TODO: Fully featured dynamic models
+ * This is FAR from the final system we want at all, but it will do for now */
+struct dynamic_model_1texture{
+   glmesh mesh;
+   GLuint texture;
+};
+
+struct player_board{
+   struct dynamic_model_1texture mdl;
+
+   v4f wheel_positions[4],
+       truck_positions[2],
+       board_position;
+
+   mdl_submesh wheels[4],
+               trucks[2],
+               board;
+};
+
+struct player_model{
+   struct dynamic_model_1texture mdl;
+};
+
+enum board_shader{
+   k_board_shader_player,
+   k_board_shader_entity
+};
+
+void dynamic_model_load( mdl_context *ctx,
+                            struct dynamic_model_1texture *mdl, 
+                            const char *path, u32 *fixup_table );
+void dynamic_model_unload( struct dynamic_model_1texture *mdl );
+
+void player_board_load( struct player_board *mdl, const char *path );
+void player_board_unload( struct player_board *mdl );
+
+void player_model_load( struct player_model *board, const char *path);
+void player_model_unload( struct player_model *board );
+
+void render_board( vg_camera *cam, world_instance *world,
+                      struct player_board *board, m4x3f root,
+                      struct player_board_pose *pose,
+                      enum board_shader shader );
+
+void render_playermodel( vg_camera *cam, world_instance *world,
+                            int depth_compare,
+                            struct player_model *model,
+                            struct skeleton *skeleton,
+                            m4x3f *final_mtx );
+void apply_full_skeleton_pose( struct skeleton *sk, player_pose *pose,
+                               m4x3f *final_mtx );
+void lerp_player_pose( player_pose *pose0, player_pose *pose1, f32 t,
+                       player_pose *posed );
+void player_mirror_pose( mdl_keyframe pose[32], 
+                         mdl_keyframe mirrored[32] );
+void player__observe_system( enum player_subsystem id );
+void player_load_animation_reference( const char *path );
+void player__render( vg_camera *cam );
+void player__animate_from_replay( replay_buffer *replay );
+void player__animate(void);
+void player__pre_render(void);
diff --git a/src/player_replay.c b/src/player_replay.c
new file mode 100644 (file)
index 0000000..950ad78
--- /dev/null
@@ -0,0 +1,1207 @@
+#include "skaterift.h"
+#include "player.h"
+#include "player_replay.h"
+#include "input.h"
+#include "gui.h"
+#include "freecam.h"
+
+#include "player_walk.h"
+#include "player_skate.h"
+#include "player_dead.h"
+#include "player_glide.h"
+
+struct replay_globals player_replay = 
+{
+   .active_keyframe = -1,
+   .show_ui = 1,
+   .editor_mode = 0
+};
+
+void replay_clear( replay_buffer *replay )
+{
+   replay->head = NULL;
+   replay->tail = NULL;
+   replay->cursor_frame = NULL;
+   replay->statehead = NULL;
+   replay->cursor = -99999.9;
+}
+
+void *replay_frame_data( replay_frame *frame, enum replay_framedata type )
+{
+   if( frame->data_table[type][1] == 0 )
+      return NULL;
+
+   void *baseptr = frame;
+   return baseptr + frame->data_table[type][0];
+}
+
+static u16 replay_frame_calculate_data_offsets( 
+      u16 data_table[k_replay_framedata_rows][2] ){
+
+   u32 total = vg_align8( sizeof(replay_frame) );
+   for( u32 i=0; i<k_replay_framedata_rows; i++ ){
+      data_table[i][0] = total;
+      total += vg_align8(data_table[i][1]);
+
+      if( total > 0xffff )
+         vg_fatal_error( "Exceeded frame storage capacity\n" );
+   }
+   return total;
+}
+
+static void replay_tailpop( replay_buffer *replay ){
+   if( replay->cursor_frame == replay->tail )
+      replay->cursor_frame = NULL;
+   if( replay->statehead == replay->tail )
+      replay->statehead = NULL;
+
+   replay->tail = replay->tail->r;
+
+   if( replay->tail )
+      replay->tail->l = NULL;
+   else
+      replay->head = NULL;
+}
+
+static replay_frame *replay_newframe( replay_buffer *replay, 
+                                         u16 animator_size,
+                                         u16 gamestate_size,
+                                         u16 sfx_count,
+                                         bool save_glider ){
+   u16 data_table[ k_replay_framedata_rows ][2];
+   data_table[ k_replay_framedata_animator ][1]  = animator_size;
+   data_table[ k_replay_framedata_gamestate ][1] = gamestate_size;
+   data_table[ k_replay_framedata_sfx ][1] = sfx_count*sizeof(struct net_sfx);
+   data_table[ k_replay_framedata_internal_gamestate ][1] = 0;
+   if( gamestate_size )
+   {
+      data_table[ k_replay_framedata_internal_gamestate ][1] = 
+         sizeof( replay_gamestate );
+   }
+
+   data_table[ k_replay_framedata_glider ][1] = 0;
+   if( save_glider )
+   {
+      data_table[ k_replay_framedata_glider ][1] = 
+         sizeof(struct replay_glider_data);
+   }
+
+   u32 nextsize = replay_frame_calculate_data_offsets( data_table );
+
+   replay_frame *frame = NULL;
+   if( replay->head )
+   {
+      u32 headsize = replay->head->total_size,
+          nextpos  = ((void *)replay->head - replay->data) + headsize;
+
+      if( nextpos + nextsize > replay->size )
+      {
+         nextpos = 0;
+         
+         /* maintain contiguity */
+         while( replay->tail )
+         {
+            if( (void *)replay->tail - replay->data )
+               replay_tailpop( replay );
+            else break;
+         }
+      }
+
+check_again:;
+      u32 tailpos = (void *)replay->tail - replay->data;
+
+      if( tailpos >= nextpos )
+      {
+         if( nextpos + nextsize > tailpos )
+         {
+            replay_tailpop( replay );
+
+            if( replay->tail )
+               goto check_again;
+         }
+      }
+
+      frame = replay->data + nextpos;
+
+      if( replay->head )
+         replay->head->r = frame;
+   }
+   else
+      frame = replay->data;
+
+   for( u32 i=0; i<k_replay_framedata_rows; i++ )
+   {
+      frame->data_table[i][0] = data_table[i][0];
+      frame->data_table[i][1] = data_table[i][1];
+   }
+
+   frame->total_size = nextsize;
+   frame->l = replay->head;
+   frame->r = NULL;
+   replay->head = frame;
+   if( !replay->tail ) replay->tail = frame;
+   if( gamestate_size ) replay->statehead = frame;
+
+   return frame;
+}
+
+static void replay_emit_frame_sounds( replay_frame *frame ){
+   void *baseptr = frame;
+   u16 *inf = frame->data_table[k_replay_framedata_sfx];
+   struct net_sfx *buffer = baseptr + inf[0];
+   u32 count = inf[1] / sizeof(struct net_sfx);
+
+   for( u32 i=0; i<count; i ++ ){
+      net_sfx_play( buffer + i );
+   }
+}
+
+int replay_seek( replay_buffer *replay, f64 t )
+{
+   if( !replay->head ) return 0;
+
+   if( t < replay->tail->time ) t = replay->tail->time;
+   if( t > replay->head->time ) t = replay->head->time;
+
+   if( !replay->cursor_frame ) {
+      replay->cursor = replay->head->time;
+      replay->cursor_frame = replay->head;
+
+      if( fabs(replay->head->time-t) > fabs(replay->tail->time-t) ){
+         replay->cursor = replay->tail->time;
+         replay->cursor_frame = replay->tail;
+      }
+   }
+
+   f64 dir = t - replay->cursor;
+   if( dir == 0.0 ) return 0;
+   dir = vg_signf( dir );
+
+   
+   u32 i=4096;
+   while( i --> 0 ){
+      if( dir < 0.0 ){
+         if( t > replay->cursor_frame->time ) {
+            replay->cursor = t;
+            return 1;
+         }
+      }
+
+      replay_frame *next;
+      if( dir > 0.0 ) next = replay->cursor_frame->r;
+      else            next = replay->cursor_frame->l;
+
+      if( !next ) break;
+
+      if( dir > 0.0 ){
+         if( t < next->time ){
+            replay->cursor = t;
+            return 1;
+         }
+      }
+
+      replay_emit_frame_sounds( next );
+
+      replay->cursor_frame = next;
+      replay->cursor = next->time;
+
+      if( !i ) return 1;
+   }
+
+   replay->cursor = t;
+   return 0;
+}
+
+replay_frame *replay_find_recent_stateframe( replay_buffer *replay )
+{
+   replay_frame *frame = replay->cursor_frame;
+   u32 i=4096;
+   while( i --> 0 ){
+      if( !frame ) return frame;
+      if( frame->data_table[ k_replay_framedata_gamestate ][1] ) return frame;
+      frame = frame->l;
+   }
+
+   return NULL;
+}
+
+f32 replay_subframe_time( replay_buffer *replay )
+{
+   replay_frame *frame = replay->cursor_frame;
+   if( !frame ) return 0.0f;
+   replay_frame *next = frame->r;
+   if( next )
+   {
+      f64 l = next->time - frame->time,
+          t = (l <= (1.0/128.0))? 0.0: (replay->cursor - frame->time) / l;
+      return vg_clampf( t, 0.0f, 1.0f );
+   }
+   else 
+      return 0.0f;
+}
+
+void replay_get_frame_camera( replay_frame *frame, vg_camera *cam )
+{
+   cam->fov = frame->cam.fov;
+   v3_copy( frame->cam.pos, cam->pos );
+   v3_copy( frame->cam.angles, cam->angles );
+}
+
+void replay_get_camera( replay_buffer *replay, vg_camera *cam )
+{
+   cam->nearz = 0.1f;
+   cam->farz = 100.0f;
+   if( replay->cursor_frame )
+   {
+      replay_frame *next = replay->cursor_frame->r;
+
+      if( next )
+      {
+         vg_camera temp;
+         
+         replay_get_frame_camera( replay->cursor_frame, cam );
+         replay_get_frame_camera( next, &temp );
+         vg_camera_lerp( cam, &temp, replay_subframe_time( replay ), cam );
+      }
+      else 
+      {
+         replay_get_frame_camera( replay->cursor_frame, cam );
+      }
+   }
+   else 
+   {
+      v3_zero( cam->pos );
+      v3_zero( cam->angles );
+      cam->fov = 90.0f;
+   }
+}
+
+void skaterift_get_replay_cam( vg_camera *cam )
+{
+   replay_buffer *replay = &player_replay.local;
+
+   if( player_replay.active_keyframe != -1 )
+   {
+      replay_keyframe *kf = 
+         &player_replay.keyframes[player_replay.active_keyframe];
+
+      v3_copy( kf->cam.pos, cam->pos );
+      v3_copy( kf->cam.angles, cam->angles );
+      cam->fov = kf->cam.fov;
+      return;
+   }
+
+   if( player_replay.keyframe_count >= 2 )
+   {
+      for( u32 i=0; i<player_replay.keyframe_count-1; i ++ )
+      {
+         replay_keyframe *kf = &player_replay.keyframes[i];
+
+         if( (kf[0].time<=replay->cursor) && (kf[1].time>replay->cursor) )
+         {
+            f64 l = kf[1].time - kf[0].time,
+                t = (l <= (1.0/128.0))? 0.0: (replay->cursor-kf[0].time) / l;
+
+            if( player_replay.keyframe_count >= 3 )
+            {
+               f32 m_start = 0.5f, m_end = 0.5f;
+
+               if( i > 0 )
+               {
+                  if( (t < 0.5f) || (i==player_replay.keyframe_count-2) )
+                  {
+                     kf --;
+                  }
+               }
+
+               u32 last = player_replay.keyframe_count-1;
+               if( kf+0 == player_replay.keyframes ) m_start = 1.0f;
+               if( kf+2 == player_replay.keyframes+last ) m_end = 1.0f;
+
+               f32 ts = vg_lerpf( kf[0].time, kf[1].time, 1.0f-m_start ),
+                   te = vg_lerpf( kf[1].time, kf[2].time, m_end );
+
+               l = te-ts;
+               t = (replay->cursor-ts)/l;
+
+               /* 
+                * Adjust t, so that its derivative matches at the endpoints.
+                * Since t needs to go from 0 to 1, it will naturally change at 
+                * different rates between keyframes. So this smooths it out.
+                *
+                * Newton method, going through standard direct quadratic eq has 
+                * precision / other problems. Also we only care about 0>t>1. 
+                */
+               f32 b = (kf[1].time-ts)/l,
+                  x0 = 1.0-t;
+               for( u32 i=0; i<4; i ++ )
+               {
+                  f32 ix0 = 1.0f-x0,
+                      fx_x0 = 2.0f*b*x0*ix0 + ix0*ix0 - t,
+                      fxd_x0 = 2.0f*(-2.0f*b*x0 + b + x0 - 1.0f);
+                   x0 = x0 - (fx_x0/fxd_x0);
+               }
+               t = 1.0-x0;
+
+               f32 t0 = t*m_start+(1.0f-m_start),
+                   t1 = t*m_end;
+
+               v3f ps, pe, as, ae;
+               f32 fs, fe;
+
+               /* first order */
+               v3_lerp( kf[0].cam.pos, kf[1].cam.pos, t0, ps );
+               vg_camera_lerp_angles( kf[0].cam.angles, kf[1].cam.angles, 
+                                      t0, as );
+               fs = vg_lerpf( kf[0].cam.fov, kf[1].cam.fov, t0 );
+
+               v3_lerp( kf[1].cam.pos, kf[2].cam.pos, t1, pe );
+               vg_camera_lerp_angles( kf[1].cam.angles, kf[2].cam.angles, 
+                                      t1, ae );
+               fe = vg_lerpf( kf[1].cam.fov, kf[2].cam.fov, t1 );
+
+               /* second order */
+               v3_lerp( ps, pe, t, cam->pos );
+               vg_camera_lerp_angles( as, ae, t, cam->angles );
+               cam->fov = vg_lerpf( fs, fe, t );
+            }
+            else 
+            {
+               v3_lerp( kf[0].cam.pos,  kf[1].cam.pos, t, cam->pos );
+               vg_camera_lerp_angles( kf[0].cam.angles, kf[1].cam.angles, 
+                                      t, cam->angles );
+               cam->fov = vg_lerpf( kf[0].cam.fov, kf[1].cam.fov, t );
+            }
+            return;
+         }
+      }
+   }
+
+   replay_get_camera( replay, cam );
+}
+
+struct replay_rb
+{
+   v3f co, v, w;
+   v4f q;
+};
+
+void skaterift_record_frame( replay_buffer *replay, int force_gamestate )
+{
+   f64 delta      = 9999999.9,
+       statedelta = 9999999.9;
+
+   if( replay->head )
+      delta = vg.time - replay->head->time;
+
+   if( replay->statehead )
+      statedelta = vg.time - replay->statehead->time;
+
+   const f64 k_replay_rate = 1.0/30.0,
+             k_gamestate_rate = 0.5;
+
+   int save_frame = 0,
+       save_state = 0,
+       save_glider = 0;
+
+   if( force_gamestate ) save_state = 1;
+   if( statedelta > k_gamestate_rate ) save_state = 1;
+   if( delta > k_replay_rate ) save_frame = 1;
+   if( save_state ) save_frame = 1;
+
+   if( localplayer.have_glider || localplayer.glider_orphan ||
+       localplayer.subsystem == k_player_subsystem_glide ){
+      save_glider = 1;
+   }
+
+   if( !save_frame ) return;
+
+   u16 gamestate_size = 0;
+   if( save_state ){
+      /* TODO: have as part of system struct */
+      gamestate_size = (u32 []){ 
+         [k_player_subsystem_walk ] = sizeof(struct player_walk_state),
+         [k_player_subsystem_drive] = 0,
+         [k_player_subsystem_skate] = sizeof(struct player_skate_state),
+         [k_player_subsystem_dead ] = localplayer.ragdoll.part_count * 
+                                       sizeof(struct replay_rb),
+         [k_player_subsystem_glide] = sizeof(struct replay_rb),
+      }[ localplayer.subsystem ];
+   }
+
+   u16 animator_size = player_subsystems[localplayer.subsystem]->animator_size;
+   
+   replay_frame *frame = replay_newframe( replay,
+                                          animator_size, gamestate_size, 
+                                          localplayer.local_sfx_buffer_count,
+                                          save_glider );
+   frame->system = localplayer.subsystem;
+
+   if( save_state ){
+      replay_gamestate *gs = 
+         replay_frame_data( frame, k_replay_framedata_internal_gamestate );
+
+      gs->current_run_version = world_static.current_run_version;
+      gs->drowned = localplayer.drowned;
+
+      /* permanent block */
+      memcpy( &gs->rb, &localplayer.rb, sizeof(rigidbody) );
+      memcpy( &gs->glider_rb, &player_glide.rb, sizeof(rigidbody) );
+      memcpy( &gs->cam_control, &localplayer.cam_control, 
+               sizeof(struct player_cam_controller) );
+      v3_copy( localplayer.angles, gs->angles );
+
+      void *dst = replay_frame_data( frame, k_replay_framedata_gamestate );
+
+      /* subsytem/dynamic block */
+      if( localplayer.subsystem == k_player_subsystem_walk )
+         memcpy( dst, &player_walk.state, gamestate_size );
+      else if( localplayer.subsystem == k_player_subsystem_skate )
+         memcpy( dst, &player_skate.state, gamestate_size );
+      else if( localplayer.subsystem == k_player_subsystem_dead ){
+         struct replay_rb *arr = dst;
+         for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
+            rigidbody *rb = &localplayer.ragdoll.parts[i].rb;
+            v3_copy( rb->co, arr[i].co );
+            v3_copy( rb->w, arr[i].w );
+            v3_copy( rb->v, arr[i].v );
+            v4_copy( rb->q, arr[i].q );
+         }
+      }
+      else if( localplayer.subsystem == k_player_subsystem_glide ){
+         struct replay_rb *arr = dst;
+         rigidbody *rb = &player_glide.rb;
+         v3_copy( rb->co, arr[0].co );
+         v3_copy( rb->w, arr[0].w );
+         v3_copy( rb->v, arr[0].v );
+         v4_copy( rb->q, arr[0].q );
+      }
+   }
+
+   if( save_glider ){
+      struct replay_glider_data *inf = 
+         replay_frame_data( frame, k_replay_framedata_glider );
+
+      inf->have_glider   = localplayer.have_glider;
+      inf->glider_orphan = localplayer.glider_orphan;
+      inf->t             = player_glide.t;
+      v3_copy( player_glide.rb.co, inf->co );
+      v4_copy( player_glide.rb.q,  inf->q );
+   }
+
+   replay->cursor = vg.time;
+   replay->cursor_frame = frame;
+   frame->time = vg.time;
+
+   /* camera */
+   v3_copy( localplayer.cam.pos, frame->cam.pos );
+   if( localplayer.gate_waiting ){
+      m4x3_mulv( localplayer.gate_waiting->transport, 
+                  frame->cam.pos, frame->cam.pos );
+
+      v3f v0;
+      v3_angles_vector( localplayer.cam.angles, v0 );
+      m3x3_mulv( localplayer.gate_waiting->transport, v0, v0 );
+      v3_angles( v0, frame->cam.angles );
+   }
+   else 
+      v3_copy( localplayer.cam.angles, frame->cam.angles );
+
+   frame->cam.fov = localplayer.cam.fov;
+
+   /* animator */
+   void *dst = replay_frame_data( frame, k_replay_framedata_animator ),
+        *src = player_subsystems[localplayer.subsystem]->animator_data;
+   memcpy( dst, src, animator_size );
+
+   /* sound effects */
+   memcpy( replay_frame_data( frame, k_replay_framedata_sfx ),
+           localplayer.local_sfx_buffer,
+           sizeof(struct net_sfx)*localplayer.local_sfx_buffer_count );
+
+   localplayer.local_sfx_buffer_count = 0;
+}
+
+static void skaterift_restore_frame( replay_frame *frame )
+{
+   replay_gamestate *gs = 
+      replay_frame_data( frame, k_replay_framedata_internal_gamestate );
+   void *src = replay_frame_data( frame, k_replay_framedata_gamestate );
+   u16 src_size = frame->data_table[ k_replay_framedata_gamestate ][1];
+   world_static.current_run_version = gs->current_run_version;
+   localplayer.drowned = gs->drowned;
+
+   if(frame->system == k_player_subsystem_walk ){
+      memcpy( &player_walk.state, src, src_size );
+   }
+   else if( frame->system == k_player_subsystem_skate ){
+      memcpy( &player_skate.state, src, src_size );
+   }
+   else if( frame->system == k_player_subsystem_dead ){
+      player__dead_transition(0);
+      struct replay_rb *arr = src;
+
+      for( u32 i=0; i<localplayer.ragdoll.part_count; i ++ ){
+         struct ragdoll_part *part = &localplayer.ragdoll.parts[i];
+         rigidbody *rb = &part->rb;
+
+         v3_copy( arr[i].co, rb->co );
+         v3_copy( arr[i].w, rb->w );
+         v3_copy( arr[i].v, rb->v );
+         v4_copy( arr[i].q, rb->q );
+
+         v3_copy( arr[i].co, part->prev_co );
+         v4_copy( arr[i].q, part->prev_q );
+         rb_update_matrices( rb );
+      }
+   }
+   else if( frame->system == k_player_subsystem_glide ){
+      struct replay_rb *arr = src;
+      rigidbody *rb = &player_glide.rb;
+      v3_copy( arr[0].co, rb->co );
+      v3_copy( arr[0].w, rb->w );
+      v3_copy( arr[0].v, rb->v );
+      v4_copy( arr[0].q, rb->q );
+      rb_update_matrices( rb );
+   }
+
+   localplayer.subsystem = frame->system;
+
+   /* restore the seperated glider data if we have it */
+   if( frame->data_table[ k_replay_framedata_glider ][1] ){
+      struct replay_glider_data *inf = 
+         replay_frame_data( frame, k_replay_framedata_glider );
+
+      localplayer.have_glider   = inf->have_glider;
+      localplayer.glider_orphan = inf->glider_orphan;
+      player_glide.t            = inf->t;
+   }
+   else {
+      localplayer.have_glider = 0;
+      localplayer.glider_orphan = 0;
+      player_glide.t = 0.0f;
+   }
+
+   memcpy( &localplayer.rb, &gs->rb, sizeof(rigidbody) );
+   memcpy( &player_glide.rb, &gs->glider_rb, sizeof(rigidbody) );
+   v3_copy( gs->angles, localplayer.angles );
+
+   v3_copy( frame->cam.pos, localplayer.cam.pos );
+   v3_copy( frame->cam.angles, localplayer.cam.angles );
+   localplayer.cam.fov = frame->cam.fov;
+
+   memcpy( &localplayer.cam_control, &gs->cam_control, 
+            sizeof(struct player_cam_controller) );
+
+   /* chop end off replay */
+   frame->r = NULL;
+   player_replay.local.statehead = frame;
+   player_replay.local.head = frame;
+   player_replay.local.cursor_frame = frame;
+   player_replay.local.cursor = frame->time;
+   player_replay.replay_control = k_replay_control_scrub;
+   skaterift.activity = k_skaterift_default;
+   vg.time = frame->time;
+}
+
+static void skaterift_replay_resume(void){
+   replay_frame *prev = replay_find_recent_stateframe(&player_replay.local);
+
+   if( prev ){
+      player_replay.replay_control = k_replay_control_resume;
+      player_replay.resume_target = prev;
+      player_replay.resume_begin = player_replay.local.cursor;
+      player_replay.resume_transition = 0.0f;
+   }
+
+   gui_helper_clear();
+}
+
+static void skaterift_replay_update_helpers(void);
+
+void skaterift_replay_pre_update(void)
+{
+   if( skaterift.activity != k_skaterift_replay ) return;
+
+   bool input = player_replay.editor_mode^0x1;
+
+   if( player_replay.replay_control == k_replay_control_resume )
+   {
+      if( player_replay.local.cursor_frame == player_replay.resume_target ||
+          player_replay.local.cursor_frame == NULL )
+      {
+         skaterift_restore_frame( player_replay.resume_target );
+      }
+      else 
+      {
+         vg_slewf( &player_replay.resume_transition, 1.0f, 
+                   vg.time_frame_delta * (1.0f/1.0f) );
+
+         if( player_replay.resume_transition >= 1.0f )
+            skaterift_restore_frame( player_replay.resume_target );
+         else {
+            f64 target = vg_lerp( player_replay.resume_begin, 
+                           player_replay.resume_target->time, 
+                           vg_smoothstepf( player_replay.resume_transition ) );
+            if( replay_seek( &player_replay.local, target ) )
+               player_replay.track_velocity = 1.0f;
+            else
+               player_replay.track_velocity = 0.0f;
+         }
+      }
+   }
+   else 
+   {
+      if( input && button_down( k_srbind_replay_play ) )
+         player_replay.replay_control = k_replay_control_play;
+      if( input && button_down( k_srbind_replay_freecam ) )
+      {
+         player_replay.use_freecam ^= 0x1;
+
+         if( player_replay.use_freecam )
+         {
+            replay_get_camera( &player_replay.local, 
+                               &player_replay.replay_freecam );
+         }
+         skaterift_replay_update_helpers();
+      }
+
+      f32 target_speed = 0.0f;
+      if( input )
+         target_speed = axis_state( k_sraxis_replay_h ) * 5.0;
+
+      if( input && button_press( k_srbind_reset ) )
+         target_speed += -2.0;
+
+      if( fabsf(target_speed) > 0.01f )
+         player_replay.replay_control = k_replay_control_scrub;
+
+      if( player_replay.replay_control == k_replay_control_play )
+         target_speed = 1.0;
+
+      vg_slewf( &player_replay.track_velocity, target_speed, 
+                18.0f*vg.time_frame_delta );
+
+      if( fabsf( player_replay.track_velocity ) > 0.0001f )
+      {
+         f64 target = player_replay.local.cursor;
+         target += player_replay.track_velocity * vg.time_frame_delta;
+
+         if( !replay_seek( &player_replay.local, target ) )
+            player_replay.track_velocity = 0.0f;
+      }
+
+      if( input && button_down( k_srbind_mback ) )
+      {
+         if( player_replay.local.statehead )
+            skaterift_restore_frame( player_replay.local.statehead );
+         else
+            skaterift.activity = k_skaterift_default;
+         srinput.state = k_input_state_resume;
+         gui_helper_clear();
+      }
+
+      if( input )
+      {
+         if( player_replay.use_freecam )
+         {
+            freecam_preupdate();
+         }
+         else 
+         {
+            if( button_down( k_srbind_replay_resume ) )
+            {
+               skaterift_replay_resume();
+            }
+         }
+      }
+   }
+}
+
+static void skaterift_replay_update_helpers(void)
+{
+   player_replay.helper_resume->greyed = player_replay.use_freecam;
+
+   vg_str freecam_text;
+   vg_strnull( &freecam_text, player_replay.helper_freecam->text, 
+               GUI_HELPER_TEXT_LENGTH );
+   vg_strcat( &freecam_text, 
+               player_replay.use_freecam? "Exit freecam": "Freecam" );
+}
+
+static void replay_show_helpers(void)
+{
+   gui_helper_clear();
+   vg_str text;
+
+   if( gui_new_helper( input_axis_list[k_sraxis_replay_h], &text ) )
+      vg_strcat( &text, "Scrub" );
+
+   if( (player_replay.helper_resume = gui_new_helper( 
+               input_button_list[k_srbind_replay_resume], &text )) )
+      vg_strcat( &text, "Resume" );
+   
+   if( gui_new_helper( input_button_list[k_srbind_replay_play], &text ))
+      vg_strcat( &text, "Playback" );
+
+   player_replay.helper_freecam = gui_new_helper(
+         input_button_list[k_srbind_replay_freecam], &text );
+
+   skaterift_replay_update_helpers();
+}
+
+void skaterift_replay_post_render(void)
+{
+#ifndef SR_ALLOW_REWIND_HUB
+   if( world_static.active_instance != k_world_purpose_client )
+      return;
+#endif
+
+   /* capture the current resume frame at the very last point */
+   if( button_down( k_srbind_reset ) )
+   {
+      if( skaterift.activity == k_skaterift_default )
+      {
+         localplayer.rewinded_since_last_gate = 1;
+         skaterift.activity = k_skaterift_replay;
+         skaterift_record_frame( &player_replay.local, 1 );
+         if( player_replay.local.head )
+         {
+            player_replay.local.cursor = player_replay.local.head->time;
+            player_replay.local.cursor_frame = player_replay.local.head;
+         }
+         player_replay.replay_control = k_replay_control_scrub;
+         replay_show_helpers();
+      }
+   }
+}
+
+void skaterift_replay_init(void)
+{
+   u32 bytes = 1024*1024*10;
+   player_replay.local.data = vg_linear_alloc( vg_mem.rtmemory, bytes );
+   player_replay.local.size = bytes;
+   replay_clear( &player_replay.local );
+}
+
+void skaterift_replay_debug_info( ui_context *ctx )
+{
+   player__debugtext( ctx, 2, "replay info" );
+   replay_buffer *replay = &player_replay.local;
+
+   u32 head = 0,
+       tail = 0;
+   if( replay->tail ) tail = (void *)replay->tail - replay->data;
+   if( replay->head ) head = (void *)replay->head - replay->data;
+
+   player__debugtext( ctx, 1, "head @%u | tail @%u\n", head, tail );
+
+   if( replay->statehead )
+   {
+      for( u32 i=0; i<k_replay_framedata_rows; i++ )
+      {
+         player__debugtext( ctx, 1, "[%u]: [%hu, %hu]\n", i,
+              replay->statehead->data_table[i][0],
+              replay->statehead->data_table[i][1] );
+      }
+      u32 state = (void *)replay->statehead - replay->data;
+      player__debugtext( ctx, 1, "gs @%u\n", state );
+      player__debugtext( ctx, 1, "gamestate_size: %hu\n", 
+           replay->statehead->data_table[k_replay_framedata_gamestate][1] );
+   }
+   else
+      player__debugtext( ctx, 1, "gs @NULL\n" );
+
+   f64 start = replay->cursor,
+       end   = replay->cursor;
+   if( replay->tail ) start = replay->tail->time;
+   if( replay->head ) end = replay->head->time;
+
+   f64 cur = replay->cursor - start,
+       len = end - start;
+
+   player__debugtext( ctx, 1, "cursor: %.2fs / %.2fs\n", cur, len );
+}
+
+static int _keyframe_cmp( const void *p1, const void *p2 )
+{
+   const replay_keyframe *kf1 = p1, *kf2 = p2;
+   return kf1->time > kf2->time;
+}
+
+static void replay_keyframe_sort(void)
+{
+   qsort( player_replay.keyframes, player_replay.keyframe_count,
+          sizeof(replay_keyframe), _keyframe_cmp );
+}
+
+static void replay_fly_edit_keyframe( ui_context *ctx, replay_keyframe *kf )
+{
+   if( ui_click_down( ctx, UI_MOUSE_LEFT ) )
+   {
+      /* init freecam */
+      v3_copy( kf->cam.pos, player_replay.replay_freecam.pos );
+      v3_copy( kf->cam.angles, player_replay.replay_freecam.angles );
+      v3_zero( player_replay.freecam_v );
+      v3_zero( player_replay.freecam_w );
+      player_replay.replay_freecam.fov = kf->cam.fov;
+   }
+
+   /* move freecam */
+   ui_capture_mouse( ctx, 0 );
+   freecam_preupdate();
+
+   if( vg_getkey(SDLK_q) )
+      player_replay.freecam_v[1] -= vg.time_frame_delta*6.0f*20.0f;
+   if( vg_getkey(SDLK_e) )
+      player_replay.freecam_v[1] += vg.time_frame_delta*6.0f*20.0f;
+
+   v3_copy( player_replay.replay_freecam.pos, g_render.cam.pos );
+   v3_copy( player_replay.replay_freecam.angles, g_render.cam.angles);
+   g_render.cam.fov = player_replay.replay_freecam.fov;
+
+   v3_copy( g_render.cam.pos, kf->cam.pos );
+   v3_copy( g_render.cam.angles, kf->cam.angles );
+   kf->cam.fov = g_render.cam.fov;
+}
+
+void skaterift_replay_imgui( ui_context *ctx )
+{
+   if( skaterift.activity != k_skaterift_replay ) return;
+
+   /* extra keys for entering editor */
+   static u8 f1_key = 0;
+   u8 f1_now = vg_getkey(SDLK_F1);
+   if( f1_now && !f1_key && player_replay.show_ui )
+   {
+      player_replay.editor_mode ^= 0x1;
+
+      if( player_replay.editor_mode )
+         gui_helper_clear();
+      else
+         replay_show_helpers();
+   }
+   f1_key = f1_now;
+
+   static u8 f2_key = 0;
+   u8 f2_now = vg_getkey(SDLK_F2);
+   if( f2_now && !f2_key )
+   {
+      player_replay.show_ui ^= 0x1;
+   }
+   f2_key = f2_now;
+
+   if( player_replay.editor_mode )
+   {
+      static u8 space_key = 0;
+      u8 space_now = vg_getkey(SDLK_SPACE);
+      if( space_now & !space_key )
+      {
+         player_replay.replay_control ^= k_replay_control_play;
+      }
+      space_key = space_now;
+   }
+
+   if( !player_replay.show_ui ) return;
+
+   if( player_replay.editor_mode )
+   {
+      u32 colour = ui_opacity( ui_colour(ctx,k_ui_fg), 0.3333f );
+      ui_rect cx = { vg.window_x/2, 0, 1, vg.window_y },
+              cy = { 0, vg.window_y/2, vg.window_x, 1 };
+      ui_fill( ctx, cx, colour );
+      ui_fill( ctx, cy, colour );
+   }
+
+   replay_buffer *replay = &player_replay.local;
+   f64 start = replay->cursor,
+       end   = replay->cursor;
+   if( replay->tail ) start = replay->tail->time;
+   if( replay->head ) end = replay->head->time;
+   f64 len = end - start,
+       cur = (replay->cursor - start) / len;
+
+   char buffer[ 128 ];
+
+   /* mainbar */
+   ui_px height = 32,
+         cwidth = 2;
+   ui_rect timeline = { 0, 0, vg.window_x, height };
+   ui_fill( ctx, timeline, ui_colour( ctx, k_ui_bg ) );
+
+   /* cursor frame block */
+   if( replay->cursor_frame )
+   {
+      if( replay->cursor_frame->r )
+      {
+         f64 l = (replay->cursor_frame->r->time-replay->cursor_frame->time)/len,
+             s = (replay->cursor_frame->time - start) / len;
+         ui_rect box = { s*(f64)vg.window_x, 0, 
+                         VG_MAX(4,(ui_px)(l*vg.window_x)), timeline[3]+2 };
+         ui_fill( ctx, box, ui_colour( ctx, k_ui_bg+4 ) );
+      }
+   }
+
+   /* cursor */
+   ui_rect cusor = { cur * (f64)vg.window_x - (cwidth/2), 0, 
+                     cwidth, (player_replay.editor_mode? 0: 16) + timeline[3] };
+   ui_fill( ctx, cusor, ui_colour( ctx, k_ui_bg+7 ) );
+
+   /* latest state marker */
+   if( replay->statehead )
+   {
+      f64 t = (replay->statehead->time - start) / len;
+      ui_rect tag = { t*(f64)vg.window_x, 0, 2, timeline[3]+8 };
+      ui_fill( ctx, tag, ui_colour( ctx, k_ui_green+k_ui_brighter ) );
+   }
+
+   /* previous state marker */
+   replay_frame *prev = replay_find_recent_stateframe( replay );
+   if( prev )
+   {
+      f64 t = (prev->time - start) / len;
+      ui_rect tag = { t*(f64)vg.window_x, 0, 2, timeline[3]+8 };
+      ui_fill( ctx, tag, ui_colour( ctx, k_ui_yellow+k_ui_brighter ) );
+   }
+
+   snprintf( buffer, 128, "-%.2fs (F1: Edit replay)", (end-replay->cursor) );
+   ui_text( ctx, timeline, buffer, 1, k_ui_align_middle_left, 0 );
+   ui_text( ctx, timeline, "0s", 1, k_ui_align_middle_right, 0 );
+
+   if( !player_replay.editor_mode ) return;
+   ui_capture_mouse( ctx, 1 );
+
+   ui_rect panel = { 0, timeline[3] + 20, 200, 400 };
+   ui_fill( ctx, panel, ui_opacity( ui_colour( ctx, k_ui_bg ), 0.5f ) );
+   ui_rect_pad( panel, (ui_px[2]){4,4} );
+
+   if( ui_button( ctx, panel, 
+            (player_replay.replay_control == k_replay_control_play)? 
+               "Pause (space)": "Play (space)" ) == k_ui_button_click )
+   {
+      player_replay.replay_control ^= k_replay_control_play;
+   }
+
+   /* script bar */
+   ui_rect script = { 0, height + 2, vg.window_x, 16 };
+   ui_fill( ctx, script, ui_colour( ctx, k_ui_bg ) );
+   f64 mouse_t = start + ((f64)ctx->mouse[0] / (f64)vg.window_x)*len;
+
+   /* keyframe draw and select */
+   bool absorb_by_keyframe = 0;
+   ui_px lx = 0;
+   for( u32 i=0; i<player_replay.keyframe_count; i ++ )
+   {
+      replay_keyframe *kf = &player_replay.keyframes[i];
+      f64 t = (kf->time-start)/len;
+
+      ui_px x = t*(f64)vg.window_x-8;
+
+      /* draw connections between keyframes */
+      if( i )
+      {
+         ui_rect con = { lx, script[1]+7, x-lx, 1 };
+         ui_fill( ctx, con, ui_colour( ctx, k_ui_blue ) );
+         vg_line( kf->cam.pos, player_replay.keyframes[i-1].cam.pos, VG__BLUE );
+      }
+
+      /* keyframe selection */
+      ui_rect tag = { x, script[1], 16, 16 };
+
+      if( ui_inside_rect( tag, ctx->mouse ) )
+      {
+         absorb_by_keyframe = 1;
+
+         if( ui_click_down( ctx, UI_MOUSE_LEFT ) )
+         {
+            if( player_replay.active_keyframe != i )
+            {
+               player_replay.active_keyframe = i;
+               replay_seek( &player_replay.local, kf->time );
+            }
+         }
+         else
+         {
+            ui_outline( ctx, tag, 1, ui_colour(ctx, k_ui_fg), 0 );
+         }
+      }
+
+      /* edit controls */
+      u32 drag_colour = ui_opacity( ui_colour(ctx, k_ui_bg+2), 0.5f );
+      if( i == player_replay.active_keyframe )
+      {
+         ui_outline( ctx, tag, 2, ui_colour(ctx, k_ui_fg), 0 );
+
+         ui_rect tray = { tag[0]+8-32, tag[1]+16+2, 64, 16 };
+         ui_rect dragbar = { tray[0]+16, tray[1], 32, 16 };
+
+         bool pos_correct = 0;
+
+         if( ui_inside_rect( dragbar, ctx->mouse_click ) )
+         {
+            if( ui_clicking( ctx, UI_MOUSE_LEFT ) )
+            {
+               drag_colour = ui_opacity( ui_colour(ctx,k_ui_fg), 0.5f );
+               pos_correct = 1;
+               replay_seek( &player_replay.local, mouse_t );
+            }
+            else if( ui_click_up( ctx, UI_MOUSE_LEFT ) )
+            {
+               pos_correct = 1;
+               kf->time = mouse_t;
+               replay_keyframe_sort();
+
+               for( u32 j=0; j<player_replay.keyframe_count; j ++ )
+               {
+                  if( player_replay.keyframes[j].time == mouse_t )
+                  {
+                     player_replay.active_keyframe = j;
+                     break;
+                  }
+               }
+            }
+
+            if( pos_correct )
+            {
+               tag[0] = ctx->mouse[0]-8;
+               tray[0] = tag[0]+8-32;
+               dragbar[0] = tray[0]+16;
+            }
+         }
+
+         if( ui_inside_rect( dragbar, ctx->mouse ) )
+         {
+            ctx->cursor = k_ui_cursor_hand;
+         }
+
+         if( !pos_correct )
+         {
+            ui_fill( ctx, tray, 
+                     ui_opacity( ui_colour( ctx, k_ui_bg+2 ), 0.5f ) );
+         }
+
+         ui_fill( ctx, dragbar, drag_colour );
+         ui_text( ctx, dragbar, ":::", 1, k_ui_align_middle_center, 0 );
+
+         if( !pos_correct )
+         {
+            ui_rect btn = { tray[0], tray[1], 16, 16 };
+            if( ui_button_text( ctx, btn, "X", 1 ) == k_ui_button_click )
+            {
+               for( u32 j=i; j<player_replay.keyframe_count-1; j ++ )
+                  player_replay.keyframes[j] = player_replay.keyframes[j+1];
+
+               player_replay.keyframe_count --;
+               player_replay.active_keyframe = -1;
+            }
+
+            ui_rect btn1 = { tray[0]+48, tray[1], 16, 16 };
+
+            enum ui_button_state mask_using = 
+                  k_ui_button_holding_inside |
+                  k_ui_button_holding_outside |
+                  k_ui_button_click;
+
+            if( ui_button_text( ctx, btn1, "E", 1 ) & mask_using )
+            {
+               replay_fly_edit_keyframe( ctx, kf );
+               vg_ui_set_mouse_pos( btn1[0]+8, btn1[1]+8 );
+            }
+         }
+      }
+
+      ui_fill( ctx, tag, ui_colour( ctx, k_ui_blue ) );
+      lx = x;
+   }
+
+   /* adding keyframes */
+   if( ui_inside_rect( script, ctx->mouse ) )
+   {
+      ctx->cursor = k_ui_cursor_hand;
+
+      ui_rect cursor = { ctx->mouse[0], script[1], 4, 16 };
+      ui_fill( ctx, cursor, ui_colour( ctx, k_ui_fg ) );
+
+      if( !absorb_by_keyframe && ui_click_down( ctx, UI_MOUSE_LEFT ) )
+      {
+         u32 max = VG_ARRAY_LEN( player_replay.keyframes );
+         if( player_replay.keyframe_count == max )
+         {
+            ui_start_modal( ctx, "Maximum keyframes reached", UI_MODAL_BAD );
+         }
+         else 
+         {
+            replay_keyframe *kf = 
+               &player_replay.keyframes[player_replay.keyframe_count++];
+
+            kf->time = mouse_t;
+            v3_copy( g_render.cam.pos, kf->cam.pos );
+            v3_copy( g_render.cam.angles, kf->cam.angles );
+            kf->cam.fov = g_render.cam.fov;
+
+            replay_keyframe_sort();
+         }
+      }
+   }
+
+   /* timeline scrub */
+   bool start_in_timeline = 
+      ui_clicking(ctx, UI_MOUSE_LEFT) && 
+      ui_inside_rect(timeline, ctx->mouse_click);
+   if( (ui_inside_rect( timeline, ctx->mouse )) || start_in_timeline )
+   {
+      ui_rect cursor = { ctx->mouse[0], timeline[1], 4, timeline[3] };
+      ui_fill( ctx, cursor, ui_colour( ctx, k_ui_fg ) );
+      ctx->cursor = k_ui_cursor_ibeam;
+
+      if( ui_clicking( ctx, UI_MOUSE_LEFT ) && start_in_timeline )
+      {
+         replay_seek( &player_replay.local, mouse_t );
+         player_replay.active_keyframe = -1;
+      }
+   }
+
+   if( ui_button( ctx, panel, "Clear keyframes" ) == k_ui_button_click )
+   {
+      player_replay.keyframe_count = 0;
+   }
+
+   if( (ui_button( ctx, panel, "Hide UI (F2)" ) == k_ui_button_click) )
+   {
+      player_replay.show_ui ^= 0x1;
+   }
+
+   if( player_replay.active_keyframe != -1 )
+   {
+      replay_keyframe *kf = 
+         &player_replay.keyframes[ player_replay.active_keyframe ];
+
+      enum ui_button_state mask_using = 
+            k_ui_button_holding_inside |
+            k_ui_button_holding_outside |
+            k_ui_button_click;
+
+      if( ui_button( ctx, panel, "Edit cam" ) & mask_using )
+      {
+         replay_fly_edit_keyframe( ctx, kf );
+      }
+   }
+
+   ui_info( ctx, panel, "World settings" );
+   f32 new_time = world_current_instance()->time;
+   if( ui_slider( ctx, panel, "Time of day", 0, 1, &new_time ) )
+   {
+      world_current_instance()->time = new_time;
+   }
+
+   ui_info( ctx, panel, "" );
+   if( ui_button( ctx, panel, "Exit editor (F1)" ) == k_ui_button_click )
+   {
+      player_replay.editor_mode = 0;
+      replay_show_helpers();
+   }
+
+   /* TODO: Add Q/E scrub here too.
+    *       Add replay trimming 
+    */
+}
diff --git a/src/player_replay.h b/src/player_replay.h
new file mode 100644 (file)
index 0000000..313b4d4
--- /dev/null
@@ -0,0 +1,125 @@
+#pragma once
+#include "player_render.h"
+#include "vg/vg_rigidbody.h"
+
+typedef struct replay_buffer replay_buffer;
+typedef struct replay_frame replay_frame;
+typedef struct replay_keyframe replay_keyframe;
+
+typedef struct replay_gamestate replay_gamestate;
+typedef struct replay_sfx replay_sfx;
+
+struct replay_buffer {
+   void *data;
+   u32 size; /* bytes */
+
+   replay_frame *head, *tail, *cursor_frame,
+                *statehead;
+   f64 cursor;
+};
+
+enum replay_framedata{
+   k_replay_framedata_animator,
+   k_replay_framedata_gamestate,
+   k_replay_framedata_internal_gamestate,
+   k_replay_framedata_sfx,
+   k_replay_framedata_glider,
+   k_replay_framedata_rows
+};
+
+struct replay_cam
+{
+   v3f pos, angles;
+   f32 fov;
+};
+
+struct replay_frame 
+{
+   struct replay_cam cam;
+   f64 time;
+
+   replay_frame *l, *r;
+
+   enum player_subsystem system;
+   u16 total_size;
+   u16 data_table[k_replay_framedata_rows][2];
+};
+
+/* player-defined replay frames */
+struct replay_keyframe
+{
+   struct replay_cam cam;
+   f64 time;
+};
+
+struct replay_gamestate 
+{
+   rigidbody rb, glider_rb; /* TODO: these don't need to be saved with their 
+                                     full matrices */
+   v3f angles;
+   struct player_cam_controller cam_control;
+   u32 current_run_version;
+   bool drowned;
+};
+
+/* we save this per-anim-frame. if there glider is existing in any state */
+struct replay_glider_data 
+{
+   bool have_glider, glider_orphan;
+   f32 t;
+   v3f co; 
+   v4f q;
+};
+
+struct replay_sfx {
+   u32 none;
+};
+
+struct replay_globals 
+{
+   replay_buffer local;
+   replay_frame *resume_target;
+   f64 resume_begin;
+   f32 resume_transition;
+
+   enum replay_control {
+      k_replay_control_scrub = 0x00,
+      k_replay_control_play  = 0x01,
+      k_replay_control_resume= 0x02
+   }
+   replay_control;
+   f32 track_velocity;
+   struct gui_helper *helper_resume, *helper_freecam;
+
+   vg_camera replay_freecam;
+
+   bool use_freecam;
+   bool show_ui;
+   v3f freecam_v, freecam_w;
+
+   i32 editor_mode;
+
+   replay_keyframe keyframes[32];
+   u32 keyframe_count;
+   i32 active_keyframe;
+}
+extern player_replay;
+
+int replay_seek( replay_buffer *replay, f64 t );
+
+replay_frame *replay_find_recent_stateframe( replay_buffer *replay );
+void replay_get_camera( replay_buffer *replay, vg_camera *cam );
+void replay_get_frame_camera( replay_frame *frame, vg_camera *cam );
+f32 replay_subframe_time( replay_buffer *replay );
+void replay_clear( replay_buffer *replay );
+void *
+replay_frame_data( replay_frame *frame, enum replay_framedata type );
+
+void skaterift_replay_pre_update(void);
+void skaterift_replay_imgui( ui_context *ctx );
+void skaterift_replay_debug_info( ui_context *ctx );
+void skaterift_record_frame( replay_buffer *replay, 
+                                    int force_gamestate );
+void skaterift_replay_post_render(void);
+void skaterift_replay_init(void);
+void skaterift_get_replay_cam( vg_camera *cam );
diff --git a/src/player_skate.c b/src/player_skate.c
new file mode 100644 (file)
index 0000000..aea7027
--- /dev/null
@@ -0,0 +1,3664 @@
+#include "player_skate.h"
+#include "player.h"
+#include "audio.h"
+#include "vg/vg_perlin.h"
+#include "vg/vg_lines.h"
+#include "menu.h"
+#include "ent_skateshop.h"
+#include "addon.h"
+#include "input.h"
+#include "ent_tornado.h"
+
+#include "vg/vg_rigidbody.h"
+#include "scene_rigidbody.h"
+#include "player_glide.h"
+#include "player_dead.h"
+#include "player_walk.h"
+#include <string.h>
+
+struct player_skate player_skate;
+struct player_subsystem_interface player_subsystem_skate = 
+{
+   .system_register = player__skate_register,
+   .bind = player__skate_bind,
+   .pre_update = player__skate_pre_update,
+   .update = player__skate_update,
+   .post_update = player__skate_post_update,
+   .im_gui = player__skate_im_gui,
+   .animate = player__skate_animate,
+   .pose = player__skate_pose,
+   .effects = player__skate_effects,
+   .post_animate = player__skate_post_animate,
+   .network_animator_exchange = player__skate_animator_exchange,
+   .sfx_oneshot = player__skate_sfx_oneshot,
+   .sfx_comp = player__skate_comp_audio,
+   .sfx_kill = player__skate_kill_audio,
+
+   .animator_data = &player_skate.animator,
+   .animator_size = sizeof(player_skate.animator),
+   .name = "Skate"
+};
+
+void player__skate_bind(void){
+   struct skeleton *sk = &localplayer.skeleton;
+   rb_update_matrices( &localplayer.rb );
+
+   struct { struct skeleton_anim **anim; const char *name; }
+   bindings[] = {
+      { &player_skate.anim_grind,        "pose_grind" },
+      { &player_skate.anim_grind_jump,   "pose_grind_jump" },
+      { &player_skate.anim_stand,        "pose_stand" },
+      { &player_skate.anim_highg,        "pose_highg" },
+      { &player_skate.anim_air,          "pose_air" },
+      { &player_skate.anim_slide,        "pose_slide" },
+      { &player_skate.anim_push,         "push" },
+      { &player_skate.anim_push_reverse, "push_reverse" },
+      { &player_skate.anim_ollie,        "ollie" },
+      { &player_skate.anim_ollie_reverse,"ollie_reverse" },
+      { &player_skate.anim_grabs,        "grabs" },
+      { &player_skate.anim_handplant,    "handplant" },
+   };
+
+   for( u32 i=0; i<VG_ARRAY_LEN(bindings); i++ )
+      *bindings[i].anim = skeleton_get_anim( sk, bindings[i].name );
+}
+
+void player__skate_kill_audio(void){
+   audio_lock();
+   if( player_skate.aud_main ){
+      player_skate.aud_main = 
+         audio_channel_fadeout( player_skate.aud_main, 0.1f );
+   }
+   if( player_skate.aud_air ){
+      player_skate.aud_air = 
+         audio_channel_fadeout( player_skate.aud_air, 0.1f );
+   }
+   if( player_skate.aud_slide ){
+      player_skate.aud_slide = 
+         audio_channel_fadeout( player_skate.aud_slide, 0.1f );
+   }
+   audio_unlock();
+}
+
+/* 
+ * Collision detection routines
+ *
+ *
+ */
+
+/*
+ * Does collision detection on a sphere vs world, and applies some smoothing
+ * filters to the manifold afterwards
+ */
+static int skate_collide_smooth( m4x3f mtx, f32 r, rb_ct *man ){
+   world_instance *world = world_current_instance();
+
+   int len = 0;
+   len = rb_sphere__scene( mtx, r, NULL, world->geo_bh, man,
+                           k_material_flag_walking );
+
+   for( int i=0; i<len; i++ ){
+      man[i].rba = &localplayer.rb;
+      man[i].rbb = NULL;
+   }
+
+   rb_manifold_filter_coplanar( man, len, 0.03f );
+
+   if( len > 1 ){
+      rb_manifold_filter_backface( man, len );
+      rb_manifold_filter_joint_edges( man, len, 0.03f );
+      rb_manifold_filter_pairs( man, len, 0.03f );
+   }
+   int new_len = rb_manifold_apply_filtered( man, len );
+   if( len && !new_len )
+      len = 1;
+   else
+      len = new_len;
+
+   return len;
+}
+
+struct grind_info
+{
+   v3f co, dir, n;
+};
+
+static int skate_grind_scansq( v3f pos, v3f dir, float r,
+                                  struct grind_info *inf ){
+   world_instance *world = world_current_instance();
+
+   v4f plane;
+   v3_copy( dir, plane );
+   v3_normalize( plane );
+   plane[3] = v3_dot( plane, pos );
+
+   boxf box;
+   v3_add( pos, (v3f){ r, r, r }, box[1] );
+   v3_sub( pos, (v3f){ r, r, r }, box[0] );
+
+   struct grind_sample{
+      v2f co;
+      v2f normal;
+      v3f normal3,
+          centroid;
+   }
+   samples[48];
+   int sample_count = 0;
+
+   v2f support_min,
+       support_max;
+
+   v3f support_axis;
+   v3_cross( plane, (v3f){0,1,0}, support_axis );
+   v3_normalize( support_axis );
+   
+   bh_iter it;
+   bh_iter_init_box( 0, &it, box );
+   i32 idx;
+   
+   while( bh_next( world->geo_bh, &it, &idx ) ){
+      u32 *ptri = &world->scene_geo.arrindices[ idx*3 ];
+      v3f tri[3];
+
+      struct world_surface *surf = world_tri_index_surface(world,ptri[0]);
+      if( !(surf->info.flags & k_material_flag_grindable) )
+         continue;
+
+      for( int j=0; j<3; j++ )
+         v3_copy( world->scene_geo.arrvertices[ptri[j]].co, tri[j] );
+
+      for( int j=0; j<3; j++ ){
+         int i0 = j,
+             i1 = (j+1) % 3;
+         
+         struct grind_sample *sample = &samples[ sample_count ];
+         v3f co;
+
+         if( plane_segment( plane, tri[i0], tri[i1], co ) ){
+            v3f d;
+            v3_sub( co, pos, d );
+            if( v3_length2( d ) > r*r )
+               continue;
+
+            v3f va, vb, normal;
+            v3_sub( tri[1], tri[0], va );
+            v3_sub( tri[2], tri[0], vb );
+            v3_cross( va, vb, normal );
+
+            sample->normal[0] = v3_dot( support_axis, normal );
+            sample->normal[1] = normal[1];
+            sample->co[0]     = v3_dot( support_axis, d );
+            sample->co[1]     = d[1];
+
+            v3_copy( normal, sample->normal3 ); /* normalize later
+                                                   if we want to us it */
+
+            v3_muls(                      tri[0], 1.0f/3.0f, sample->centroid );
+            v3_muladds( sample->centroid, tri[1], 1.0f/3.0f, sample->centroid );
+            v3_muladds( sample->centroid, tri[2], 1.0f/3.0f, sample->centroid );
+
+            v2_normalize( sample->normal );
+            sample_count ++;
+
+            if( sample_count == VG_ARRAY_LEN( samples ) )
+               goto too_many_samples;
+         }
+      }
+   }
+
+too_many_samples:
+
+   if( sample_count < 2 )
+      return 0;
+
+   v3f average_direction,
+       average_normal;
+
+   v2f min_co, max_co;
+   v2_fill( min_co,  INFINITY );
+   v2_fill( max_co, -INFINITY );
+
+   v3_zero( average_direction );
+   v3_zero( average_normal );
+
+   int passed_samples = 0;
+   
+   for( int i=0; i<sample_count-1; i++ ){
+      struct grind_sample *si, *sj;
+
+      si = &samples[i];
+
+      for( int j=i+1; j<sample_count; j++ ){
+         if( i == j )
+            continue;
+
+         sj = &samples[j];
+
+         /* non overlapping */
+         if( v2_dist2( si->co, sj->co ) >= (0.01f*0.01f) )
+            continue;
+
+         /* not sharp angle */
+         if( v2_dot( si->normal, sj->normal ) >= 0.7f )
+            continue;
+
+         /* not convex */
+         v3f v0;
+         v3_sub( sj->centroid, si->centroid, v0 );
+         if( v3_dot( v0, si->normal3 ) >= 0.0f ||
+             v3_dot( v0, sj->normal3 ) <= 0.0f )
+            continue;
+
+         v2_minv( sj->co, min_co, min_co );
+         v2_maxv( sj->co, max_co, max_co );
+         
+         v3f n0, n1, dir;
+         v3_copy( si->normal3, n0 );
+         v3_copy( sj->normal3, n1 );
+         v3_cross( n0, n1, dir );
+
+         if( v3_length2( dir ) <= 0.000001f )
+            continue;
+
+         v3_normalize( dir );
+
+         /* make sure the directions all face a common hemisphere */
+         v3_muls( dir, vg_signf(v3_dot(dir,plane)), dir );
+         v3_add( average_direction, dir, average_direction );
+
+         float yi = si->normal3[1],
+               yj = sj->normal3[1];
+
+         if( yi > yj ) v3_add( si->normal3, average_normal, average_normal );
+         else          v3_add( sj->normal3, average_normal, average_normal );
+
+         passed_samples ++;
+      }
+   }
+
+   if( !passed_samples )
+      return 0;
+
+   if( (v3_length2( average_direction ) <= 0.001f) ||
+       (v3_length2( average_normal ) <= 0.001f ) )
+      return 0;
+
+   float div = 1.0f/(float)passed_samples;
+   v3_normalize( average_direction );
+   v3_normalize( average_normal );
+
+   v2f average_coord;
+   v2_add( min_co, max_co, average_coord );
+   v2_muls( average_coord, 0.5f, average_coord );
+
+   v3_muls( support_axis, average_coord[0], inf->co );
+   inf->co[1] += average_coord[1];
+   v3_add( pos, inf->co, inf->co );
+   v3_copy( average_normal, inf->n );
+   v3_copy( average_direction, inf->dir );
+
+   vg_line_point( inf->co, 0.02f, VG__GREEN );
+   vg_line_arrow( inf->co, average_direction, 0.3f, VG__GREEN );
+   vg_line_arrow( inf->co, inf->n, 0.2f, VG__CYAN );
+
+   return passed_samples;
+}
+
+static void reset_jump_info( jump_info *inf ){
+   inf->log_length = 0;
+   inf->land_dist = 0.0f;
+   inf->score = 0.0f;
+   inf->type = k_prediction_unset;
+   v3_zero( inf->apex );
+}
+
+static int create_jumps_to_hit_target( jump_info *jumps,
+                                          v3f target, float max_angle_delta,
+                                          float gravity ){
+   /* calculate the exact 2 solutions to jump onto that grind spot */
+
+   v3f v0;
+   v3_sub( target, localplayer.rb.co, v0 );
+
+   v3f ax;
+   v3_copy( v0, ax );
+   ax[1] = 0.0f;
+   v3_normalize( ax );
+
+   v2f d = { v3_dot( ax, v0 ),               v0[1] },
+       v = { v3_dot( ax, localplayer.rb.v ), localplayer.rb.v[1] };
+
+   float a = atan2f( v[1], v[0] ),
+         m = v2_length( v ),
+         root = m*m*m*m - gravity*(gravity*d[0]*d[0] + 2.0f*d[1]*m*m);
+
+   int valid_count = 0;
+
+   if( root > 0.0f ){
+      root = sqrtf( root );
+      float a0 = atanf( (m*m + root) / (gravity * d[0]) ),
+            a1 = atanf( (m*m - root) / (gravity * d[0]) );
+
+      if( fabsf(a0-a) < max_angle_delta ){
+         jump_info *inf = &jumps[ valid_count ++ ];
+         reset_jump_info( inf );
+
+         v3_muls( ax, cosf( a0 ) * m, inf->v );
+         inf->v[1] += sinf( a0 ) * m;
+         inf->land_dist = d[0] / (cosf(a0)*m);
+         inf->gravity = gravity;
+
+         v3_copy( target, inf->log[inf->log_length ++] );
+      }
+
+      if( fabsf(a1-a) < max_angle_delta ){
+         jump_info *inf = &jumps[ valid_count ++ ];
+         reset_jump_info( inf );
+
+         v3_muls( ax, cosf( a1 ) * m, inf->v );
+         inf->v[1] += sinf( a1 ) * m;
+         inf->land_dist = d[0] / (cosf(a1)*m);
+         inf->gravity = gravity;
+
+         v3_copy( target, inf->log[inf->log_length ++] );
+      }
+   }
+
+   return valid_count;
+}
+
+void player__approximate_best_trajectory(void)
+{
+   world_instance *world0 = world_current_instance();
+
+   float k_trace_delta = vg.time_fixed_delta * 10.0f;
+   struct player_skate_state *state = &player_skate.state;
+
+   state->air_start = vg.time;
+   v3_copy( localplayer.rb.v, state->air_init_v );
+   v3_copy( localplayer.rb.co, state->air_init_co );
+
+   player_skate.possible_jump_count = 0;
+
+   v3f axis;
+   v3_cross( localplayer.rb.v, localplayer.rb.to_world[1], axis );
+   v3_normalize( axis );
+
+   /* at high slopes, Y component is low */
+   float upness      = localplayer.rb.to_world[1][1],
+         angle_begin = -(1.0f-fabsf( upness )),
+         angle_end   =   1.0f;
+
+   struct grind_info grind;
+   int grind_located = 0;
+   float grind_located_gravity = k_gravity;
+
+
+   v3f launch_v_bounds[2];
+
+   for( int i=0; i<2; i++ ){
+      v3_copy( localplayer.rb.v, launch_v_bounds[i] );
+      float ang = (float[]){ angle_begin, angle_end }[ i ];
+            ang *= 0.15f;
+
+      v4f qbias;
+      q_axis_angle( qbias, axis, ang );
+      q_mulv( qbias, launch_v_bounds[i], launch_v_bounds[i] );
+   }
+
+   for( int m=0;m<=30; m++ ){
+      jump_info *inf = 
+         &player_skate.possible_jumps[ player_skate.possible_jump_count ++ ];
+      reset_jump_info( inf );
+
+      v3f launch_co, launch_v, co0, co1;
+      v3_copy( localplayer.rb.co, launch_co );
+      v3_copy( localplayer.rb.v,  launch_v );
+      v3_copy( launch_co, co0 );
+      world_instance *trace_world = world0;
+
+      float vt  = (float)m * (1.0f/30.0f),
+            ang = vg_lerpf( angle_begin, angle_end, vt ) * 0.15f;
+
+      v4f qbias;
+      q_axis_angle( qbias, axis, ang );
+      q_mulv( qbias, launch_v, launch_v );
+
+      float yaw_sketch = 1.0f-fabsf(upness);
+
+      float yaw_bias = ((float)(m%3) - 1.0f) * 0.08f * yaw_sketch;
+      q_axis_angle( qbias, localplayer.rb.to_world[1], yaw_bias );
+      q_mulv( qbias, launch_v, launch_v );
+
+      float gravity_bias = vg_lerpf( 0.85f, 1.4f, vt ),
+            gravity      = k_gravity * gravity_bias;
+      inf->gravity = gravity;
+      v3_copy( launch_v, inf->v );
+
+      /* initial conditions */
+      v3f v;
+      v3_copy( launch_v, v );
+      v3_copy( launch_co, co1 );
+
+      for( int i=1; i<=50; i++ ){
+         f32 t = (f32)i * k_trace_delta;
+
+         /* integrate forces */
+         v3f a;
+         ent_tornado_forces( co1, v, a );
+         a[1] -= gravity;
+
+         /* position */
+         v3_muladds( co1, v, k_trace_delta, co1 );
+         v3_muladds( co1, a, 0.5f*k_trace_delta*k_trace_delta, co1 );
+
+         /* velocity */
+         v3_muladds( v, a, k_trace_delta, v );
+
+         int search_for_grind = 1;
+         if( grind_located ) search_for_grind = 0;
+         if( v[1] > 0.0f ) search_for_grind = 0;
+
+         /* REFACTOR */
+
+         v3f closest={0.0f,0.0f,0.0f};
+         if( search_for_grind ){
+            if( bh_closest_point(trace_world->geo_bh,co1,closest,1.0f) != -1 ){
+               float min_dist = 0.75f;
+                     min_dist *= min_dist;
+
+               if( v3_dist2( closest, launch_co ) < min_dist )
+                  search_for_grind = 0;
+
+               v3f bound[2];
+
+               for( int j=0; j<2; j++ ){
+                  v3_muls( launch_v_bounds[j], t, bound[j] );
+                  bound[j][1] += -0.5f*gravity*t*t;
+                  v3_add( launch_co, bound[j], bound[j] );
+               }
+
+               float limh = vg_minf( 2.0f, t ),
+                     minh = vg_minf( bound[0][1], bound[1][1] )-limh,
+                     maxh = vg_maxf( bound[0][1], bound[1][1] )+limh;
+
+               if( (closest[1] < minh) || (closest[1] > maxh) ){
+                  search_for_grind = 0;
+               }
+            }
+            else
+               search_for_grind = 0;
+         }
+
+         if( search_for_grind ){
+            if( skate_grind_scansq( closest, v, 0.5f, &grind ) ){
+               /* check alignment */
+               v2f v0 = { v[0], v[2] },
+                   v1 = { grind.dir[0], grind.dir[2] };
+
+               v2_normalize( v0 );
+               v2_normalize( v1 );
+
+               float a = v2_dot( v0, v1 );
+
+               float a_min = cosf( VG_PIf * 0.185f );
+               if( state->grind_cooldown )
+                  a_min = cosf( VG_PIf * 0.05f );
+
+               /* check speed */
+               if( (fabsf(v3_dot( v, grind.dir ))>=k_grind_axel_min_vel) &&
+                   (a >= a_min) && 
+                   (fabsf(grind.dir[1]) < 0.70710678118654752f))
+               {
+                  grind_located = 1;
+                  grind_located_gravity = inf->gravity;
+               }
+            }
+         }
+
+         if( trace_world->rendering_gate ){
+            ent_gate *gate = trace_world->rendering_gate;
+            if( gate_intersect( gate, co1, co0 ) ){
+               m4x3_mulv( gate->transport, co0, co0 );
+               m4x3_mulv( gate->transport, co1, co1 );
+               m3x3_mulv( gate->transport, launch_v, launch_v);
+               m4x3_mulv( gate->transport, launch_co, launch_co );
+
+               if( gate->flags & k_ent_gate_nonlocal )
+                  trace_world = &world_static.instances[ gate->target ];
+            }
+         }
+
+         float t1;
+         v3f n;
+
+         float scan_radius = k_board_radius;
+               scan_radius *= vg_clampf( t, 0.02f, 1.0f );
+
+         int idx = spherecast_world( trace_world, co0, co1, scan_radius, &t1, n,
+                                     k_material_flag_walking );
+         if( idx != -1 ){
+            v3f co;
+            v3_lerp( co0, co1, t1, co );
+            v3_copy( co, inf->log[ inf->log_length ++ ] ); 
+
+            v3_copy( n, inf->n );
+            u32 *tri = &trace_world->scene_geo.arrindices[ idx*3 ];
+            struct world_surface *surf = 
+               world_tri_index_surface( trace_world, tri[0] );
+
+            inf->type = k_prediction_land;
+            inf->score = -v3_dot( v, inf->n );
+            inf->land_dist = t + k_trace_delta * t1;
+
+            /* Bias prediction towords ramps */
+            if( !(surf->info.flags & k_material_flag_skate_target) )
+               inf->score *= 10.0f;
+
+            if( surf->info.flags & k_material_flag_boundary )
+               player_skate.possible_jump_count --;
+
+            break;
+         }
+         
+         if( i % 3 == 0 )
+            v3_copy( co1, inf->log[ inf->log_length ++ ] ); 
+         v3_copy( co1, co0 );
+      }
+
+      if( inf->type == k_prediction_unset )
+         player_skate.possible_jump_count --;
+   }
+
+   if( grind_located ){
+      jump_info grind_jumps[2];
+      
+      int valid_count = 
+         create_jumps_to_hit_target( grind_jumps, grind.co, 
+                                     0.175f*VG_PIf, grind_located_gravity );
+
+      /* knock out original landing points in the 1m area */
+      for( u32 j=0; j<player_skate.possible_jump_count; j++ ){
+         jump_info *jump = &player_skate.possible_jumps[ j ];
+         float dist = v3_dist2( jump->log[jump->log_length-1], grind.co );
+         float descale = 1.0f-vg_minf(1.0f,dist);
+         jump->score += descale*3.0f;
+      }
+
+      for( int i=0; i<valid_count; i++ ){
+         jump_info *jump = &grind_jumps[i];
+         jump->type = k_prediction_grind;
+
+         v3f launch_v, launch_co, co0, co1;
+
+         v3_copy( jump->v, launch_v );
+         v3_copy( localplayer.rb.co, launch_co );
+         
+         float t = 0.05f * jump->land_dist;
+         v3_muls( launch_v, t, co0 );
+         co0[1] += -0.5f * jump->gravity * t*t;
+         v3_add( launch_co, co0, co0 );
+
+         /* rough scan to make sure we dont collide with anything */
+         for( int j=1; j<=16; j++ ){
+            t  = (float)j*(1.0f/16.0f);
+            t *= 0.9f;
+            t += 0.05f;
+            t *= jump->land_dist;
+
+            v3_muls( launch_v, t, co1 );
+            co1[1] += -0.5f * jump->gravity * t*t;
+            v3_add( launch_co, co1, co1 );
+            
+            float t1;
+            v3f n;
+
+            int idx = spherecast_world( world0, co0,co1,
+                                        k_board_radius*0.1f, &t1, n, 
+                                        k_material_flag_walking );
+            if( idx != -1 ){
+               goto invalidated_grind;
+            }
+
+            v3_copy( co1, co0 );
+         }
+
+         v3_copy( grind.n, jump->n );
+
+         /* determine score */
+         v3f ve;
+         v3_copy( jump->v, ve );
+         ve[1] += -jump->gravity*jump->land_dist;
+         jump->score = -v3_dot( ve, grind.n ) * 0.9f;
+
+         player_skate.possible_jumps[ player_skate.possible_jump_count ++ ] = 
+            *jump;
+
+         continue;
+invalidated_grind:;
+      }
+   }
+
+
+   float score_min =  INFINITY,
+         score_max = -INFINITY;
+
+   jump_info *best = NULL;
+
+   for( int i=0; i<player_skate.possible_jump_count; i ++ ){
+      jump_info *jump = &player_skate.possible_jumps[i];
+
+      if( jump->score < score_min )
+         best = jump;
+
+      score_min = vg_minf( score_min, jump->score );
+      score_max = vg_maxf( score_max, jump->score );
+   }
+
+   for( int i=0; i<player_skate.possible_jump_count; i ++ ){
+      jump_info *jump = &player_skate.possible_jumps[i];
+      float s = jump->score;
+
+      s -= score_min;
+      s /= (score_max-score_min);
+      s  = 1.0f - s;
+
+      jump->score = s;
+      jump->colour = s * 255.0f;
+
+      if( jump == best )
+         jump->colour <<= 16;
+      else if( jump->type == k_prediction_land )
+         jump->colour <<= 8;
+      
+      jump->colour |= 0xff000000;
+   }
+
+   if( best ){
+      v3_copy( best->n, state->land_normal );
+      v3_copy( best->v, localplayer.rb.v );
+      state->land_dist = best->land_dist;
+      state->gravity_bias = best->gravity;
+
+      if( best->type == k_prediction_grind ){
+         state->activity = k_skate_activity_air_to_grind;
+      }
+
+      v2f steer;
+      joystick_state( k_srjoystick_steer, steer );
+      v2_normalize_clamp( steer );
+
+      if( (fabsf(steer[1]) > 0.5f) && (state->land_dist >= 1.5f) ){
+         state->flip_rate = (1.0f/state->land_dist) * vg_signf(steer[1]) *
+                                 state->reverse ;
+         state->flip_time = 0.0f;
+         v3_copy( localplayer.rb.to_world[0], state->flip_axis );
+      }
+      else{
+         state->flip_rate = 0.0f;
+         v3_zero( state->flip_axis );
+      }
+   }
+   else
+      v3_copy( (v3f){0,1,0}, state->land_normal );
+}
+
+/*
+ *
+ * Varius physics models
+ * ------------------------------------------------
+ */
+
+/*
+ * Air control, no real physics
+ */
+static void skate_apply_air_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( state->activity_prev > k_skate_activity_air_to_grind )
+      player__approximate_best_trajectory();
+
+   float angle = v3_dot( localplayer.rb.to_world[1], state->land_normal );
+   angle = vg_clampf( angle, -1.0f, 1.0f );
+   v3f axis; 
+   v3_cross( localplayer.rb.to_world[1], state->land_normal, axis );
+
+   v4f correction;
+   q_axis_angle( correction, axis, 
+                  acosf(angle)*2.0f*VG_TIMESTEP_FIXED );
+   q_mul( correction, localplayer.rb.q, localplayer.rb.q );
+}
+
+static enum trick_type player_skate_trick_input(void);
+static void skate_apply_trick_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   v3f Fd, Fs, F;
+   v3f strength = { 3.7f, 3.6f, 8.0f };
+
+   v3_muls( state->trick_residualv, -4.0f , Fd );
+   v3_muls( state->trick_residuald, -10.0f, Fs );
+   v3_add( Fd, Fs, F );
+   v3_mul( strength, F, F );
+
+   v3_muladds( state->trick_residualv, F, vg.time_fixed_delta, 
+               state->trick_residualv );
+   v3_muladds( state->trick_residuald, state->trick_residualv,
+               vg.time_fixed_delta, state->trick_residuald );
+
+   if( state->activity <= k_skate_activity_air_to_grind ){
+      if( v3_length2( state->trick_vel ) < 0.0001f )
+         return;
+
+      int carry_on = state->trick_type == player_skate_trick_input();
+
+      /* we assume velocities share a common divisor, in which case the 
+       * interval is the minimum value (if not zero) */
+
+      float min_rate = 99999.0f;
+
+      for( int i=0; i<3; i++ ){
+         float v = state->trick_vel[i];
+         if( (v > 0.0f) && (v < min_rate) )
+            min_rate = v;
+      }
+
+      float interval = 1.0f / min_rate,
+            current  = floorf( state->trick_time ),
+            next_end = current+1.0f;
+
+
+      /* integrate trick velocities */
+      v3_muladds( state->trick_euler, state->trick_vel, vg.time_fixed_delta,
+                  state->trick_euler );
+
+      if( !carry_on && (state->trick_time + vg.time_fixed_delta/interval >= next_end) ){
+         state->trick_time = 0.0f;
+         state->trick_euler[0] = roundf( state->trick_euler[0] );
+         state->trick_euler[1] = roundf( state->trick_euler[1] );
+         state->trick_euler[2] = roundf( state->trick_euler[2] );
+         v3_copy( state->trick_vel, state->trick_residualv );
+         v3_zero( state->trick_vel );
+
+         audio_lock();
+         audio_oneshot_3d( &audio_flips[vg_randu32(&vg.rand)%4], 
+               localplayer.rb.co, 40.0f, 1.0f );
+         audio_unlock();
+      }
+      else 
+         state->trick_time += vg.time_fixed_delta / interval;
+   }
+   else{
+      if( (v3_length2(state->trick_vel) >= 0.0001f ) &&
+          state->trick_time > 0.2f)
+      {
+         vg_info( "player fell off due to lack of skill\n" );
+         player__dead_transition( k_player_die_type_feet );
+      }
+
+      state->trick_euler[0] = roundf( state->trick_euler[0] );
+      state->trick_euler[1] = roundf( state->trick_euler[1] );
+      state->trick_euler[2] = roundf( state->trick_euler[2] );
+      state->trick_time = 0.0f;
+      v3_zero( state->trick_vel );
+   }
+}
+
+static void skate_apply_grab_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   float grabt = axis_state( k_sraxis_grab );
+
+   if( grabt > 0.5f ){
+      v2_muladds( state->grab_mouse_delta, vg.mouse_delta, 0.02f, 
+                  state->grab_mouse_delta );
+
+      v2_normalize_clamp( state->grab_mouse_delta );
+   }
+   else
+      v2_zero( state->grab_mouse_delta );
+
+   state->grabbing = vg_lerpf( state->grabbing, grabt, 8.4f*vg.time_fixed_delta );
+}
+
+static void skate_apply_steering_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   v2f jsteer;
+   joystick_state( k_srjoystick_steer, jsteer );
+
+   /* Steering */
+   float steer = jsteer[0],
+         grab  = axis_state( k_sraxis_grab );
+
+   steer = vg_signf( steer ) * steer*steer * k_steer_ground;
+
+   v3f steer_axis;
+   v3_muls( localplayer.rb.to_world[1], -vg_signf( steer ), steer_axis );
+
+   float rate = 26.0f,
+         top  = 1.0f;
+
+   f32 skid_target = 0.0f;
+
+   if( state->activity <= k_skate_activity_air_to_grind ){
+      rate = 6.0f * fabsf(steer);
+      top  = 1.5f;
+   }
+   else{
+      /* rotate slower when grabbing on ground */
+      steer *= (1.0f-(state->jump_charge+grab)*0.4f);
+
+      if( state->activity == k_skate_activity_grind_5050 ){
+         rate = 0.0f;
+         top  = 0.0f;
+      }
+
+      else if( state->activity >= k_skate_activity_grind_any ){
+         rate *= fabsf(steer);
+
+         float a = 0.8f * -steer * vg.time_fixed_delta;
+
+         v4f q;
+         q_axis_angle( q, localplayer.rb.to_world[1], a );
+         q_mulv( q, player_skate.grind_vec, player_skate.grind_vec );
+
+         v3_normalize( player_skate.grind_vec );
+      }
+
+      else if( state->manual_direction ){
+         rate = 35.0f;
+         top  = 1.5f;
+      }
+      else {
+         f32 skid = axis_state(k_sraxis_skid);
+
+         /* skids on keyboard lock to the first direction pressed */
+         if( vg_input.display_input_method == k_input_method_kbm ){
+            if( button_press(k_srbind_skid) && (fabsf(state->skid)<0.01f) &&
+                (fabsf(steer) > 0.4f) ){
+               state->skid = vg_signf( steer ) * 0.02f;
+            }
+
+            if( button_press(k_srbind_skid) && (fabsf(state->skid)>0.01f) ){
+               skid_target = vg_signf( state->skid );
+            }
+         }
+         else {
+            if( fabsf(skid) > 0.1f ){
+               skid_target = skid;
+            }
+         }
+      }
+
+      if( grab < 0.5f ){
+         top *= 1.0f+v3_length( state->throw_v )*k_mmthrow_steer;
+      }
+   }
+
+   vg_slewf( &state->skid, skid_target, vg.time_fixed_delta*(1.0f/0.1f) );
+   steer = vg_lerpf( steer, state->skid*k_steer_ground*0.5f, 
+                     fabsf(state->skid*0.8f) );
+
+   float current  = v3_dot( localplayer.rb.to_world[1], localplayer.rb.w ),
+         addspeed = (steer * -top) - current,
+         maxaccel = rate * vg.time_fixed_delta,
+         accel    = vg_clampf( addspeed, -maxaccel, maxaccel );
+
+   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[1], 
+               accel, localplayer.rb.w );
+}
+
+/*
+ * Computes friction and surface interface model
+ */
+static void skate_apply_friction_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   /*
+    * Computing localized friction forces for controlling the character
+    * Friction across X is significantly more than Z
+    */
+
+   v3f vel;
+   m3x3_mulv( localplayer.rb.to_local, localplayer.rb.v, vel );
+   float slip = 0.0f;
+   
+   if( fabsf(vel[2]) > 0.01f )
+      slip = fabsf(-vel[0] / vel[2]) * vg_signf(vel[0]);
+
+   if( fabsf( slip ) > 1.2f )
+      slip = vg_signf( slip ) * 1.2f;
+
+   state->slip = slip;
+   state->reverse = -vg_signf(vel[2]);
+
+   f32 lat = k_friction_lat;
+
+   if( fabsf(axis_state(k_sraxis_skid)) > 0.1f ){
+      if( (player_skate.surface == k_surface_prop_snow) ||
+          (player_skate.surface == k_surface_prop_sand) ){
+         lat *= 8.0f;
+      }
+      else
+         lat *= 1.5f;
+   }
+
+   if( player_skate.surface == k_surface_prop_snow )
+      lat *= 0.5f;
+   else if( player_skate.surface == k_surface_prop_sand )
+      lat *= 0.6f;
+
+   vel[0] += vg_cfrictf( vel[0], lat * vg.time_fixed_delta );
+   vel[2] += vg_cfrictf( vel[2], k_friction_resistance * vg.time_fixed_delta );
+
+   /* Pushing additive force */
+
+   if( !button_press( k_srbind_jump ) && (fabsf(state->skid)<0.1f) ){
+      if( button_press( k_srbind_push ) || (vg.time-state->start_push<0.75) ){
+         if( (vg.time - state->cur_push) > 0.25 )
+            state->start_push = vg.time;
+
+         state->cur_push = vg.time;
+
+         double push_time = vg.time - state->start_push;
+
+         float cycle_time = push_time*k_push_cycle_rate,
+               accel      = k_push_accel * (sinf(cycle_time)*0.5f+0.5f),
+               amt        = accel * VG_TIMESTEP_FIXED,
+               current    = v3_length( vel ),
+               new_vel    = vg_minf( current + amt, k_max_push_speed ),
+               delta      = new_vel - vg_minf( current, k_max_push_speed );
+
+         vel[2] += delta * -state->reverse;
+      }
+   }
+
+   /* Send back to velocity */
+   m3x3_mulv( localplayer.rb.to_world, vel, localplayer.rb.v );
+}
+
+static void skate_apply_jump_model(void){
+   struct player_skate_state *state = &player_skate.state;
+   int charging_jump_prev = state->charging_jump;
+   state->charging_jump = button_press( k_srbind_jump );
+
+   /* Cannot charge this in air */
+   if( state->activity <= k_skate_activity_air_to_grind ){
+      state->charging_jump = 0;
+      return;
+   }
+
+   if( state->charging_jump ){
+      state->jump_charge += vg.time_fixed_delta * k_jump_charge_speed;
+
+      if( !charging_jump_prev )
+         state->jump_dir = state->reverse>0.0f? 1: 0;
+   }
+   else{
+      state->jump_charge -= k_jump_charge_speed * vg.time_fixed_delta;
+   }
+
+   state->jump_charge = vg_clampf( state->jump_charge, 0.0f, 1.0f );
+
+   /* player let go after charging past 0.2: trigger jump */
+   if( (!state->charging_jump) && (state->jump_charge > 0.2f) ){
+      v3f jumpdir;
+      
+      /* Launch more up if alignment is up else improve velocity */
+      float aup = localplayer.rb.to_world[1][1],
+            mod = 0.5f,
+            dir = mod + fabsf(aup)*(1.0f-mod);
+
+      if( state->activity == k_skate_activity_ground ){
+         v3_copy( localplayer.rb.v, jumpdir );
+         v3_normalize( jumpdir );
+         v3_muls( jumpdir, 1.0f-dir, jumpdir );
+         v3_muladds( jumpdir, localplayer.rb.to_world[1], dir, jumpdir );
+         v3_normalize( jumpdir );
+      }else{
+         v3_copy( state->up_dir, jumpdir );
+         state->grind_cooldown = 30;
+         state->activity = k_skate_activity_ground;
+
+         v2f steer;
+         joystick_state( k_srjoystick_steer, steer );
+
+         float tilt  = steer[0] * 0.3f;
+               tilt *= vg_signf(v3_dot( localplayer.rb.v, 
+                                        player_skate.grind_dir ));
+
+         v4f qtilt;
+         q_axis_angle( qtilt, player_skate.grind_dir, tilt );
+         q_mulv( qtilt, jumpdir, jumpdir );
+      }
+      state->surface_cooldown = 10;
+      state->trick_input_collect = 0.0f;
+      
+      float force = k_jump_force*state->jump_charge;
+      v3_muladds( localplayer.rb.v, jumpdir, force, localplayer.rb.v );
+      state->jump_charge = 0.0f;
+      state->jump_time = vg.time;
+      player__networked_sfx( k_player_subsystem_skate, 32, 
+                             k_player_skate_soundeffect_jump,
+                             localplayer.rb.co, 1.0f );
+   }
+}
+
+static void skate_apply_handplant_model(void){
+   struct player_skate_state *state = &player_skate.state;
+   if( localplayer.rb.to_world[1][1] < -0.1f ) return;
+   if( localplayer.rb.to_world[1][1] > 0.6f ) return;
+   if( !( button_press(k_srbind_skid) || (fabsf(state->skid)>0.1f)) ) return;
+
+   v3f lco = { 0.0f, -0.2f, -state->reverse },
+       co, dir;
+   m4x3_mulv( localplayer.rb.to_world, lco, co );
+   v3_muls( localplayer.rb.to_world[2], state->reverse, dir );
+   vg_line_arrow( co, dir, 0.13f, 0xff000000 );
+
+   ray_hit hit = { .dist = 2.0f };
+   if( ray_world( world_current_instance(), co, dir, 
+                  &hit, k_material_flag_ghosts )) {
+      vg_line( co, hit.pos, 0xff000000 );
+      vg_line_point( hit.pos, 0.1f, 0xff000000 );
+
+      if( hit.normal[1] < 0.7f ) return;
+      if( hit.dist < 0.95f ) return;
+
+      state->activity = k_skate_activity_handplant;
+      state->handplant_t = 0.0f;
+      v3_copy( localplayer.rb.co, state->store_co );
+      v3_copy( localplayer.rb.v, state->air_init_v );
+      v4_copy( localplayer.rb.q, state->store_q );
+      v3_copy( state->cog, state->store_cog );
+      v3_copy( state->cog_v, state->store_cog_v );
+      v4_copy( state->smoothed_rotation, state->store_smoothed );
+   }
+}
+
+static void skate_apply_pump_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( state->activity != k_skate_activity_ground ){
+      v3_zero( state->throw_v );
+      return;
+   }
+
+   /* Throw / collect routine 
+    */
+   if( axis_state( k_sraxis_grab ) > 0.5f ){
+      if( state->activity == k_skate_activity_ground ){
+         /* Throw */
+         v3_muls( localplayer.rb.to_world[1], k_mmthrow_scale, state->throw_v );
+      }
+   }
+   else{
+      /* Collect */
+      f32 doty = v3_dot( localplayer.rb.to_world[1], state->throw_v );
+      
+      v3f Fl, Fv;
+      v3_muladds( state->throw_v, localplayer.rb.to_world[1], -doty, Fl);
+      player_skate.collect_feedback = v3_length(Fl) * 4.0f;
+
+      if( state->activity == k_skate_activity_ground ){
+         if( v3_length2(localplayer.rb.v)<(20.0f*20.0f) ){
+            v3_muladds( localplayer.rb.v, Fl, 
+                        k_mmcollect_lat, localplayer.rb.v );
+         }
+         v3_muladds( state->throw_v, Fl, -k_mmcollect_lat, state->throw_v );
+      }
+
+      v3_muls( localplayer.rb.to_world[1], -doty, Fv );
+      v3_muladds( localplayer.rb.v, Fv, k_mmcollect_vert, localplayer.rb.v );
+      v3_muladds( state->throw_v, Fv, k_mmcollect_vert, state->throw_v );
+   }
+
+   /* Decay */
+   if( v3_length2( state->throw_v ) > 0.0001f ){
+      v3f dir;
+      v3_copy( state->throw_v, dir );
+      v3_normalize( dir );
+
+      float max = v3_dot( dir, state->throw_v ),
+            amt = vg_minf( k_mmdecay * vg.time_fixed_delta, max );
+      v3_muladds( state->throw_v, dir, -amt, state->throw_v );
+   }
+}
+
+static void skate_apply_cog_model(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   v3f ideal_cog, ideal_diff, ideal_dir;
+   v3_copy( state->up_dir, ideal_dir );
+   v3_normalize( ideal_dir );
+
+   float grab = axis_state( k_sraxis_grab );
+   v3_muladds( localplayer.rb.co, ideal_dir, 1.0f-grab, ideal_cog );
+   v3_sub( ideal_cog, state->cog, ideal_diff );
+
+   /* Apply velocities */
+   v3f rv;
+   v3_sub( localplayer.rb.v, state->cog_v, rv );
+
+   v3f F;
+   v3_muls( ideal_diff, -k_cog_spring * 60.0f, F );
+   v3_muladds( F, rv,   -k_cog_damp * 60.0f, F );
+
+   float ra = k_cog_mass_ratio,
+         rb = 1.0f-k_cog_mass_ratio;
+
+   /* Apply forces & intergrate */
+   v3_muladds( state->cog_v, F, -rb, state->cog_v );
+   state->cog_v[1] += -9.8f * vg.time_fixed_delta;
+   v3_muladds( state->cog, state->cog_v, vg.time_fixed_delta, state->cog );
+}
+
+static void skate_integrate(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   float rate_x = 1.0f - (vg.time_fixed_delta * 3.0f),
+         rate_z = rate_x,
+         rate_y = 1.0f;
+
+   if( state->activity >= k_skate_activity_grind_any ){
+      rate_x = 1.0f-(16.0f*vg.time_fixed_delta);
+      rate_y = 1.0f-(10.0f*vg.time_fixed_delta);
+      rate_z = 1.0f-(40.0f*vg.time_fixed_delta);
+   }
+
+   float wx = v3_dot( localplayer.rb.w, localplayer.rb.to_world[0] ) * rate_x,
+         wy = v3_dot( localplayer.rb.w, localplayer.rb.to_world[1] ) * rate_y,
+         wz = v3_dot( localplayer.rb.w, localplayer.rb.to_world[2] ) * rate_z;
+
+   v3_muls(                  localplayer.rb.to_world[0], wx, localplayer.rb.w );
+   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[1], wy, 
+               localplayer.rb.w );
+   v3_muladds( localplayer.rb.w, localplayer.rb.to_world[2], wz, 
+               localplayer.rb.w );
+
+   state->flip_time += state->flip_rate * vg.time_fixed_delta;
+   rb_update_matrices( &localplayer.rb );
+}
+
+static enum trick_type player_skate_trick_input(void){
+   return (button_press( k_srbind_trick0 )     ) |
+          (button_press( k_srbind_trick1 ) << 1) |
+          (button_press( k_srbind_trick2 ) << 1) |
+          (button_press( k_srbind_trick2 )     );
+}
+
+void player__skate_pre_update(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( state->activity == k_skate_activity_handplant ){
+      state->handplant_t += vg.time_delta;
+      mdl_keyframe hpose[32];
+
+      struct skeleton_anim *anim = player_skate.anim_handplant;
+
+      int end = !skeleton_sample_anim_clamped( 
+               &localplayer.skeleton, anim,
+               state->handplant_t, hpose );
+
+      if( state->reverse < 0.0f )
+         player_mirror_pose( hpose, hpose );
+
+      mdl_keyframe *kf_world = &hpose[ localplayer.id_world -1 ];
+      m4x3f world, mmdl, world_view;
+      q_m3x3( kf_world->q, world );
+      v3_copy( kf_world->co, world[3] );
+
+      /* original mtx */
+      q_m3x3( state->store_q, mmdl );
+      v3_copy( state->store_co, mmdl[3] );
+      m4x3_mul( mmdl, world, world_view );
+
+      vg_line_arrow( world_view[3], world_view[0], 1.0f, 0xff0000ff );
+      vg_line_arrow( world_view[3], world_view[1], 1.0f, 0xff00ff00 );
+      vg_line_arrow( world_view[3], world_view[2], 1.0f, 0xffff0000 );
+
+      m4x3f invworld;
+      m4x3_invert_affine( world, invworld );
+      m4x3_mul( mmdl, invworld, world_view );
+
+      v3_copy( world_view[3], localplayer.rb.co );
+      m3x3_q( world_view, localplayer.rb.q );
+
+      /* new * old^-1 = transfer function */
+      m4x3f transfer;
+      m4x3_invert_affine( mmdl, transfer );
+      m4x3_mul( world_view, transfer, transfer );
+
+      m3x3_mulv( transfer, state->air_init_v, localplayer.rb.v );
+      m3x3_mulv( transfer, state->store_cog_v, state->cog_v );
+
+      m4x3_mulv( transfer, state->store_cog, state->cog );
+      v3_muladds( state->cog, localplayer.rb.to_world[1], 
+                  -state->handplant_t*0.5f, state->cog );
+
+      v4f qtransfer;
+      m3x3_q( transfer, qtransfer );
+      q_mul( qtransfer, state->store_smoothed, state->smoothed_rotation );
+      q_normalize( state->smoothed_rotation );
+      rb_update_matrices( &localplayer.rb );
+
+      if( end ){
+         state->activity = k_skate_activity_air;
+      }
+      else return;
+   }
+
+   if( button_down(k_srbind_use) && (v3_length2(state->trick_vel) < 0.01f) ){
+      localplayer.subsystem = k_player_subsystem_walk;
+
+      if( (state->activity <= k_skate_activity_air_to_grind) &&
+           localplayer.have_glider ){
+         player_glide_transition();
+         return;
+      }
+
+      v3f angles;
+      v3_copy( localplayer.cam.angles, localplayer.angles );
+      localplayer.angles[2] = 0.0f;
+
+      v3f newpos, offset;
+      m4x3_mulv( localplayer.rb.to_world, (v3f){0.0f,1.0f,0.0f}, newpos );
+      v3_add( newpos, (v3f){0.0f,-1.0f,0.0f}, newpos );
+      v3_sub( localplayer.rb.co, newpos, offset );
+      v3_copy( newpos, localplayer.rb.co );
+      v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], -0.1f,
+                  localplayer.rb.co );
+
+      player__begin_holdout( offset );
+      player__walk_transition( state->activity <= k_skate_activity_air_to_grind?
+                               0: 1, state->trick_euler[0] );
+
+      return;
+   }
+
+   enum trick_type trick = player_skate_trick_input();
+   if( trick )
+      state->trick_input_collect += vg.time_frame_delta;
+   else 
+      state->trick_input_collect = 0.0f;
+
+   if( state->activity <= k_skate_activity_air_to_grind ){
+      if( trick && (state->trick_input_collect < 0.1f) ){
+         if( state->trick_time == 0.0f ){
+            audio_lock();
+            audio_oneshot_3d( &audio_flips[vg_randu32(&vg.rand)%4], 
+                  localplayer.rb.co, 40.0f, 1.0f );
+            audio_unlock();
+         }
+
+         if( state->trick_time < 0.1f ){
+            v3_zero( state->trick_vel );
+
+            if( trick == k_trick_type_kickflip ){
+               state->trick_vel[0] = 3.0f;
+            }
+            else if( trick == k_trick_type_shuvit ){
+               state->trick_vel[2] = 3.0f;
+            }
+            else if( trick == k_trick_type_treflip ){
+               state->trick_vel[0] = 2.0f;
+               state->trick_vel[2] = 2.0f;
+            }
+            state->trick_type = trick;
+         }
+      }
+   }
+   else
+      state->trick_type = k_trick_type_none;
+}
+
+void player__skate_comp_audio( void *_animator ){
+   struct player_skate_animator *animator = _animator;
+   audio_lock();
+
+   f32 air   = ((animator->activity <= k_skate_activity_air_to_grind) ||
+                (animator->activity == k_skate_activity_handplant))? 1.0f: 0.0f,
+       speed = v3_length( animator->root_v ),
+       attn  = vg_minf( 1.0f, speed*0.1f ),
+       slide = animator->slide;
+
+   if( animator->activity >= k_skate_activity_grind_any )
+      slide = 0.0f;
+
+   f32 gate = skaterift.time_rate;
+
+   if( skaterift.activity == k_skaterift_replay ){
+      gate = vg_minf( 1.0f, fabsf(player_replay.track_velocity) );
+   }
+
+   f32
+       vol_main    = sqrtf( (1.0f-air)*attn*(1.0f-slide) * 0.4f ) * gate,
+       vol_air     = sqrtf(       air *attn * 0.5f )              * gate,
+       vol_slide   = sqrtf( (1.0f-air)*attn*slide * 0.25f )       * gate;
+
+   const u32 flags = AUDIO_FLAG_SPACIAL_3D|AUDIO_FLAG_LOOP;
+
+   if( !player_skate.aud_air ){
+      player_skate.aud_air = audio_get_first_idle_channel();
+      if( player_skate.aud_air )
+         audio_channel_init( player_skate.aud_air, &audio_board[1], flags );
+   }
+
+   if( !player_skate.aud_slide ){
+      player_skate.aud_slide = audio_get_first_idle_channel();
+      if( player_skate.aud_slide ) 
+         audio_channel_init( player_skate.aud_slide, &audio_board[2], flags );
+   }
+
+
+   /* brrrrrrrrrrrt sound for tiles and stuff 
+    * --------------------------------------------------------*/
+   float sidechain_amt = 0.0f,
+         hz            = vg_maxf( speed * 2.0f, 2.0f );
+
+   if( (animator->surface == k_surface_prop_tiles) &&
+       (animator->activity < k_skate_activity_grind_any) )
+      sidechain_amt = 1.0f;
+   else
+      sidechain_amt = 0.0f;
+
+   audio_set_lfo_frequency( 0, hz );
+   audio_set_lfo_wave( 0, k_lfo_polynomial_bipolar, 
+                          vg_lerpf( 250.0f, 80.0f, attn ) );
+
+   if( player_skate.sample_change_cooldown > 0.0f ){
+      player_skate.sample_change_cooldown -= vg.time_frame_delta;
+   }
+   else{
+      int sample_type = k_skate_sample_concrete;
+
+      if( animator->activity == k_skate_activity_grind_5050 ){
+         if( animator->surface == k_surface_prop_metal )
+            sample_type = k_skate_sample_metal_scrape_generic;
+         else
+            sample_type = k_skate_sample_concrete_scrape_metal;
+      }
+      else if( (animator->activity == k_skate_activity_grind_back50) ||
+               (animator->activity == k_skate_activity_grind_front50) )
+      {
+         if( animator->surface == k_surface_prop_metal ){
+            sample_type = k_skate_sample_metal_scrape_generic;
+         }
+         else{
+#if 0
+            float a = v3_dot( localplayer.rb.to_world[2], 
+                              player_skate.grind_dir );
+            if( fabsf(a) > 0.70710678118654752f )
+               sample_type = k_skate_sample_concrete_scrape_wood;
+            else 
+               sample_type = k_skate_sample_concrete_scrape_metal;
+#endif
+
+            sample_type = k_skate_sample_concrete_scrape_wood;
+         }
+      }
+      else if( animator->activity == k_skate_activity_grind_boardslide ){
+         if( animator->surface == k_surface_prop_metal )
+            sample_type = k_skate_sample_metal_scrape_generic;
+         else
+            sample_type = k_skate_sample_concrete_scrape_wood;
+      }
+
+      audio_clip *relevant_samples[] = {
+         &audio_board[0],
+         &audio_board[0],
+         &audio_board[7],
+         &audio_board[6],
+         &audio_board[5]
+      };
+
+      if( (player_skate.main_sample_type != sample_type) || 
+          (!player_skate.aud_main) ){
+
+         player_skate.aud_main = 
+            audio_channel_crossfade( player_skate.aud_main, 
+                                     relevant_samples[sample_type],
+                                     0.06f, flags );
+         player_skate.sample_change_cooldown = 0.1f;
+         player_skate.main_sample_type = sample_type;
+      }
+   }
+
+   if( player_skate.aud_main ){
+      player_skate.aud_main->colour = 0x00103efe;
+      audio_channel_set_spacial( player_skate.aud_main, 
+                                 animator->root_co, 40.0f );
+      //audio_channel_slope_volume( player_skate.aud_main, 0.05f, vol_main );
+      audio_channel_edit_volume( player_skate.aud_main, vol_main, 1 );
+      audio_channel_sidechain_lfo( player_skate.aud_main, 0, sidechain_amt );
+
+      float rate = 1.0f + (attn-0.5f)*0.2f;
+      audio_channel_set_sampling_rate( player_skate.aud_main, rate );
+   }
+
+   if( player_skate.aud_slide ){
+      player_skate.aud_slide->colour = 0x00103efe;
+      audio_channel_set_spacial( player_skate.aud_slide, 
+                                 animator->root_co, 40.0f );
+      //audio_channel_slope_volume( player_skate.aud_slide, 0.05f, vol_slide );
+      audio_channel_edit_volume( player_skate.aud_slide, vol_slide, 1 );
+      audio_channel_sidechain_lfo( player_skate.aud_slide, 0, sidechain_amt );
+   }
+
+   if( player_skate.aud_air ){
+      player_skate.aud_air->colour = 0x00103efe;
+      audio_channel_set_spacial( player_skate.aud_air, 
+                                 animator->root_co, 40.0f );
+      //audio_channel_slope_volume( player_skate.aud_air, 0.05f, vol_air );
+      audio_channel_edit_volume( player_skate.aud_air, vol_air, 1 );
+   }
+
+   audio_unlock();
+}
+
+void player__skate_post_update(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   for( int i=0; i<player_skate.possible_jump_count; i++ ){
+      jump_info *jump = &player_skate.possible_jumps[i];
+
+      if( jump->log_length == 0 ){
+         vg_fatal_error( "assert: jump->log_length == 0\n" );
+      }
+      
+      for( int j=0; j<jump->log_length - 1; j ++ ){
+         float brightness = jump->score*jump->score*jump->score;
+         v3f p1;
+         v3_lerp( jump->log[j], jump->log[j+1], brightness, p1 );
+         vg_line( jump->log[j], p1, jump->colour );
+      }
+
+      vg_line_cross( jump->log[jump->log_length-1], jump->colour, 0.25f );
+
+      v3f p1;
+      v3_add( jump->log[jump->log_length-1], jump->n, p1 );
+      vg_line( jump->log[jump->log_length-1], p1, 0xffffffff );
+
+      vg_line_point( jump->apex, 0.02f, 0xffffffff );
+   }
+}
+
+/*
+ * truck alignment model at ra(local)
+ * returns 1 if valid surface:
+ *             surface_normal will be filled out with an averaged normal vector
+ *             axel_dir will be the direction from left to right wheels
+ *
+ * returns 0 if no good surface found
+ */
+static 
+int skate_compute_surface_alignment( v3f ra, u32 colour,
+                                     v3f surface_normal, v3f axel_dir ){
+   world_instance *world = world_current_instance();
+
+   v3f truck, left, right;
+   m4x3_mulv( localplayer.rb.to_world, ra, truck );
+
+   v3_muladds( truck, localplayer.rb.to_world[0], -k_board_width, left  );
+   v3_muladds( truck, localplayer.rb.to_world[0],  k_board_width, right );
+   vg_line( left, right, colour );
+
+   float k_max_truck_flex = VG_PIf * 0.25f;
+   
+   ray_hit ray_l, ray_r;
+
+   v3f dir;
+   v3_muls( localplayer.rb.to_world[1], -1.0f, dir );
+
+   int res_l = 0, res_r = 0;
+
+   for( int i=0; i<8; i++ ){
+      float t = 1.0f - (float)i * (1.0f/8.0f);
+      v3_muladds( truck, localplayer.rb.to_world[0], -k_board_radius*t, left );
+      v3_muladds( left,  localplayer.rb.to_world[1],  k_board_radius,   left );
+      ray_l.dist = 2.1f * k_board_radius;
+
+      res_l = ray_world( world, left, dir, &ray_l, k_material_flag_walking );
+
+      if( res_l )
+         break;
+   }
+
+   for( int i=0; i<8; i++ ){
+      float t = 1.0f - (float)i * (1.0f/8.0f);
+      v3_muladds( truck, localplayer.rb.to_world[0],  k_board_radius*t, right );
+      v3_muladds( right, localplayer.rb.to_world[1],  k_board_radius,   right );
+      ray_r.dist = 2.1f * k_board_radius;
+
+      res_r = ray_world( world, right, dir, &ray_r, k_material_flag_walking );
+
+      if( res_r )
+         break;
+   }
+
+   v3f v0;
+   v3f midpoint;
+   v3f tangent_average;
+   v3_muladds( truck, localplayer.rb.to_world[1], -k_board_radius, midpoint );
+   v3_zero( tangent_average );
+
+   if( res_l || res_r ){
+      v3f p0, p1, t;
+      v3_copy( midpoint, p0 );
+      v3_copy( midpoint, p1 );
+
+      if( res_l ){
+         v3_copy( ray_l.pos, p0 );
+         v3_cross( ray_l.normal, localplayer.rb.to_world[0], t );
+         v3_add( t, tangent_average, tangent_average );
+      }
+      if( res_r ){
+         v3_copy( ray_r.pos, p1 );
+         v3_cross( ray_r.normal, localplayer.rb.to_world[0], t );
+         v3_add( t, tangent_average, tangent_average );
+      }
+
+      v3_sub( p1, p0, v0 );
+      v3_normalize( v0 );
+   }
+   else{
+      /* fallback: use the closes point to the trucks */
+      v3f closest;
+      int idx = bh_closest_point( world->geo_bh, midpoint, closest, 0.1f );
+
+      if( idx != -1 ){
+         u32 *tri = &world->scene_geo.arrindices[ idx * 3 ];
+         v3f verts[3];
+
+         for( int j=0; j<3; j++ )
+            v3_copy( world->scene_geo.arrvertices[ tri[j] ].co, verts[j] );
+
+         v3f vert0, vert1, n;
+         v3_sub( verts[1], verts[0], vert0 );
+         v3_sub( verts[2], verts[0], vert1 );
+         v3_cross( vert0, vert1, n );
+         v3_normalize( n );
+
+         if( v3_dot( n, localplayer.rb.to_world[1] ) < 0.3f )
+            return 0;
+
+         v3_cross( n, localplayer.rb.to_world[2], v0 );
+         v3_muladds( v0, localplayer.rb.to_world[2],
+                     -v3_dot( localplayer.rb.to_world[2], v0 ), v0 );
+         v3_normalize( v0 );
+
+         v3f t;
+         v3_cross( n, localplayer.rb.to_world[0], t );
+         v3_add( t, tangent_average, tangent_average );
+      }
+      else
+         return 0;
+   }
+
+   v3_muladds( truck, v0,  k_board_width, right );
+   v3_muladds( truck, v0, -k_board_width, left );
+
+   vg_line( left, right, VG__WHITE );
+
+   v3_normalize( tangent_average );
+   v3_cross( v0, tangent_average, surface_normal );
+   v3_copy( v0, axel_dir );
+
+   return 1;
+}
+
+static void skate_weight_distribute(void){
+   struct player_skate_state *state = &player_skate.state;
+   v3_zero( player_skate.weight_distribution );
+
+   int reverse_dir = v3_dot( localplayer.rb.to_world[2], 
+                             localplayer.rb.v ) < 0.0f?1:-1;
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   if( state->manual_direction == 0 ){
+      if( (steer[1] > 0.7f) && (state->activity == k_skate_activity_ground) &&
+          (state->jump_charge <= 0.01f) )
+         state->manual_direction = reverse_dir;
+   }
+   else{
+      if( steer[1] < 0.1f ){
+         state->manual_direction = 0;
+      }
+      else{
+         if( reverse_dir != state->manual_direction ){
+            return;
+         }
+      }
+   }
+
+   if( state->manual_direction ){
+      float amt = vg_minf( steer[1] * 8.0f, 1.0f );
+      player_skate.weight_distribution[2] = k_board_length * amt * 
+                                          (float)state->manual_direction;
+   }
+
+   if( state->manual_direction ){
+      v3f plane_z;
+
+      m3x3_mulv( localplayer.rb.to_world, player_skate.weight_distribution, 
+                 plane_z );
+      v3_negate( plane_z, plane_z );
+
+      v3_muladds( plane_z, player_skate.surface_picture,
+                  -v3_dot( plane_z, player_skate.surface_picture ), plane_z );
+      v3_normalize( plane_z );
+
+      v3_muladds( plane_z, player_skate.surface_picture, 0.3f, plane_z );
+      v3_normalize( plane_z );
+
+      v3f p1;
+      v3_muladds( localplayer.rb.co, plane_z, 1.5f, p1 );
+      vg_line( localplayer.rb.co, p1, VG__GREEN );
+
+      v3f refdir;
+      v3_muls( localplayer.rb.to_world[2], -(float)state->manual_direction,
+               refdir );
+
+      rb_effect_spring_target_vector( &localplayer.rb, refdir, plane_z,
+                                       k_manul_spring, k_manul_dampener,
+                                       player_skate.substep_delta );
+   }
+}
+
+static void skate_adjust_up_direction(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( state->activity == k_skate_activity_ground ){
+      v3f target;
+      v3_copy( player_skate.surface_picture, target );
+
+      target[1] += 2.0f * player_skate.surface_picture[1];
+      v3_normalize( target );
+
+      v3_lerp( state->up_dir, target,
+               8.0f * player_skate.substep_delta, state->up_dir );
+   }
+   else if( state->activity <= k_skate_activity_air_to_grind ){
+      v3_lerp( state->up_dir, localplayer.rb.to_world[1],
+               8.0f * player_skate.substep_delta, state->up_dir );
+   }
+   else{
+      v3f avg;
+      v3_add( localplayer.rb.to_world[1], (v3f){0,1,0}, avg );
+      v3_normalize( avg );
+
+      v3_lerp( state->up_dir, avg,
+               6.0f * player_skate.substep_delta, state->up_dir );
+   }
+}
+
+static int skate_point_visible( v3f origin, v3f target ){
+   v3f dir;
+   v3_sub( target, origin, dir );
+   
+   ray_hit ray;
+   ray.dist = v3_length( dir );
+   v3_muls( dir, 1.0f/ray.dist, dir );
+   ray.dist -= 0.025f;
+
+   if( ray_world( world_current_instance(), origin, dir, &ray, 
+                  k_material_flag_walking ) )
+      return 0;
+
+   return 1;
+}
+
+static void skate_grind_orient( struct grind_info *inf, m3x3f mtx ){
+   v3_copy( inf->dir, mtx[0] );
+   v3_copy( inf->n, mtx[1] );
+   v3_cross( mtx[0], mtx[1], mtx[2] );
+}
+
+static void skate_grind_friction( struct grind_info *inf, float strength ){
+   v3f v2;
+   v3_muladds( localplayer.rb.to_world[2], inf->n, 
+               -v3_dot( localplayer.rb.to_world[2], inf->n ), v2 );
+
+   float a        = 1.0f-fabsf( v3_dot( v2, inf->dir ) ),
+         dir      = vg_signf( v3_dot( localplayer.rb.v, inf->dir ) ),
+         F        = a * -dir * k_grind_max_friction;
+
+   v3_muladds( localplayer.rb.v, inf->dir, F*vg.time_fixed_delta*strength, 
+               localplayer.rb.v );
+}
+
+static void skate_grind_decay( struct grind_info *inf, float strength ){
+   m3x3f mtx, mtx_inv;
+   skate_grind_orient( inf, mtx );
+   m3x3_transpose( mtx, mtx_inv );
+
+   v3f v_grind;
+   m3x3_mulv( mtx_inv, localplayer.rb.v, v_grind );
+
+   float decay = 1.0f - ( vg.time_fixed_delta * k_grind_decayxy * strength );
+   v3_mul( v_grind, (v3f){ 1.0f, decay, decay }, v_grind );
+   m3x3_mulv( mtx, v_grind, localplayer.rb.v );
+}
+
+static void skate_grind_truck_apply( float sign, struct grind_info *inf,
+                                        float strength ){
+   struct player_skate_state *state = &player_skate.state;
+   /* REFACTOR */
+   v3f ra = { 0.0f, -k_board_radius, sign * k_board_length };
+   v3f raw, wsp;
+   m3x3_mulv( localplayer.rb.to_world, ra, raw );
+   v3_add( localplayer.rb.co, raw, wsp );
+
+   v3_copy( ra, player_skate.weight_distribution );
+
+   v3f delta;
+   v3_sub( inf->co, wsp, delta );
+
+   /* spring force */
+   v3_muladds( localplayer.rb.v, delta, k_spring_force*strength*vg.time_fixed_delta, 
+               localplayer.rb.v );
+
+   skate_grind_decay( inf, strength );
+   skate_grind_friction( inf, strength );
+
+   /* yeah yeah yeah yeah */
+   v3f raw_nplane, axis;
+   v3_muladds( raw, inf->n, -v3_dot( inf->n, raw ), raw_nplane );
+   v3_cross( raw_nplane, inf->n, axis );
+   v3_normalize( axis );
+
+   /* orientation */
+   m3x3f mtx;
+   skate_grind_orient( inf, mtx );
+   v3f target_fwd, fwd, up, target_up;
+   m3x3_mulv( mtx, player_skate.grind_vec, target_fwd );
+   v3_copy( raw_nplane, fwd );
+   v3_copy( localplayer.rb.to_world[1], up );
+   v3_copy( inf->n, target_up );
+
+   v3_muladds( target_fwd, inf->n, -v3_dot(inf->n,target_fwd), target_fwd );
+   v3_muladds( fwd, inf->n, -v3_dot(inf->n,fwd), fwd );
+
+   v3_normalize( target_fwd );
+   v3_normalize( fwd );
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   float way = steer[1] * vg_signf( v3_dot( raw_nplane, localplayer.rb.v ) );
+
+   v4f q;
+   q_axis_angle( q, axis, VG_PIf*0.125f * way );
+   q_mulv( q, target_up,  target_up );
+   q_mulv( q, target_fwd, target_fwd );
+
+   rb_effect_spring_target_vector( &localplayer.rb, up, target_up,
+                                    k_grind_spring, 
+                                    k_grind_dampener,
+                                    vg.time_fixed_delta );
+
+   rb_effect_spring_target_vector( &localplayer.rb, fwd, target_fwd,
+                                    k_grind_spring*strength, 
+                                    k_grind_dampener*strength,
+                                    vg.time_fixed_delta );
+
+   vg_line_arrow( localplayer.rb.co, target_up, 1.0f, VG__GREEN );
+   vg_line_arrow( localplayer.rb.co, fwd, 0.8f, VG__RED );
+   vg_line_arrow( localplayer.rb.co, target_fwd, 1.0f, VG__YELOW );
+
+   player_skate.grind_strength = strength;
+
+   /* Fake contact */
+   struct grind_limit *limit = 
+      &player_skate.limits[ player_skate.limit_count ++ ];
+   m4x3_mulv( localplayer.rb.to_local, wsp, limit->ra );
+   m3x3_mulv( localplayer.rb.to_local, inf->n, limit->n );
+   limit->p = 0.0f;
+
+   v3_copy( inf->dir, player_skate.grind_dir );
+}
+
+static void skate_5050_apply( struct grind_info *inf_front,
+                                 struct grind_info *inf_back ){
+   struct player_skate_state *state = &player_skate.state;
+   struct grind_info inf_avg;
+
+   v3_sub( inf_front->co, inf_back->co, inf_avg.dir );
+   v3_muladds( inf_back->co, inf_avg.dir, 0.5f, inf_avg.co );
+   v3_normalize( inf_avg.dir );
+
+   /* dont ask */
+   v3_muls( inf_avg.dir, vg_signf(v3_dot(inf_avg.dir,localplayer.rb.v)), 
+            inf_avg.dir );
+
+   v3f axis_front, axis_back, axis;
+   v3_cross( inf_front->dir, inf_front->n, axis_front );
+   v3_cross( inf_back->dir,  inf_back->n,  axis_back  );
+   v3_add( axis_front, axis_back, axis );
+   v3_normalize( axis );
+
+   v3_cross( axis, inf_avg.dir, inf_avg.n );
+   skate_grind_decay( &inf_avg, 1.0f );
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   float way = steer[1] * vg_signf( v3_dot( localplayer.rb.to_world[2], 
+                                            localplayer.rb.v ) );
+   v4f q;
+   v3f up, target_up;
+   v3_copy( localplayer.rb.to_world[1], up );
+   v3_copy( inf_avg.n, target_up );
+   q_axis_angle( q, localplayer.rb.to_world[0], VG_PIf*0.25f * -way );
+   q_mulv( q, target_up,  target_up );
+
+   v3_zero( player_skate.weight_distribution );
+   player_skate.weight_distribution[2] = k_board_length * -way;
+
+   rb_effect_spring_target_vector( &localplayer.rb, up, target_up,
+                                    k_grind_spring, 
+                                    k_grind_dampener,
+                                    vg.time_fixed_delta );
+   vg_line_arrow( localplayer.rb.co, up, 1.0f, VG__GREEN );
+   vg_line_arrow( localplayer.rb.co, target_up, 1.0f, VG__GREEN );
+
+   v3f fwd_nplane, dir_nplane;
+   v3_muladds( localplayer.rb.to_world[2], inf_avg.n,
+               -v3_dot( localplayer.rb.to_world[2], inf_avg.n ), fwd_nplane );
+
+   v3f dir;
+   v3_muls( inf_avg.dir, v3_dot( fwd_nplane, inf_avg.dir ), dir );
+   v3_muladds( dir, inf_avg.n, -v3_dot( dir, inf_avg.n ), dir_nplane );
+
+   v3_normalize( fwd_nplane );
+   v3_normalize( dir_nplane );
+
+   rb_effect_spring_target_vector( &localplayer.rb, fwd_nplane, dir_nplane,
+                                    1000.0f,
+                                    k_grind_dampener,
+                                    vg.time_fixed_delta );
+   vg_line_arrow( localplayer.rb.co, fwd_nplane, 0.8f, VG__RED );
+   vg_line_arrow( localplayer.rb.co, dir_nplane, 0.8f, VG__RED );
+
+   v3f pos_front = { 0.0f, -k_board_radius, -1.0f * k_board_length },
+       pos_back  = { 0.0f, -k_board_radius,  1.0f * k_board_length },
+       delta_front, delta_back, delta_total;
+
+   m4x3_mulv( localplayer.rb.to_world, pos_front, pos_front );
+   m4x3_mulv( localplayer.rb.to_world, pos_back,  pos_back  );
+
+   v3_sub( inf_front->co, pos_front, delta_front );
+   v3_sub( inf_back->co,  pos_back, delta_back );
+   v3_add( delta_front, delta_back, delta_total );
+
+   v3_muladds( localplayer.rb.v, delta_total, 50.0f * vg.time_fixed_delta, 
+               localplayer.rb.v );
+
+   /* Fake contact */
+   struct grind_limit *limit = 
+      &player_skate.limits[ player_skate.limit_count ++ ];
+   v3_zero( limit->ra );
+   m3x3_mulv( localplayer.rb.to_local, inf_avg.n, limit->n );
+   limit->p = 0.0f;
+
+   v3_copy( inf_avg.dir, player_skate.grind_dir );
+}
+
+static int skate_grind_truck_renew( f32 sign, struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   v3f wheel_co = { 0.0f, 0.0f,            sign * k_board_length },
+       grind_co = { 0.0f, -k_board_radius, sign * k_board_length };
+
+   m4x3_mulv( localplayer.rb.to_world, wheel_co, wheel_co );
+   m4x3_mulv( localplayer.rb.to_world, grind_co, grind_co );
+
+   /* Exit condition: lost grind tracking */
+   if( !skate_grind_scansq( grind_co, localplayer.rb.v, 0.3f, inf ) )
+      return 0;
+
+   /* Exit condition: cant see grind target directly */
+   if( !skate_point_visible( wheel_co, inf->co ) )
+      return 0;
+
+   /* Exit condition: minimum velocity not reached, but allow a bit of error */
+   float dv   = fabsf(v3_dot( localplayer.rb.v, inf->dir )),
+         minv = k_grind_axel_min_vel*0.8f;
+
+   if( dv < minv )
+      return 0;
+
+   if( fabsf(v3_dot( inf->dir, player_skate.grind_dir )) < k_grind_max_edge_angle )
+      return 0;
+
+   v3_copy( inf->dir, player_skate.grind_dir );
+   return 1;
+}
+
+static int skate_grind_truck_entry( f32 sign, struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   /* REFACTOR */
+   v3f ra = { 0.0f, -k_board_radius, sign * k_board_length };
+
+   v3f raw, wsp;
+   m3x3_mulv( localplayer.rb.to_world, ra, raw );
+   v3_add( localplayer.rb.co, raw, wsp );
+
+   if( skate_grind_scansq( wsp, localplayer.rb.v, 0.3, inf ) ){
+      if( fabsf(v3_dot( localplayer.rb.v, inf->dir )) < k_grind_axel_min_vel )
+         return 0;
+
+      /* velocity should be at least 60% aligned */
+      v3f pv, axis;
+      v3_cross( inf->n, inf->dir, axis );
+      v3_muladds( localplayer.rb.v, inf->n, 
+                  -v3_dot( localplayer.rb.v, inf->n ), pv );
+      
+      if( v3_length2( pv ) < 0.0001f )
+         return 0;
+      v3_normalize( pv );
+
+      if( fabsf(v3_dot( pv, inf->dir )) < k_grind_axel_max_angle )
+         return 0;
+
+      if( v3_dot( localplayer.rb.v, inf->n ) > 0.5f )
+         return 0;
+      
+      v3f local_co, local_dir, local_n;
+      m4x3_mulv( localplayer.rb.to_local, inf->co,  local_co );
+      m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
+      m3x3_mulv( localplayer.rb.to_local, inf->n,   local_n );
+
+      v2f delta = { local_co[0], local_co[2] - k_board_length*sign };
+
+      float truck_height = -(k_board_radius+0.03f);
+
+      v3f rv;
+      v3_cross( localplayer.rb.w, raw, rv );
+      v3_add( localplayer.rb.v, rv, rv );
+
+      if( (local_co[1] >= truck_height) &&
+          (v2_length2( delta ) <= k_board_radius*k_board_radius) )
+      {
+         return 1;
+      }
+   }
+
+   return 0;
+}
+
+static void skate_boardslide_apply( struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   v3f local_co, local_dir, local_n;
+   m4x3_mulv( localplayer.rb.to_local, inf->co, local_co );
+   m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
+   m3x3_mulv( localplayer.rb.to_local, inf->n, local_n );
+
+   v3f intersection;
+   v3_muladds( local_co, local_dir, local_co[0]/-local_dir[0], 
+               intersection );
+   v3_copy( intersection, player_skate.weight_distribution );
+
+   skate_grind_decay( inf, 0.0125f );
+   skate_grind_friction( inf, 0.25f );
+
+   /* direction alignment */
+   v3f dir, perp;
+   v3_cross( local_dir, local_n, perp );
+   v3_muls( local_dir, vg_signf(local_dir[0]), dir );
+   v3_muls( perp, vg_signf(perp[2]), perp );
+
+   m3x3_mulv( localplayer.rb.to_world, dir, dir );
+   m3x3_mulv( localplayer.rb.to_world, perp, perp );
+
+   v4f qbalance;
+   q_axis_angle( qbalance, dir, local_co[0]*k_grind_balance );
+   q_mulv( qbalance, perp, perp );
+
+   rb_effect_spring_target_vector( &localplayer.rb, localplayer.rb.to_world[0],
+                                    dir, 
+                                    k_grind_spring, k_grind_dampener,
+                                    vg.time_fixed_delta );
+
+   rb_effect_spring_target_vector( &localplayer.rb, localplayer.rb.to_world[2],
+                                    perp,
+                                    k_grind_spring, k_grind_dampener,
+                                    vg.time_fixed_delta );
+
+   vg_line_arrow( localplayer.rb.co, dir, 0.5f, VG__GREEN );
+   vg_line_arrow( localplayer.rb.co, perp, 0.5f, VG__BLUE );
+
+   v3_copy( inf->dir, player_skate.grind_dir );
+}
+
+static int skate_boardslide_entry( struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( skate_grind_scansq( localplayer.rb.co, 
+                           localplayer.rb.to_world[0], k_board_length,
+                           inf ) )
+   {
+      v3f local_co, local_dir;
+      m4x3_mulv( localplayer.rb.to_local, inf->co, local_co );
+      m3x3_mulv( localplayer.rb.to_local, inf->dir, local_dir );
+
+      if( (fabsf(local_co[2]) <= k_board_length) &&   /* within wood area */
+          (local_co[1] >= 0.0f) &&                    /* at deck level */
+          (fabsf(local_dir[0]) >= 0.25f) )            /* perpendicular to us */
+      {
+         if( fabsf(v3_dot( localplayer.rb.v, inf->dir )) < k_grind_axel_min_vel )
+            return 0;
+
+         return 1;
+      }
+   }
+
+   return 0;
+}
+
+static int skate_boardslide_renew( struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( !skate_grind_scansq( localplayer.rb.co, 
+                            localplayer.rb.to_world[0], k_board_length,
+                            inf ) )
+      return 0;
+
+   /* Exit condition: cant see grind target directly */
+   v3f vis;
+   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 0.2f, vis );
+   if( !skate_point_visible( vis, inf->co ) )
+      return 0;
+
+   /* Exit condition: minimum velocity not reached, but allow a bit of error */
+   float dv   = fabsf(v3_dot( localplayer.rb.v, inf->dir )),
+         minv = k_grind_axel_min_vel*0.8f;
+
+   if( dv < minv )
+      return 0;
+
+   if( fabsf(v3_dot( inf->dir, player_skate.grind_dir )) < k_grind_max_edge_angle )
+      return 0;
+
+   return 1;
+}
+
+static void skate_store_grind_vec( struct grind_info *inf ){
+   struct player_skate_state *state = &player_skate.state;
+
+   m3x3f mtx;
+   skate_grind_orient( inf, mtx );
+   m3x3_transpose( mtx, mtx );
+
+   v3f raw;
+   v3_sub( inf->co, localplayer.rb.co, raw );
+
+   m3x3_mulv( mtx, raw, player_skate.grind_vec );
+   v3_normalize( player_skate.grind_vec );
+   v3_copy( inf->dir, player_skate.grind_dir );
+}
+
+static enum skate_activity skate_availible_grind(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   if( state->grind_cooldown > 100 ){
+      vg_fatal_error( "wth!\n" );
+   }
+
+   /* debounces this state manager a little bit */
+   if( state->grind_cooldown ){
+      state->grind_cooldown --;
+      return k_skate_activity_undefined;
+   }
+
+   struct grind_info inf_back50,
+                     inf_front50,
+                     inf_slide;
+
+   int res_back50  = 0,
+       res_front50 = 0,
+       res_slide   = 0;
+
+   int allow_back  = 1,
+       allow_front = 1;
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   if( state->activity == k_skate_activity_grind_5050 || 
+       state->activity == k_skate_activity_grind_back50 ||
+       state->activity == k_skate_activity_grind_front50 )
+   {
+      float tilt = steer[1];
+
+      if( fabsf(tilt) >= 0.25f ){
+         v3f raw = {0.0f,0.0f,tilt};
+         m3x3_mulv( localplayer.rb.to_world, raw, raw );
+
+         float way = tilt * vg_signf( v3_dot( raw, localplayer.rb.v ) );
+
+         if( way < 0.0f ) allow_front = 0;
+         else allow_back = 0;
+      }
+   }
+
+   if( state->activity == k_skate_activity_grind_boardslide ){
+      res_slide = skate_boardslide_renew( &inf_slide );
+   }
+   else if( state->activity == k_skate_activity_grind_back50 ){
+      res_back50  = skate_grind_truck_renew( 1.0f, &inf_back50 );
+
+      if( allow_front )
+         res_front50 = skate_grind_truck_entry( -1.0f, &inf_front50 );
+   }
+   else if( state->activity == k_skate_activity_grind_front50 ){
+      res_front50 = skate_grind_truck_renew( -1.0f, &inf_front50 );
+
+      if( allow_back )
+         res_back50  = skate_grind_truck_entry(  1.0f, &inf_back50 );
+   }
+   else if( state->activity == k_skate_activity_grind_5050 ){
+      if( allow_front )
+         res_front50 = skate_grind_truck_renew( -1.0f, &inf_front50 );
+      if( allow_back )
+         res_back50  = skate_grind_truck_renew(  1.0f, &inf_back50 );
+   }
+   else{
+      res_slide   = skate_boardslide_entry( &inf_slide );
+
+      if( allow_back )
+         res_back50  = skate_grind_truck_entry(  1.0f, &inf_back50 );
+
+      if( allow_front )
+         res_front50 = skate_grind_truck_entry( -1.0f, &inf_front50 );
+
+      if( res_back50 != res_front50 ){
+         int wants_to_do_that = fabsf(steer[1]) >= 0.25f;
+
+         res_back50  &= wants_to_do_that;
+         res_front50 &= wants_to_do_that;
+      }
+   }
+
+   const enum skate_activity table[] =
+   {                                      /* slide | back | front */
+      k_skate_activity_undefined,         /* 0       0      0     */
+      k_skate_activity_grind_front50,     /* 0       0      1     */
+      k_skate_activity_grind_back50,      /* 0       1      0     */
+      k_skate_activity_grind_5050,        /* 0       1      1     */
+
+      /* slide has priority always */
+      k_skate_activity_grind_boardslide,  /* 1       0      0     */
+      k_skate_activity_grind_boardslide,  /* 1       0      1     */
+      k_skate_activity_grind_boardslide,  /* 1       1      0     */
+      k_skate_activity_grind_boardslide,  /* 1       1      1     */
+   }
+   , new_activity = table[ res_slide << 2 | res_back50 << 1 | res_front50 ];
+
+   if(      new_activity == k_skate_activity_undefined ){
+      if( state->activity >= k_skate_activity_grind_any ){
+         state->grind_cooldown = 15;
+         state->surface_cooldown = 10;
+      }
+   }
+   else if( new_activity == k_skate_activity_grind_boardslide ){
+      skate_boardslide_apply( &inf_slide );
+   }
+   else if( new_activity == k_skate_activity_grind_back50 ){
+      if( state->activity != k_skate_activity_grind_back50 )
+         skate_store_grind_vec( &inf_back50 );
+
+      skate_grind_truck_apply(  1.0f, &inf_back50, 1.0f );
+   }
+   else if( new_activity == k_skate_activity_grind_front50 ){
+      if( state->activity != k_skate_activity_grind_front50 )
+         skate_store_grind_vec( &inf_front50 );
+
+      skate_grind_truck_apply( -1.0f, &inf_front50, 1.0f );
+   }
+   else if( new_activity == k_skate_activity_grind_5050 )
+      skate_5050_apply( &inf_front50, &inf_back50 );
+
+   return new_activity;
+}
+
+void player__skate_update(void){
+   struct player_skate_state *state = &player_skate.state;
+   world_instance *world = world_current_instance();
+
+   if( state->activity == k_skate_activity_handplant )
+      return;
+
+   if( !world_water_player_safe( world, 0.25f ) ) return;
+
+   v3_copy( localplayer.rb.co, state->prev_pos );
+   state->activity_prev = state->activity;
+   v3f normal_total;
+   v3_zero( normal_total );
+
+   struct board_collider
+   {
+      v3f   pos;
+      float radius;
+
+      u32   colour;
+
+      enum  board_collider_state
+      {
+         k_collider_state_default,
+         k_collider_state_disabled,
+         k_collider_state_colliding
+      }
+      state;
+   }
+   wheels[] =
+   {
+      { 
+         { 0.0f, 0.0f,    -k_board_length }, 
+         .radius = k_board_radius,
+         .colour = VG__RED
+      },
+      { 
+         { 0.0f, 0.0f,     k_board_length }, 
+         .radius = k_board_radius,
+         .colour = VG__GREEN
+      }
+   };
+
+   float slap = 0.0f;
+
+   if( state->activity <= k_skate_activity_air_to_grind ){
+      float min_dist = 0.6f;
+      for( int i=0; i<2; i++ ){
+         v3f wpos, closest;
+         m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, wpos );
+
+         if( bh_closest_point( world->geo_bh, wpos, closest, min_dist ) != -1 ){
+            min_dist = vg_minf( min_dist, v3_dist( closest, wpos ) );
+         }
+      }
+      min_dist -= 0.2f;
+      float vy = vg_maxf( 0.0f, localplayer.rb.v[1] );
+      slap = vg_clampf( (min_dist/0.5f) + vy, 0.0f, 1.0f )*0.3f;
+   }
+   state->slap = vg_lerpf( state->slap, slap, 10.0f*vg.time_fixed_delta );
+
+   wheels[0].pos[1] = state->slap;
+   wheels[1].pos[1] = state->slap;
+
+
+   const int k_wheel_count = 2;
+
+   player_skate.substep = vg.time_fixed_delta;
+   player_skate.substep_delta = player_skate.substep;
+   player_skate.limit_count = 0;
+
+   int substep_count = 0;
+
+   v3_zero( player_skate.surface_picture );
+
+   int prev_contacts[2];
+
+   for( int i=0; i<k_wheel_count; i++ ){
+      wheels[i].state = k_collider_state_default;
+      prev_contacts[i] = player_skate.wheel_contacts[i];
+   }
+
+   /* check if we can enter or continue grind */
+   enum skate_activity grindable_activity = skate_availible_grind();
+   if( grindable_activity != k_skate_activity_undefined ){
+      state->activity = grindable_activity;
+      goto grinding;
+   }
+
+   int contact_count = 0;
+   for( int i=0; i<2; i++ ){
+      v3f normal, axel;
+      v3_copy( localplayer.rb.to_world[0], axel );
+
+      if( skate_compute_surface_alignment( wheels[i].pos, 
+                                           wheels[i].colour, normal, axel ) )
+      {
+         rb_effect_spring_target_vector( &localplayer.rb, 
+                                          localplayer.rb.to_world[0],
+                                          axel,
+                                          k_surface_spring, k_surface_dampener,
+                                          player_skate.substep_delta );
+
+         v3_add( normal, player_skate.surface_picture, 
+                 player_skate.surface_picture );
+         contact_count ++;
+         player_skate.wheel_contacts[i] = 1;
+      }
+      else{
+         player_skate.wheel_contacts[i] = 0;
+      }
+
+      m3x3_mulv( localplayer.rb.to_local, axel, player_skate.truckv0[i] );
+   }
+
+   if( state->surface_cooldown ){
+      state->surface_cooldown --;
+      contact_count = 0;
+   }
+
+   if( (prev_contacts[0]+prev_contacts[1] == 1) && (contact_count == 2) ){
+      for( int i=0; i<2; i++ ){
+         if( !prev_contacts[i] ){
+            v3f co;
+            m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, co );
+            player__networked_sfx( k_player_subsystem_skate, 32, 
+                                   k_player_skate_soundeffect_tap,
+                                   localplayer.rb.co, 0.75f );
+         }
+      }
+   }
+
+   if( contact_count ){
+      state->activity = k_skate_activity_ground;
+      state->gravity_bias = k_gravity;
+      v3_normalize( player_skate.surface_picture );
+
+      skate_apply_friction_model();
+      skate_weight_distribute();
+   }
+   else{
+      if( state->activity > k_skate_activity_air_to_grind )
+         state->activity = k_skate_activity_air;
+
+      v3_zero( player_skate.weight_distribution );
+      skate_apply_air_model();
+   }
+
+grinding:;
+
+   if( state->activity == k_skate_activity_grind_back50 )
+      wheels[1].state = k_collider_state_disabled;
+   if( state->activity == k_skate_activity_grind_front50 )
+      wheels[0].state = k_collider_state_disabled;
+   if( state->activity == k_skate_activity_grind_5050 ){
+      wheels[0].state = k_collider_state_disabled;
+      wheels[1].state = k_collider_state_disabled;
+   }
+
+   /* all activities */
+   skate_apply_steering_model();
+   skate_adjust_up_direction();
+   skate_apply_cog_model();
+   skate_apply_jump_model();
+   skate_apply_handplant_model();
+   skate_apply_grab_model();
+   skate_apply_trick_model();
+   skate_apply_pump_model();
+
+   ent_tornado_debug();
+   v3f a;
+   ent_tornado_forces( localplayer.rb.co, localplayer.rb.v, a );
+   v3_muladds( localplayer.rb.v, a, vg.time_fixed_delta, localplayer.rb.v );
+
+begin_collision:;
+
+   /*
+    * Phase 0: Continous collision detection
+    * --------------------------------------------------------------------------
+    */
+
+   v3f head_wp0, head_wp1, start_co;
+   m4x3_mulv( localplayer.rb.to_world, state->head_position, head_wp0 );
+   v3_copy( localplayer.rb.co, start_co );
+
+   /* calculate transform one step into future */
+   v3f future_co;
+   v4f future_q;
+   v3_muladds( localplayer.rb.co, localplayer.rb.v, player_skate.substep, 
+               future_co );
+
+   if( v3_length2( localplayer.rb.w ) > 0.0f ){
+      v4f rotation;
+      v3f axis;
+      v3_copy( localplayer.rb.w, axis );
+      
+      float mag = v3_length( axis );
+      v3_divs( axis, mag, axis );
+      q_axis_angle( rotation, axis, mag*player_skate.substep );
+      q_mul( rotation, localplayer.rb.q, future_q );
+      q_normalize( future_q );
+   }
+   else
+      v4_copy( localplayer.rb.q, future_q );
+
+   v3f future_cg, current_cg, cg_offset;
+   q_mulv( localplayer.rb.q, player_skate.weight_distribution, current_cg );
+   q_mulv( future_q, player_skate.weight_distribution, future_cg );
+   v3_sub( future_cg, current_cg, cg_offset );
+
+   /* calculate the minimum time we can move */
+   float max_time = player_skate.substep;
+
+   for( int i=0; i<k_wheel_count; i++ ){
+      if( wheels[i].state == k_collider_state_disabled )
+         continue;
+
+      v3f current, future, r_cg;
+      
+      q_mulv( future_q, wheels[i].pos, future );
+      v3_add( future, future_co, future );
+      v3_add( cg_offset, future, future );
+
+      q_mulv( localplayer.rb.q, wheels[i].pos, current );
+      v3_add( current, localplayer.rb.co, current );
+      
+      float t;
+      v3f n;
+
+      float cast_radius = wheels[i].radius - k_penetration_slop * 2.0f;
+      if( spherecast_world( world, current, future, cast_radius, &t, n,
+                            k_material_flag_walking ) != -1)
+         max_time = vg_minf( max_time, t * player_skate.substep );
+   }
+
+   /* clamp to a fraction of delta, to prevent locking */
+   float rate_lock = substep_count;
+   rate_lock *= vg.time_fixed_delta * 0.1f;
+   rate_lock *= rate_lock;
+
+   max_time = vg_maxf( max_time, rate_lock );
+   player_skate.substep_delta = max_time;
+
+   /* integrate */
+   v3_muladds( localplayer.rb.co, localplayer.rb.v, 
+               player_skate.substep_delta, localplayer.rb.co );
+   if( v3_length2( localplayer.rb.w ) > 0.0f ){
+      v4f rotation;
+      v3f axis;
+      v3_copy( localplayer.rb.w, axis );
+      
+      float mag = v3_length( axis );
+      v3_divs( axis, mag, axis );
+      q_axis_angle( rotation, axis, mag*player_skate.substep_delta );
+      q_mul( rotation, localplayer.rb.q, localplayer.rb.q );
+      q_normalize( localplayer.rb.q );
+
+      q_mulv( localplayer.rb.q, player_skate.weight_distribution, future_cg );
+      v3_sub( current_cg, future_cg, cg_offset );
+      v3_add( localplayer.rb.co, cg_offset, localplayer.rb.co );
+   }
+
+   rb_update_matrices( &localplayer.rb );
+   localplayer.rb.v[1] += -state->gravity_bias * player_skate.substep_delta;
+
+   player_skate.substep -= player_skate.substep_delta;
+
+   rb_ct manifold[128];
+   int manifold_len = 0;
+   /*
+    * Phase -1: head detection
+    * --------------------------------------------------------------------------
+    */
+   m4x3_mulv( localplayer.rb.to_world, state->head_position, head_wp1 );
+
+   float t;
+   v3f n;
+   if( (v3_dist2( head_wp0, head_wp1 ) > 0.001f) &&
+       (spherecast_world( world, head_wp0, head_wp1, 0.2f, &t, n,
+                          k_material_flag_walking ) != -1) )
+   {
+      v3_lerp( start_co, localplayer.rb.co, t, localplayer.rb.co );
+      rb_update_matrices( &localplayer.rb );
+
+      vg_info( "player fell of due to hitting head\n" );
+      player__dead_transition( k_player_die_type_head );
+      return;
+   }
+
+   /*
+    * Phase 1: Regular collision detection
+    * --------------------------------------------------------------------------
+    */
+
+   for( int i=0; i<k_wheel_count; i++ ){
+      if( wheels[i].state == k_collider_state_disabled )
+         continue;
+
+      m4x3f mtx;
+      m3x3_identity( mtx );
+      m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, mtx[3] );
+
+      rb_ct *man = &manifold[ manifold_len ];
+
+      int l = skate_collide_smooth( mtx, wheels[i].radius, man );
+      if( l )
+         wheels[i].state = k_collider_state_colliding;
+
+      manifold_len += l;
+   }
+
+   float grind_radius = k_board_radius * 0.75f;
+   rb_capsule capsule = { .h = (k_board_length+0.2f)*2.0f, 
+                          .r = grind_radius };
+   m4x3f mtx;
+   v3_muls( localplayer.rb.to_world[0],  1.0f, mtx[0] );
+   v3_muls( localplayer.rb.to_world[2], -1.0f, mtx[1] );
+   v3_muls( localplayer.rb.to_world[1],  1.0f, mtx[2] );
+   v3_muladds( localplayer.rb.to_world[3], localplayer.rb.to_world[1], 
+               grind_radius + k_board_radius*0.25f+state->slap, mtx[3] );
+
+   rb_ct *cman = &manifold[manifold_len];
+
+   int l = rb_capsule__scene( mtx, &capsule, NULL, world->geo_bh,
+                              cman, k_material_flag_walking );
+
+   /* weld joints */
+   for( int i=0; i<l; i ++ )
+      cman[l].type = k_contact_type_edge;
+   rb_manifold_filter_joint_edges( cman, l, 0.03f );
+   l = rb_manifold_apply_filtered( cman, l );
+
+   manifold_len += l;
+   vg_line_capsule( mtx, capsule.r, capsule.h, VG__WHITE );
+
+   /* add limits */
+   if( state->activity >= k_skate_activity_grind_any ){
+      for( int i=0; i<player_skate.limit_count; i++ ){
+         struct grind_limit *limit = &player_skate.limits[i];
+         rb_ct *ct = &manifold[ manifold_len ++ ];
+         m4x3_mulv( localplayer.rb.to_world, limit->ra, ct->co );
+         m3x3_mulv( localplayer.rb.to_world, limit->n, ct->n );
+         ct->p = limit->p;
+         ct->type = k_contact_type_default;
+      }
+   }
+
+   /* 
+    * Phase 3: Dynamics
+    * --------------------------------------------------------------------------
+    */
+
+
+   v3f world_cog;
+   m4x3_mulv( localplayer.rb.to_world, 
+              player_skate.weight_distribution, world_cog );
+   vg_line_point( world_cog, 0.02f, VG__BLACK );
+
+   for( int i=0; i<manifold_len; i ++ ){
+      rb_prepare_contact( &manifold[i], player_skate.substep_delta );
+      rb_debug_contact( &manifold[i] );
+   }
+
+   /* yes, we are currently rebuilding mass matrices every frame. too bad! */
+   v3f extent = { k_board_width*10.0f, 0.1f, k_board_length };
+   float ex2 = k_board_interia*extent[0]*extent[0],
+         ey2 = k_board_interia*extent[1]*extent[1],
+         ez2 = k_board_interia*extent[2]*extent[2];
+
+   float mass = 2.0f * (extent[0]*extent[1]*extent[2]);
+   float inv_mass = 1.0f/mass;
+
+   v3f I;
+   I[0] = ((1.0f/12.0f) * mass * (ey2+ez2));
+   I[1] = ((1.0f/12.0f) * mass * (ex2+ez2));
+   I[2] = ((1.0f/12.0f) * mass * (ex2+ey2));
+
+   m3x3f iI;
+   m3x3_identity( iI );
+   iI[0][0] = I[0];
+   iI[1][1] = I[1];
+   iI[2][2] = I[2];
+   m3x3_inv( iI, iI );
+
+   m3x3f iIw;
+   m3x3_mul( iI, localplayer.rb.to_local, iIw );
+   m3x3_mul( localplayer.rb.to_world, iIw, iIw );
+
+   for( int j=0; j<10; j++ ){
+      for( int i=0; i<manifold_len; i++ ){
+         /* 
+          * regular dance; calculate velocity & total mass, apply impulse.
+          */
+
+         rb_ct *ct = &manifold[i];
+         
+         v3f rv, delta;
+         v3_sub( ct->co, world_cog, delta ); 
+         v3_cross( localplayer.rb.w, delta, rv );
+         v3_add( localplayer.rb.v, rv, rv );
+
+         v3f raCn;
+         v3_cross( delta, ct->n, raCn );
+
+         v3f raCnI, rbCnI;
+         m3x3_mulv( iIw, raCn, raCnI );
+
+         float normal_mass = 1.0f / (inv_mass + v3_dot(raCn,raCnI)),
+               vn = v3_dot( rv, ct->n ),
+               lambda = normal_mass * ( -vn );
+
+         float temp = ct->norm_impulse;
+         ct->norm_impulse = vg_maxf( temp + lambda, 0.0f );
+         lambda = ct->norm_impulse - temp;
+
+         v3f impulse;
+         v3_muls( ct->n, lambda, impulse );
+
+         v3_muladds( normal_total, impulse, inv_mass, normal_total );
+         v3_muladds( localplayer.rb.v, impulse, inv_mass, localplayer.rb.v );
+         v3_cross( delta, impulse, impulse );
+         m3x3_mulv( iIw, impulse, impulse );
+         v3_add( impulse, localplayer.rb.w, localplayer.rb.w );
+
+         v3_cross( localplayer.rb.w, delta, rv );
+         v3_add( localplayer.rb.v, rv, rv );
+         vn = v3_dot( rv, ct->n );
+      }
+   }
+
+   v3f dt;
+   rb_depenetrate( manifold, manifold_len, dt );
+   v3_add( dt, localplayer.rb.co, localplayer.rb.co );
+   rb_update_matrices( &localplayer.rb );
+
+   substep_count ++;
+
+   if( player_skate.substep >= 0.0001f )
+      goto begin_collision;      /* again! */
+
+   /* 
+    * End of collision and dynamics routine
+    * --------------------------------------------------------------------------
+    */
+
+   f32 nforce = v3_length(normal_total);
+   if( nforce > 4.0f ){
+      if( nforce > 17.6f ){
+         vg_info( "player fell off due to hitting ground too hard\n" );
+         v3_muladds( localplayer.rb.v, normal_total, -1.0f, localplayer.rb.v );
+         player__dead_transition( k_player_die_type_feet );
+         return;
+      }
+
+      f32 amt = k_cam_punch;
+      if( localplayer.cam_control.camera_mode == k_cam_firstperson ){
+         amt *= 0.25f;
+      }
+
+      v3_muladds( localplayer.cam_land_punch_v, normal_total, amt,
+                  localplayer.cam_land_punch_v );
+   }
+
+   player_skate.surface = k_surface_prop_concrete;
+
+   for( int i=0; i<manifold_len; i++ ){
+      rb_ct *ct = &manifold[i];
+      struct world_surface *surf = world_contact_surface( world, ct );
+
+      if( surf->info.surface_prop > player_skate.surface )
+         player_skate.surface = surf->info.surface_prop;
+   }
+
+   for( int i=0; i<k_wheel_count; i++ ){
+      m4x3f mtx;
+      m3x3_copy( localplayer.rb.to_world, mtx );
+      m4x3_mulv( localplayer.rb.to_world, wheels[i].pos, mtx[3] );
+      vg_line_sphere( mtx, wheels[i].radius,
+                      (u32[]){ VG__WHITE, VG__BLACK, 
+                            wheels[i].colour }[ wheels[i].state ]);
+   }
+
+   skate_integrate();
+   vg_line_point( state->cog, 0.02f, VG__WHITE );
+
+   u32 id = world_intersect_gates( world, localplayer.rb.co, state->prev_pos );
+
+   if( id ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
+
+      m4x3_mulv( gate->transport, localplayer.rb.co, localplayer.rb.co );
+      m3x3_mulv( gate->transport, localplayer.rb.v,  localplayer.rb.v );
+      m4x3_mulv( gate->transport, state->cog,   state->cog );
+      m3x3_mulv( gate->transport, state->cog_v, state->cog_v );
+      m3x3_mulv( gate->transport, state->throw_v, state->throw_v );
+      m3x3_mulv( gate->transport, state->head_position,
+                                  state->head_position );
+      m3x3_mulv( gate->transport, state->up_dir, state->up_dir );
+
+      v4f transport_rotation;
+      m3x3_q( gate->transport, transport_rotation );
+      q_mul( transport_rotation, localplayer.rb.q, localplayer.rb.q );
+      q_mul( transport_rotation, state->smoothed_rotation,
+                                 state->smoothed_rotation );
+      q_normalize( localplayer.rb.q );
+      q_normalize( state->smoothed_rotation );
+      rb_update_matrices( &localplayer.rb );
+      player__pass_gate( id );
+   }
+
+   /* FIXME: Rate limit */
+   static int stick_frames = 0;
+
+   if( state->activity >= k_skate_activity_ground )
+      stick_frames ++;
+   else
+      stick_frames = 0;
+
+   if( stick_frames > 5 ) stick_frames =  5;
+
+   if( stick_frames == 4 ){
+      if( state->activity == k_skate_activity_ground ){
+         if( (fabsf(state->slip) > 0.75f) ){
+            player__networked_sfx( k_player_subsystem_skate, 128, 
+                                   k_player_skate_soundeffect_land_bad,
+                                   localplayer.rb.co, 0.6f );
+         }
+         else{
+            player__networked_sfx( k_player_subsystem_skate, 128, 
+                                   k_player_skate_soundeffect_land_good,
+                                   localplayer.rb.co, 1.0f );
+         }
+      }
+      else if( player_skate.surface == k_surface_prop_metal ){
+         player__networked_sfx( k_player_subsystem_skate, 128, 
+                                k_player_skate_soundeffect_grind_metal,
+                                localplayer.rb.co, 1.0f );
+      }
+      else{
+         player__networked_sfx( k_player_subsystem_skate, 128, 
+                                k_player_skate_soundeffect_grind_wood,
+                                localplayer.rb.co, 1.0f );
+      }
+   } else if( stick_frames == 0 ){
+      /* TODO: EXIT SOUNDS */
+   }
+
+   if( (state->activity_prev < k_skate_activity_grind_any) && 
+       (state->activity >= k_skate_activity_grind_any) ){
+      state->velocity_limit = v3_length( localplayer.rb.v )*1.0f;
+      state->grind_y_start = localplayer.rb.co[1];
+   }
+
+   if( state->activity >= k_skate_activity_grind_any ){
+      f32 dy = localplayer.rb.co[1] - state->grind_y_start;
+      if( dy < 0.0f ){
+         state->velocity_limit += -dy*0.2f;
+      }
+      state->grind_y_start = localplayer.rb.co[1];
+
+
+      f32 speed_end = v3_length( localplayer.rb.v );
+      if( speed_end > state->velocity_limit ){
+         v3_muls( localplayer.rb.v, state->velocity_limit/speed_end, 
+                  localplayer.rb.v );
+      }
+   }
+}
+
+void player__skate_im_gui( ui_context *ctx )
+{
+   struct player_skate_state *state = &player_skate.state;
+   player__debugtext( ctx, 1, "V:  %5.2f %5.2f %5.2f",localplayer.rb.v[0],
+                                                localplayer.rb.v[1],
+                                                localplayer.rb.v[2] );
+   player__debugtext( ctx, 1, "CO: %5.2f %5.2f %5.2f",localplayer.rb.co[0],
+                                                localplayer.rb.co[1],
+                                                localplayer.rb.co[2] );
+   player__debugtext( ctx, 1, "W:  %5.2f %5.2f %5.2f",localplayer.rb.w[0],
+                                                localplayer.rb.w[1],
+                                                localplayer.rb.w[2] );
+
+   const char *activity_txt[] = {
+      "air",
+      "air_to_grind",
+      "ground",
+      "handplant",
+      "undefined (INVALID)",
+      "grind_any (INVALID)",
+      "grind_boardslide",
+      "grind_metallic (INVALID)",
+      "grind_back50",
+      "grind_front50",
+      "grind_5050"
+   };
+
+   player__debugtext( ctx, 1, "activity: %s", activity_txt[state->activity] );
+   player__debugtext( ctx, 1, "flip: %.4f %.4f", state->flip_rate, 
+                                             state->flip_time );
+   player__debugtext( ctx, 1, "trickv: %.2f %.2f %.2f", 
+                           state->trick_vel[0],
+                           state->trick_vel[1],
+                           state->trick_vel[2] );
+   player__debugtext( ctx, 1, "tricke: %.2fs %.2f %.2f %.2f", 
+                           state->trick_time,
+                           state->trick_euler[0],
+                           state->trick_euler[1],
+                           state->trick_euler[2] );
+}
+
+void player__skate_animate(void){
+   struct player_skate_state *state = &player_skate.state;
+   struct player_skate_animator *animator = &player_skate.animator;
+
+   /* Head */
+   float kheight = 2.0f,
+         kleg = 0.6f;
+
+   v3_zero( animator->offset );
+
+   v3f cog_local, cog_ideal;
+   m4x3_mulv( localplayer.rb.to_local, state->cog, cog_local );
+
+   v3_copy( state->up_dir, cog_ideal );
+   v3_normalize( cog_ideal );
+   m3x3_mulv( localplayer.rb.to_local, cog_ideal, cog_ideal );
+
+   v3_sub( cog_ideal, cog_local, animator->offset );
+
+   v3_muls( animator->offset, 4.0f, animator->offset );
+   animator->offset[1] *= -1.0f;
+
+   float curspeed  = v3_length( localplayer.rb.v ),
+         kickspeed = vg_clampf( curspeed*(1.0f/40.0f), 0.0f, 1.0f ),
+         kicks     = (vg_randf64(&vg.rand)-0.5f)*2.0f*kickspeed,
+         sign      = vg_signf( kicks );
+
+   animator->wobble[0] = vg_lerpf( animator->wobble[0], kicks*kicks*sign, 
+                                   6.0f*vg.time_delta);
+   animator->wobble[1] = vg_lerpf( animator->wobble[1], animator->wobble[0], 
+                                   2.4f*vg.time_delta);
+
+   animator->offset[0] *= 0.26f;
+   animator->offset[0] += animator->wobble[1]*3.0f;
+
+   animator->offset[1] *= -0.3f;
+   animator->offset[2] *= 0.01f;
+
+   animator->offset[0]=vg_clampf(animator->offset[0],-0.8f,0.8f)*
+                                 (1.0f-fabsf(animator->slide)*0.9f);
+   animator->offset[1]=vg_clampf(animator->offset[1],-0.5f,0.0f);
+
+   v3f cam_offset;
+   v3_mul( animator->offset, (v3f){1.0f,0.3f,1.0f}, cam_offset );
+
+   /* localized vectors */
+   m4x3_mulv( localplayer.rb.to_local, state->cog, animator->local_cog );
+
+   /* 
+    * Animation blending
+    * ===========================================
+    */
+   
+   /* sliding */
+   {
+      float desired = 0.0f;
+      if( state->activity == k_skate_activity_ground )
+         desired = vg_clampf( vg_maxf(fabsf( state->slip ),
+                                      fabsf( state->skid ) ), 0.0f, 1.0f );
+
+      animator->slide = vg_lerpf( animator->slide, desired, 2.4f*vg.time_delta);
+
+      f32 dirx = 0.0f;
+      if( fabsf(state->slip) > fabsf(dirx) ) dirx = state->slip;
+      if( fabsf(state->skid) > fabsf(dirx) ) dirx = state->skid;
+      if( fabsf( dirx ) > 0.025f ) dirx = vg_signf( dirx );
+         dirx = vg_signf( state->slip );
+      vg_slewf( &animator->x, dirx, 2.6f*vg.time_delta );
+   }
+
+   cam_offset[0] += animator->slide * -animator->x;
+   v3_copy( cam_offset, localplayer.cam_control.tpv_offset_extra );
+   
+   /* movement information */
+   int iair = state->activity <= k_skate_activity_air_to_grind;
+
+   float dirz = state->reverse > 0.0f? 0.0f: 1.0f,
+         fly  = iair?                  1.0f: 0.0f,
+         wdist= player_skate.weight_distribution[2] / k_board_length;
+
+   if( state->activity >= k_skate_activity_grind_any )
+      wdist = 0.0f;
+
+   animator->z      = vg_lerpf( animator->z,      dirz,  2.4f*vg.time_delta );
+   animator->skid = state->skid;
+   animator->fly    = vg_lerpf( animator->fly,    fly,   3.4f*vg.time_delta );
+   animator->weight = vg_lerpf( animator->weight, wdist, 9.0f*vg.time_delta );
+
+   float stand = 1.0f - vg_clampf( curspeed * 0.03f, 0.0f, 1.0f );
+   animator->stand  = vg_lerpf( animator->stand, stand, 6.0f*vg.time_delta );
+   animator->reverse = state->reverse;
+
+   if( fabsf(state->slip) > 0.3f ){
+      f32 slide_x = v3_dot(localplayer.rb.v, localplayer.rb.to_world[0]);
+      state->delayed_slip_dir = vg_signf(slide_x);
+   }
+
+   /* grinding */
+   f32 grind=state->activity >= k_skate_activity_grind_any? 1.0f: 0.0f;
+   animator->grind = vg_lerpf( animator->grind,  grind, 5.0f*vg.time_delta );
+
+   f32 grind_frame = 0.5f;
+
+   if( state->activity == k_skate_activity_grind_front50 )
+      grind_frame = 0.0f;
+   else if( state->activity == k_skate_activity_grind_back50 )
+      grind_frame = 1.0f;
+
+   animator->grind_balance = vg_lerpf( animator->grind_balance, grind_frame, 
+                                5.0f*vg.time_delta );
+   animator->activity = state->activity;
+   animator->surface = player_skate.surface;
+
+   /* pushing */
+   animator->push_time = vg.time - state->start_push;
+   animator->push = vg_lerpf( animator->push, 
+                              (vg.time - state->cur_push) < 0.125,
+                              6.0f*vg.time_delta );
+
+   /* jumping */
+   animator->jump_charge = state->jump_charge;
+   animator->jump = vg_lerpf( animator->jump, animator->jump_charge, 
+                              8.4f*vg.time_delta );
+
+   /* trick setup */
+   animator->jump_dir = state->jump_dir;
+   f32 jump_start_frame = 14.0f/30.0f;
+   animator->jump_time = animator->jump_charge * jump_start_frame;
+   f32 jump_frame = (vg.time - state->jump_time) + jump_start_frame;
+   if( jump_frame >= jump_start_frame && jump_frame <= (40.0f/30.0f) )
+      animator->jump_time = jump_frame;
+
+   /* trick */
+   float jump_t = vg.time-state->jump_time;
+   float k=17.0f;
+   float h = k*jump_t;
+   float extra = h*exp(1.0-h) * (state->jump_dir?1.0f:-1.0f);
+         extra *= state->slap * 4.0f;
+
+   v3_add( state->trick_euler, state->trick_residuald, 
+            animator->board_euler );
+   v3_muls( animator->board_euler, VG_TAUf, animator->board_euler );
+
+   animator->board_euler[0] *= 0.5f;
+   animator->board_euler[1] += extra;
+   animator->trick_type = state->trick_type;
+
+   /* board lean */
+   f32 lean1, lean2 = animator->steer[0] * animator->reverse * -0.36f,
+       lean;
+
+   lean1 = animator->slide * animator->delayed_slip_dir;
+   if( fabsf(lean1)>fabsf(lean2) ) lean = lean1;
+   else                            lean = lean2;
+
+   if( ((int)roundf(animator->board_euler[0]/VG_PIf)) % 2 ) lean = -lean;
+   lean = vg_clampf( lean, -1.0f, 1.0f );
+   animator->board_lean = 
+      vg_lerpf(animator->board_lean, lean, vg.time_delta*18.0f);
+
+   /* feet placement */
+   struct player_board *board = 
+      addon_cache_item_if_loaded( k_addon_type_board,
+                                  localplayer.board_view_slot );
+   if( board ){
+      if( animator->weight > 0.0f ){
+         animator->foot_offset[0] = 
+            board->truck_positions[k_board_truck_back][2]+0.3f;
+      }
+      else{
+         animator->foot_offset[1] = 
+            board->truck_positions[k_board_truck_front][2]-0.3f;
+      }
+   }
+
+   f32 slapm = vg_maxf( 1.0f-v3_length2( state->trick_vel ), 0.0f );
+   animator->slap = state->slap;
+   animator->subslap = vg_lerpf( animator->subslap, slapm, 
+                                 vg.time_delta*10.0f );
+
+#if 0
+   f32 l = ((state->activity < k_skate_activity_ground) &&
+             v3_length2(state->trick_vel) > 0.1f )? 1: 0;
+   animator->trick_foot = vg_lerpf( animator->trick_foot, l, 
+                                    8.4f*vg.time_delta );
+#endif
+
+   animator->trick_foot = vg_exp_impulse( state->trick_time, 5.0f );
+
+   /* grab */
+   v2f grab_input;
+   joystick_state( k_srjoystick_grab, grab_input );
+   v2_add( state->grab_mouse_delta, grab_input, grab_input );
+
+   if( v2_length2( grab_input ) <= 0.001f ) grab_input[0] = -1.0f;
+   else v2_normalize_clamp( grab_input );
+   v2_lerp( animator->grab, grab_input, 2.4f*vg.time_delta, animator->grab );
+   animator->grabbing = state->grabbing;
+
+   /* steer */
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+   animator->airdir = vg_lerpf( animator->airdir, 
+                                -steer[0], 2.4f*vg.time_delta );
+
+   animator->steer[0] = steer[0];
+   animator->steer[1] = vg_lerpf( animator->steer[1], 
+                                  steer[0], 4.0f*vg.time_delta );
+      
+
+   /* flip angle */
+   if( (state->activity <= k_skate_activity_air_to_grind) &&
+       (fabsf(state->flip_rate) > 0.01f) ){
+      float substep = vg.time_fixed_extrapolate;
+      float t  = state->flip_time+state->flip_rate*substep*vg.time_fixed_delta;
+            sign  = vg_signf( t );
+
+      t  = 1.0f - vg_minf( 1.0f, fabsf( t * 1.1f ) );
+      t  = sign * (1.0f-t*t);
+
+      f32 angle = vg_clampf( t, -1.0f, 1.0f ) * VG_TAUf,
+          distm = state->land_dist * fabsf(state->flip_rate) * 3.0f,
+          blend = vg_clampf( 1.0f-distm, 0.0f, 1.0f );
+      angle = vg_lerpf( angle, vg_signf(state->flip_rate)*VG_TAUf, blend );
+      q_axis_angle( animator->qflip, state->flip_axis, angle );
+   }
+   else 
+      q_identity( animator->qflip );
+
+   /* counter-rotation */
+   if( v3_length2( state->up_dir ) > 0.001f ){
+      v4_lerp( state->smoothed_rotation, localplayer.rb.q, 
+               2.0f*vg.time_frame_delta,
+               state->smoothed_rotation );
+      q_normalize( state->smoothed_rotation );
+
+      v3f yaw_smooth = {1.0f,0.0f,0.0f};
+      q_mulv( state->smoothed_rotation, yaw_smooth, yaw_smooth );
+      m3x3_mulv( localplayer.rb.to_local, yaw_smooth, yaw_smooth );
+      yaw_smooth[1] = 0.0f;
+      v3_normalize( yaw_smooth );
+
+      f32 yaw_counter_rotate  = yaw_smooth[0];
+          yaw_counter_rotate  = vg_maxf( 0.7f, yaw_counter_rotate );
+          yaw_counter_rotate  = acosf( yaw_counter_rotate );
+          yaw_counter_rotate *= 1.0f-animator->fly;
+
+      v3f ndir;
+      m3x3_mulv( localplayer.rb.to_local, state->up_dir, ndir );
+      v3_normalize( ndir );
+
+      v3f up = { 0.0f, 1.0f, 0.0f };
+      float a = v3_dot( ndir, up );
+      a = acosf( vg_clampf( a, -1.0f, 1.0f ) );
+
+      v3f axis;
+      v4f qcounteryaw, qfixup;
+      
+      v3_cross( up, ndir, axis );
+      q_axis_angle( qfixup, axis, a*2.0f );
+
+      v3_cross( (v3f){1.0f,0.0f,0.0f}, yaw_smooth, axis );
+      q_axis_angle( qcounteryaw, axis, yaw_counter_rotate );
+
+      q_mul( qcounteryaw, qfixup, animator->qfixuptotal );
+      q_normalize( animator->qfixuptotal );
+
+      v3f p1, p2;
+      m3x3_mulv( localplayer.rb.to_world, up, p1 );
+      m3x3_mulv( localplayer.rb.to_world, ndir, p2 );
+
+      vg_line_arrow( localplayer.rb.co, p1, 0.5f, VG__PINK );
+      vg_line_arrow( localplayer.rb.co, p2, 0.5f, VG__PINK );
+   }
+   else q_identity( animator->qfixuptotal );
+
+   if( state->activity == k_skate_activity_handplant ){
+      v3_copy( state->store_co, animator->root_co );
+      v4_copy( state->store_q, animator->root_q );
+      v3_zero( animator->root_v );
+   }
+   else {
+      rb_extrapolate( &localplayer.rb, animator->root_co, animator->root_q );
+      v3_copy( localplayer.rb.v, animator->root_v );
+   }
+
+   animator->handplant_t = state->handplant_t;
+}
+                        
+void player__skate_pose( void *_animator, player_pose *pose ){
+   struct skeleton *sk = &localplayer.skeleton;
+   struct player_skate_animator *animator = _animator;
+
+   pose->type = k_player_pose_type_ik;
+   v3_copy( animator->root_co, pose->root_co );
+   v4_copy( animator->root_q, pose->root_q );
+
+   /* transform */
+   v3f ext_up,ext_co;
+   q_mulv( pose->root_q, (v3f){0.0f,1.0f,0.0f}, ext_up );
+   v3_copy( pose->root_co, ext_co );
+   v3_muladds( pose->root_co, ext_up, -0.1f, pose->root_co );
+
+   /* apply flip rotation at midpoint */
+   q_mul( animator->qflip, pose->root_q, pose->root_q );
+   q_normalize( pose->root_q );
+
+   v3f rotation_point, rco;
+   v3_muladds( ext_co, ext_up, 0.5f, rotation_point );
+   v3_sub( pose->root_co, rotation_point, rco );
+   
+   q_mulv( animator->qflip, rco, rco );
+   v3_add( rco, rotation_point, pose->root_co );
+
+   /* ANIMATIONS 
+    * ---------------------------------------------------------------------- */
+
+   mdl_keyframe apose[32], bpose[32];
+   mdl_keyframe ground_pose[32];
+   {
+      /* stand/crouch */
+      f32 dir_frame   = animator->z * (15.0f/30.0f),
+          stand_blend = animator->offset[1]*-2.0f;
+
+      pose->board.lean = animator->board_lean;
+
+      stand_blend = vg_clampf( 1.0f-animator->local_cog[1], 0, 1 );
+
+      skeleton_sample_anim( sk, player_skate.anim_stand, dir_frame, apose );
+      skeleton_sample_anim( sk, player_skate.anim_highg, dir_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, stand_blend, apose );
+
+      /* sliding */
+      f32 slide_frame = animator->x * 0.25f + 0.25f;
+      skeleton_sample_anim( sk, player_skate.anim_slide, slide_frame, bpose );
+
+      mdl_keyframe mirrored[32];
+      player_mirror_pose( bpose, mirrored );
+      skeleton_lerp_pose( sk, bpose, mirrored, animator->z, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, animator->slide, apose );
+
+      if( animator->reverse > 0.0f ){
+         skeleton_sample_anim( sk, player_skate.anim_push, animator->push_time, 
+                               bpose );
+      }
+      else{
+         skeleton_sample_anim( sk, player_skate.anim_push_reverse, 
+                               animator->push_time, bpose );
+      }
+      skeleton_lerp_pose( sk, apose, bpose, animator->push, apose );
+
+      struct skeleton_anim *jump_anim = animator->jump_dir?
+                                        player_skate.anim_ollie:
+                                        player_skate.anim_ollie_reverse;
+
+      f32 setup_blend = vg_minf( animator->jump, 1.0f );
+      skeleton_sample_anim_clamped( sk, jump_anim, animator->jump_time, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, setup_blend, ground_pose );
+   }
+   
+   mdl_keyframe air_pose[32];
+   {
+      float air_frame = (animator->airdir*0.5f+0.5f) * (15.0f/30.0f);
+      skeleton_sample_anim( sk, player_skate.anim_air, air_frame, apose );
+
+      float ang = atan2f( animator->grab[0], animator->grab[1] ),
+            ang_unit = (ang+VG_PIf) * (1.0f/VG_TAUf),
+            grab_frame = ang_unit * (15.0f/30.0f);
+
+      skeleton_sample_anim( sk, player_skate.anim_grabs, grab_frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, animator->grabbing, air_pose );
+   }
+
+   skeleton_lerp_pose( sk, ground_pose, air_pose, animator->fly, 
+                       pose->keyframes );
+
+   mdl_keyframe *kf_board    = &pose->keyframes[localplayer.id_board-1],
+                *kf_foot_l   = &pose->keyframes[localplayer.id_ik_foot_l-1],
+                *kf_foot_r   = &pose->keyframes[localplayer.id_ik_foot_r-1],
+                *kf_knee_l   = &pose->keyframes[localplayer.id_ik_knee_l-1],
+                *kf_knee_r   = &pose->keyframes[localplayer.id_ik_knee_r-1],
+                *kf_hip      = &pose->keyframes[localplayer.id_hip-1],
+                *kf_wheels[] = { &pose->keyframes[localplayer.id_wheel_r-1],
+                                 &pose->keyframes[localplayer.id_wheel_l-1] };
+
+
+   mdl_keyframe grind_pose[32];
+   {
+      f32 frame = animator->grind_balance * 0.5f;
+
+      skeleton_sample_anim( sk, player_skate.anim_grind, frame, apose );
+      skeleton_sample_anim( sk, player_skate.anim_grind_jump, frame, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, animator->jump, grind_pose );
+   }
+   skeleton_lerp_pose( sk, pose->keyframes, grind_pose, 
+                       animator->grind, pose->keyframes );
+   float add_grab_mod = 1.0f - animator->fly;
+
+   /* additive effects */
+   u32 apply_to[] = { localplayer.id_hip, 
+                      localplayer.id_ik_hand_l,
+                      localplayer.id_ik_hand_r,
+                      localplayer.id_ik_elbow_l,
+                      localplayer.id_ik_elbow_r };
+
+   float apply_rates[] = { 1.0f,
+                           0.75f,
+                           0.75f,
+                           0.75f,
+                           0.75f };
+
+   for( int i=0; i<VG_ARRAY_LEN(apply_to); i ++ ){
+      pose->keyframes[apply_to[i]-1].co[0] += animator->offset[0]*add_grab_mod;
+      pose->keyframes[apply_to[i]-1].co[2] += animator->offset[2]*add_grab_mod;
+   }
+
+#if 1
+   /* angle 'correction' */
+   v3f origin;
+   v3_add( sk->bones[localplayer.id_hip].co, kf_hip->co, origin );
+
+   for( int i=0; i<VG_ARRAY_LEN(apply_to); i ++ ){
+      mdl_keyframe *kf = &pose->keyframes[apply_to[i]-1];
+      keyframe_rotate_around( kf, origin, sk->bones[apply_to[i]].co,
+                              animator->qfixuptotal );
+   }
+#endif
+
+
+   if( animator->activity == k_skate_activity_handplant ){
+      struct skeleton_anim *anim = player_skate.anim_handplant;
+
+      mdl_keyframe hpose[32];
+      skeleton_sample_anim_clamped( sk, anim, animator->handplant_t, hpose );
+      if( animator->reverse < 0.0f )
+         player_mirror_pose( hpose, hpose );
+
+      mdl_keyframe *kf_world = &hpose[ localplayer.id_world -1 ];
+      m4x3f world, mmdl, world_view;
+      q_m3x3( kf_world->q, world );
+      v3_copy( kf_world->co, world[3] );
+
+      q_m3x3( pose->root_q, mmdl );
+      v3_copy( pose->root_co, mmdl[3] );
+
+      m4x3_mul( mmdl, world, world_view );
+
+      vg_line_arrow( world_view[3], world_view[0], 1.0f, 0xff0000ff );
+      vg_line_arrow( world_view[3], world_view[1], 1.0f, 0xff00ff00 );
+      vg_line_arrow( world_view[3], world_view[2], 1.0f, 0xffff0000 );
+
+      m4x3f invworld;
+      m4x3_invert_affine( world, invworld );
+      m4x3_mul( mmdl, invworld, world_view );
+
+      m3x3_q( world_view, pose->root_q );
+      v3_copy( world_view[3], pose->root_co );
+
+      f32 t        = animator->handplant_t,
+          frames   = anim->length-1,
+          length   = animator->activity == k_skate_activity_handplant?
+                        frames / anim->rate:
+                        999999,
+          end_dist = vg_minf( t, length - t )/k_anim_transition,
+          blend    = vg_smoothstepf( vg_minf(1,end_dist) );
+
+      skeleton_lerp_pose( sk, pose->keyframes, hpose, blend, pose->keyframes );
+   }
+
+
+   /* trick rotation */
+   v4f qtrick, qyaw, qpitch, qroll;
+   q_axis_angle( qyaw,   (v3f){0.0f,1.0f,0.0f}, animator->board_euler[0] );
+   q_axis_angle( qpitch, (v3f){1.0f,0.0f,0.0f}, animator->board_euler[1] );
+   q_axis_angle( qroll,  (v3f){0.0f,0.0f,1.0f}, animator->board_euler[2] );
+
+   q_mul( qyaw, qroll, qtrick );
+   q_mul( qpitch, qtrick, qtrick );
+   q_mul( kf_board->q, qtrick, kf_board->q );
+   q_normalize( kf_board->q );
+
+   kf_foot_l->co[2] = vg_lerpf( kf_foot_l->co[2], animator->foot_offset[0],
+                                 0.5f * animator->weight );
+   kf_foot_r->co[2] = vg_lerpf( kf_foot_r->co[2], animator->foot_offset[1],
+                                -0.5f * animator->weight );
+
+   kf_foot_l->co[1] += animator->slap;
+   kf_foot_r->co[1] += animator->slap;
+   kf_knee_l->co[1] += animator->slap;
+   kf_knee_r->co[1] += animator->slap;
+   kf_board->co[1]  += animator->slap * animator->subslap;
+   kf_hip->co[1] += animator->slap * 0.25f;
+
+   /* kickflip and shuvit are in the wrong order for some reason */
+   if( animator->trick_type == k_trick_type_kickflip ){
+      kf_foot_l->co[0] += animator->trick_foot * 0.15f;
+      kf_foot_r->co[0] -= animator->trick_foot * 0.15f;
+      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
+      kf_foot_r->co[1] -= animator->trick_foot * 0.18f;
+   }
+   else if( animator->trick_type == k_trick_type_shuvit ){
+      kf_foot_l->co[0] += animator->trick_foot * 0.2f;
+      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
+      kf_foot_r->co[0] -= animator->trick_foot * 0.1f;
+      kf_foot_r->co[1] += animator->trick_foot * 0.09f;
+   }
+   else if( animator->trick_type == k_trick_type_treflip ){
+      kf_foot_l->co[0] += animator->trick_foot * 0.2f;
+      kf_foot_r->co[0] -= animator->trick_foot * 0.15f;
+      kf_foot_l->co[1] -= animator->trick_foot * 0.18f;
+      kf_foot_r->co[1] -= animator->trick_foot * 0.18f;
+   }
+
+   /* 
+    * animation wishlist:
+    *    boardslide/grind jump animations
+    *    when tricking the slap should not appply or less apply
+    *    not animations however DONT target grinds that are vertically down.
+    */
+
+   /* truck rotation */
+   for( int i=0; i<2; i++ ){
+      float a = vg_minf( player_skate.truckv0[i][0], 1.0f );
+      a = -acosf( a ) * vg_signf( player_skate.truckv0[i][1] );
+
+      v4f q;
+      q_axis_angle( q, (v3f){0.0f,0.0f,1.0f}, a );
+      q_mul( q, kf_wheels[i]->q, kf_wheels[i]->q );
+      q_normalize( kf_wheels[i]->q );
+   }
+#if 1
+   {
+      mdl_keyframe
+         *kf_head    = &pose->keyframes[localplayer.id_head-1],
+         *kf_elbow_l = &pose->keyframes[localplayer.id_ik_elbow_l-1],
+         *kf_elbow_r = &pose->keyframes[localplayer.id_ik_elbow_r-1],
+         *kf_hand_l  = &pose->keyframes[localplayer.id_ik_hand_l-1],
+         *kf_hand_r  = &pose->keyframes[localplayer.id_ik_hand_r-1],
+         *kf_hip     = &pose->keyframes[localplayer.id_hip-1];
+
+      float warble = vg_perlin_fract_1d( vg.time, 2.0f, 2, 300 );
+            warble *= vg_maxf(animator->grind, fabsf(animator->weight)) * 0.3f;
+
+      v4f qrot;
+      q_axis_angle( qrot, (v3f){0.8f,0.7f,0.6f}, warble );
+
+      v3f origin = {0.0f,0.2f,0.0f};
+      keyframe_rotate_around( kf_hand_l, origin, 
+                              sk->bones[localplayer.id_ik_hand_l].co, qrot );
+      keyframe_rotate_around( kf_hand_r, origin, 
+                              sk->bones[localplayer.id_ik_hand_r].co, qrot );
+      keyframe_rotate_around( kf_hip, origin, 
+                              sk->bones[localplayer.id_hip].co, qrot );
+      keyframe_rotate_around( kf_elbow_r, origin, 
+                              sk->bones[localplayer.id_ik_elbow_r].co, qrot );
+      keyframe_rotate_around( kf_elbow_l, origin, 
+                              sk->bones[localplayer.id_ik_elbow_l].co, qrot );
+
+      q_inv( qrot, qrot );
+      q_mul( qrot, kf_head->q, kf_head->q );
+      q_normalize( kf_head->q );
+
+
+      /* hand placement */
+
+      u32 hand_id = animator->z < 0.5f? 
+                    localplayer.id_ik_hand_l: localplayer.id_ik_hand_r;
+
+      v3f sample_co;
+      m4x3f mmdl;
+      q_m3x3( pose->root_q, mmdl );
+      q_mulv( pose->root_q, pose->keyframes[hand_id-1].co, mmdl[3] );
+      v3_add( mmdl[3], pose->root_co, mmdl[3] );
+      m4x3_mulv( mmdl, sk->bones[hand_id].co, sample_co );
+
+      v3_muladds( sample_co, mmdl[1], 0.3f, sample_co );
+      vg_line_point( sample_co, 0.04f, 0xff0000ff );
+
+      v3f dir;
+      v3_muls( mmdl[1], -1.0f, dir );
+      ray_hit hit = { .dist = 1.5f };
+      if(ray_world( world_current_instance(), sample_co, dir, &hit, 0 )){
+         vg_line_cross( hit.pos, 0xff0000ff, 0.05f );
+         vg_line( sample_co, hit.pos, 0xffffffff );
+
+         f32 amt = vg_maxf( 0.0f, animator->slide-0.5f ) * 
+                   2.0f * fabsf(animator->z*2.0f-1.0f);
+
+         f32 d = (hit.dist - 0.3f) * amt;
+         pose->keyframes[hand_id-1].co[1] -= d;
+         kf_hip->co[1] -= d*0.4f;
+      }
+
+      /* skid */
+      f32 amt = vg_maxf(0.0f, (animator->slide - 0.5f) * 2.0f);
+      u8 skidders[] = { localplayer.id_ik_foot_l, 
+                        localplayer.id_ik_foot_r,
+                        localplayer.id_board };
+      v4f qskid;
+      q_axis_angle( qskid, (v3f){0,1,0}, -animator->steer[1]*0.2f );
+
+      for( u32 i=0; i<VG_ARRAY_LEN(skidders); i ++ ){
+         mdl_keyframe *kf = &pose->keyframes[ skidders[i]-1 ];
+         keyframe_rotate_around( kf, 
+               (v3f){0,0,0.4f*(animator->z*2.0f-1.0f)*amt}, 
+                                 sk->bones[skidders[i]].co, qskid );
+      }
+   }
+#endif
+}
+
+void player__skate_effects( void *_animator, m4x3f *final_mtx,
+                                   struct player_board *board,
+                                   struct player_effects_data *effect_data ){
+   struct skeleton *sk = &localplayer.skeleton;
+   struct player_skate_animator *animator = _animator;
+
+   v3f vp0, vp1, vpc;
+   if( board ){
+      v3_copy((v3f){0.0f,0.02f, board->truck_positions[0][2]}, vp1 );
+      v3_copy((v3f){0.0f,0.02f, board->truck_positions[1][2]}, vp0 );
+   }
+   else{
+      v3_zero( vp0 );
+      v3_zero( vp1 );
+   }
+
+   v3f *board_mtx = final_mtx[ localplayer.id_board ];
+   m4x3_mulv( board_mtx, vp0, vp0 );
+   m4x3_mulv( board_mtx, vp1, vp1 );
+   v3_add( vp0, vp1, vpc );
+   v3_muls( vpc, 0.5f, vpc );
+
+   if( animator->surface == k_surface_prop_sand ){
+      if( (animator->slide>0.4f) && (v3_length2(animator->root_v)>4.0f*4.0f) ){
+         v3f v, co;
+         v3_muls( animator->root_v, 0.5f, v );
+         v3_lerp( vp0, vp1, vg_randf64(&vg.rand), co );
+
+         effect_data->sand.colour = 0xff8ec4e6;
+         effect_spark_apply( &effect_data->sand, co, v, vg.time_delta * 8.0 );
+      }
+   }
+
+   if( animator->grind > 0.5f ){
+      int back = 0, front = 0, mid = 0;
+
+      if( animator->activity == k_skate_activity_grind_5050 ){
+         back = 1;
+         front = 1;
+      }
+      else if( animator->activity == k_skate_activity_grind_back50 ){
+         back = 1;
+      }
+      else if( animator->activity == k_skate_activity_grind_front50 ){
+         front = 1;
+      }
+      else if( animator->activity == k_skate_activity_grind_boardslide ){
+         mid = 1;
+      }
+
+      if( back ){
+         effect_spark_apply( &effect_data->spark, vp0,
+                              animator->root_v, vg.time_delta );
+      }
+
+      if( front ){
+         effect_spark_apply( &effect_data->spark, vp1,
+                              animator->root_v, vg.time_delta );
+      }
+
+      if( mid ){
+         effect_spark_apply( &effect_data->spark, vpc,
+                              animator->root_v, vg.time_delta );
+      }
+   }
+}
+
+void player__skate_post_animate(void){
+   struct player_skate_state *state = &player_skate.state;
+   localplayer.cam_velocity_influence = 1.0f;
+   localplayer.cam_dist = 1.8f;
+
+   v3f head = { 0.0f, 1.8f, 0.0f };
+   m4x3_mulv( localplayer.final_mtx[ localplayer.id_head ], 
+              head, state->head_position );
+   m4x3_mulv( localplayer.rb.to_local, 
+              state->head_position, state->head_position );
+}
+
+void player__skate_reset_animator(void){
+   struct player_skate_state *state = &player_skate.state;
+
+   memset( &player_skate.animator, 0, sizeof(player_skate.animator) );
+
+   if( state->activity <= k_skate_activity_air_to_grind ) 
+      player_skate.animator.fly = 1.0f;
+   else 
+      player_skate.animator.fly = 0.0f;
+}
+
+void player__skate_clear_mechanics(void)
+{
+   struct player_skate_state *state = &player_skate.state;
+   state->jump_charge    = 0.0f;
+   state->charging_jump  = 0;
+   state->jump_dir       = 0;
+   v3_zero( state->flip_axis );
+   state->flip_time      = 0.0f;
+   state->flip_rate      = 0.0f;
+   state->reverse        = 0.0f;
+   state->slip           = 0.0f;
+   state->grabbing       = 0.0f;
+   v2_zero( state->grab_mouse_delta );
+   state->slap           = 0.0f;
+   state->jump_time      = 0.0;
+   state->start_push     = 0.0;
+   state->cur_push       = 0.0;
+   state->air_start      = 0.0;
+
+   v3_zero( state->air_init_v );
+   v3_zero( state->air_init_co );
+
+   state->gravity_bias   = k_gravity;
+   v3_copy( localplayer.rb.co, state->prev_pos );
+   v4_copy( localplayer.rb.q, state->smoothed_rotation );
+   v3_zero( state->throw_v );
+   v3_zero( state->trick_vel );
+   v3_zero( state->trick_euler );
+   v3_zero( state->cog_v );
+   state->grind_cooldown = 0;
+   state->surface_cooldown = 0;
+   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 1.0f, state->cog );
+   v3_copy( localplayer.rb.to_world[1], state->up_dir );
+   v3_copy( localplayer.rb.to_world[1], player_skate.surface_picture );
+   v3_copy( localplayer.rb.co, state->prev_pos );
+   v3_zero( player_skate.weight_distribution );
+
+   v3f head = { 0.0f, 1.8f, 0.0f };
+   m4x3_mulv( localplayer.rb.to_world, head, state->head_position );
+}
+
+#include "network_compression.h"
+
+void player__skate_animator_exchange( bitpack_ctx *ctx, void *data ){
+   struct player_skate_animator *animator = data;
+   
+   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
+   bitpack_qquat( ctx, animator->root_q );
+
+   bitpack_qv3f( ctx, 8, -1.0f, 1.0f, animator->offset );
+   bitpack_qv3f( ctx, 8, -1.0f, 1.0f, animator->local_cog );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->slide );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->z );
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->x );
+
+   /* these could likely be pressed down into single bits if needed */
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->fly );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grind );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->stand );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->push );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->jump );        /*??*/
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->jump_charge ); /*??*/
+
+   /* just the sign bit? */
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->reverse );
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->delayed_slip_dir );
+   bitpack_bytes( ctx, 1, &animator->jump_dir );
+   bitpack_bytes( ctx, 1, &animator->trick_type );
+
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grind_balance );
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->airdir );
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->weight );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->trick_foot );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->slap );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->subslap );
+   bitpack_qf32( ctx, 8,  0.0f, 1.0f, &animator->grabbing );
+
+   /* animator->wobble is ommited */
+
+   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->foot_offset );
+   bitpack_qquat( ctx, animator->qfixuptotal );
+   bitpack_qquat( ctx, animator->qflip );
+
+   bitpack_qv3f( ctx, 16, -100.0f, 100.0f, animator->board_euler );
+   bitpack_qf32( ctx, 8, -1.0f, 1.0f, &animator->board_lean );
+   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->steer );
+   bitpack_qv2f( ctx, 8, -1.0f, 1.0f, animator->grab );
+
+   bitpack_qf32( ctx, 16,  0.0f, 120.0f, &animator->push_time );
+   bitpack_qf32( ctx, 16,  0.0f, 120.0f, &animator->jump_time );
+   bitpack_qf32( ctx, 16,  0.0f, 4.0f, &animator->handplant_t );
+   bitpack_qv3f( ctx, 16, -100.0f, 100.0f, animator->root_v );
+   bitpack_bytes( ctx, 1, &animator->activity );
+}
+
+void player__skate_sfx_oneshot( u8 id, v3f pos, f32 volume ){
+   audio_lock();
+
+   if( id == k_player_skate_soundeffect_jump ){
+      audio_oneshot_3d( &audio_jumps[vg_randu32(&vg.rand)%2], 
+                        pos, 40.0f, volume );
+   }
+   else if( id == k_player_skate_soundeffect_tap ){
+      audio_oneshot_3d( &audio_taps[vg_randu32(&vg.rand)%4], 
+                        pos, 40.0f, volume );
+   }
+   else if( id == k_player_skate_soundeffect_land_good ){
+      audio_oneshot_3d( &audio_lands[vg_randu32(&vg.rand)%3], 
+                        pos, 40.0f, volume );
+   }
+   else if( id == k_player_skate_soundeffect_land_bad ){
+      audio_oneshot_3d( &audio_lands[vg_randu32(&vg.rand)%2+3], 
+                        pos, 40.0f, volume );
+   }
+   else if( id == k_player_skate_soundeffect_grind_metal ){
+      audio_oneshot_3d( &audio_board[3], pos, 40.0f, volume );
+   }
+   else if( id == k_player_skate_soundeffect_grind_wood ){
+      audio_oneshot_3d( &audio_board[8], pos, 40.0f, volume );
+   }
+
+   audio_unlock();
+}
diff --git a/src/player_skate.h b/src/player_skate.h
new file mode 100644 (file)
index 0000000..8adb024
--- /dev/null
@@ -0,0 +1,326 @@
+#pragma once
+#include "vg/vg_audio.h"
+#include "player.h"
+#include "player_api.h"
+
+typedef struct jump_info jump_info;
+
+struct player_skate{
+   struct player_skate_state{
+      enum skate_activity{
+         k_skate_activity_air,
+         k_skate_activity_air_to_grind,
+         k_skate_activity_ground,
+         k_skate_activity_handplant,
+         k_skate_activity_undefined,
+         k_skate_activity_grind_any,
+         k_skate_activity_grind_boardslide,
+         k_skate_activity_grind_metallic,
+         k_skate_activity_grind_back50,
+         k_skate_activity_grind_front50,
+         k_skate_activity_grind_5050
+      }
+      activity,
+      activity_prev;
+
+      u32 grind_cooldown,
+          surface_cooldown;
+
+      f32 reverse, slip, delayed_slip_dir;
+      int manual_direction;
+
+      /* tricks */
+      v3f   flip_axis;
+      float flip_time,
+            flip_rate;
+
+      v3f   trick_vel,     /* measured in units of TAU/s */
+            trick_euler;   /* measured in units of TAU */
+      v3f trick_residualv, /* spring */
+          trick_residuald;
+
+      float trick_time;
+      enum  trick_type{
+         k_trick_type_none,
+         k_trick_type_kickflip,
+         k_trick_type_shuvit,
+         k_trick_type_treflip,
+      }
+      trick_type;
+      float gravity_bias;
+
+      f32 trick_input_collect;
+
+      v3f up_dir;
+      v3f head_position;
+
+      v3f throw_v;
+      v3f cog_v, cog;
+
+      float grabbing;
+      v2f grab_mouse_delta;
+
+      int charging_jump, jump_dir;
+      float jump_charge,
+            slap;
+
+      double jump_time;
+      double start_push,
+             cur_push;
+
+      v3f prev_pos;
+
+      /* initial launch conditions */
+      double air_start;
+      v3f    air_init_v,
+             air_init_co;
+
+      float land_dist;
+      v3f land_normal;
+      v4f smoothed_rotation;
+
+      f32 velocity_limit, grind_y_start, skid;
+      f32 handplant_t;
+
+      v3f store_cog_v, store_cog, store_co;
+      v4f store_smoothed, store_q;
+   }
+   state;
+
+   struct player_skate_animator {
+      v3f root_co;
+      v4f root_q;
+      v3f root_v;
+
+      v3f offset,
+          local_cog;
+
+      f32 slide,
+          skid,
+          z,
+          x,
+          fly,
+          grind,
+          grind_balance,
+          stand,
+          push,
+          jump,
+          airdir,
+          weight,
+          trick_foot,
+          slap,
+          subslap,
+          reverse,
+          delayed_slip_dir,
+          grabbing;
+
+      v2f wobble;
+      f32 foot_offset[2];
+
+      v4f qfixuptotal;
+      v4f qflip;
+
+      v3f board_euler;
+      f32 board_lean;
+      v2f steer, grab;
+
+      f32 jump_charge;
+
+      /* linear anims. TODO: we can union a bunch of variables here depending
+       * on activity. */
+      f32 push_time, jump_time, handplant_t;
+      u8 jump_dir;
+      u8 trick_type; /* todo: should encode grind type */
+      u8 activity, surface;
+   }
+   animator;
+
+   f32 collect_feedback;
+
+   /* animation /audio
+    * --------------------------------------------------------------*/
+   struct skeleton_anim *anim_stand, *anim_highg, *anim_slide,
+                        *anim_air, *anim_grind, *anim_grind_jump,
+                        *anim_push,  *anim_push_reverse,
+                        *anim_ollie, *anim_ollie_reverse,
+                        *anim_grabs, *anim_stop,
+                        *anim_handplant;
+
+   /* vectors representing the direction of the axels in localspace */
+   v3f truckv0[2];
+
+   audio_channel *aud_main, *aud_slide, *aud_air;
+   enum mdl_surface_prop surface, audio_surface;
+
+   int wheel_contacts[2];
+   float sample_change_cooldown;
+
+   enum {
+      k_skate_sample_concrete,
+      k_skate_sample_wood,
+      k_skate_sample_concrete_scrape_metal,
+      k_skate_sample_concrete_scrape_wood,
+      k_skate_sample_metal_scrape_generic
+   }
+   main_sample_type;
+
+   /*
+    * Physics 
+    * ----------------------------------------------------
+    */
+
+   float substep, substep_delta;
+
+   struct jump_info{
+      v3f   log[50];
+      v3f   n;
+      v3f   apex;
+      v3f   v;
+
+      float gravity;
+
+      int   log_length;
+      float score,
+            land_dist;
+
+      enum prediction_type{
+         k_prediction_none,
+         k_prediction_unset,
+         k_prediction_land,
+         k_prediction_grind
+      }
+      type;
+
+      u32   colour;
+   }
+   possible_jumps[36];
+   u32 possible_jump_count;
+
+   v3f surface_picture,
+       weight_distribution,
+       grind_vec,
+       grind_dir;
+
+   float grind_strength;
+   struct grind_limit{
+      v3f ra, n;
+      float p;
+   }
+   limits[3];
+   u32 limit_count;
+}
+extern player_skate;
+extern struct player_subsystem_interface player_subsystem_skate;
+
+enum player_skate_soundeffect {
+   k_player_skate_soundeffect_jump,
+   k_player_skate_soundeffect_tap,
+   k_player_skate_soundeffect_land_good,
+   k_player_skate_soundeffect_land_bad,
+   k_player_skate_soundeffect_grind_metal,
+   k_player_skate_soundeffect_grind_wood,
+};
+
+static float 
+   k_friction_lat          = 12.0f,
+   k_friction_resistance   = 0.01f,
+
+   k_max_push_speed        = 16.0f,
+   k_push_accel            = 10.0f,
+   k_push_cycle_rate       = 8.0f,
+
+   k_steer_ground          = 2.5f,
+   k_steer_air             = 3.6f,
+
+   k_jump_charge_speed     = (1.0f/0.4f),
+   k_jump_force            = 5.0f,
+
+   k_cog_spring            = 0.2f,
+   k_cog_damp              = 0.02f,
+   k_cog_mass_ratio        = 0.9f,
+
+   k_mmthrow_steer         = 1.0f,
+   k_mmthrow_scale         = 6.0f,
+   k_mmcollect_lat         = 2.0f,
+   k_mmcollect_vert        = 0.0f,
+   k_mmdecay               = 12.0f,
+   k_spring_angular        = 1.0f,
+
+   k_spring_force          = 300.0f,
+   k_spring_dampener       = 5.0f,
+
+   k_grind_spring          = 50.0f,
+   k_grind_aligment        = 10.0f,
+   k_grind_dampener        = 5.0f,
+
+   k_surface_spring        = 100.0f,
+   k_surface_dampener      = 40.0f,
+   k_manul_spring          = 200.0f,
+   k_manul_dampener        = 30.0f,
+   k_board_interia         = 8.0f,
+
+   k_grind_decayxy         = 30.0f,
+   k_grind_axel_min_vel    = 1.0f,
+   k_grind_axel_max_angle  = 0.95f, /* cosine(|a|) */
+   k_grind_axel_max_vangle = 0.4f,
+   k_grind_max_friction    = 3.0f,
+   k_grind_max_edge_angle  = 0.97f,
+
+   k_board_length          = 0.45f,
+   k_board_width           = 0.13f,
+   k_board_end_radius      = 0.1f,
+   k_board_radius          = 0.14f,    /* 0.07 */
+   
+   k_grind_balance         = -40.0f,
+   k_anim_transition       = 0.12f;
+
+static void player__skate_register(void)
+{
+   VG_VAR_F32( k_grind_dampener,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_spring,         flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_aligment,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_surface_spring,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_surface_dampener,     flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_board_interia,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_decayxy,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_axel_min_vel,   flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_axel_max_angle, flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_max_friction,   flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_grind_balance,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_friction_lat,         flags=VG_VAR_CHEAT );
+
+   VG_VAR_F32( k_cog_spring,           flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_cog_damp,             flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_cog_mass_ratio,       flags=VG_VAR_CHEAT );
+
+   VG_VAR_F32( k_spring_force,         flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_spring_dampener,      flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_spring_angular,       flags=VG_VAR_CHEAT );
+
+   VG_VAR_F32( k_mmthrow_scale,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_mmcollect_lat,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_mmcollect_vert,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_mmdecay,              flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_mmthrow_steer,        flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_anim_transition,      flags=VG_VAR_CHEAT );
+}
+
+void player__skate_bind         (void);
+void player__skate_pre_update   (void);
+void player__skate_update       (void);
+void player__skate_post_update  (void);
+void player__skate_im_gui       ( ui_context *ctx );
+void player__skate_animate      (void);
+void player__skate_pose         (void *animator, player_pose *pose);
+void player__skate_effects( void *_animator, m4x3f *final_mtx,
+                            struct player_board *board,
+                            struct player_effects_data *effect_data );
+void player__skate_post_animate (void);
+void player__skate_animator_exchange( bitpack_ctx *ctx, void *data );
+void player__skate_sfx_oneshot  ( u8 id, v3f pos, f32 volume );
+
+void player__skate_clear_mechanics(void);
+void player__skate_reset_animator(void);
+void player__approximate_best_trajectory(void);
+void player__skate_comp_audio( void *animator );
+void player__skate_kill_audio(void);
diff --git a/src/player_walk.c b/src/player_walk.c
new file mode 100644 (file)
index 0000000..1e15afc
--- /dev/null
@@ -0,0 +1,1210 @@
+#include "vg/vg_rigidbody_collision.h"
+
+#include "skaterift.h"
+#include "player_walk.h"
+#include "player_skate.h"
+#include "player_dead.h"
+#include "player.h"
+#include "input.h"
+#include "audio.h"
+#include "scene_rigidbody.h"
+
+struct player_walk player_walk;
+struct player_subsystem_interface player_subsystem_walk = 
+{
+   .system_register = player__walk_register,
+   .bind = player__walk_bind,
+   .pre_update = player__walk_pre_update,
+   .update = player__walk_update,
+   .post_update = player__walk_post_update,
+   .im_gui = player__walk_im_gui,
+   .animate = player__walk_animate,
+   .post_animate = player__walk_post_animate,
+   .pose = player__walk_pose,
+   .network_animator_exchange = player__walk_animator_exchange,
+   .sfx_oneshot = player__walk_sfx_oneshot,
+
+   .animator_data = &player_walk.animator,
+   .animator_size = sizeof(player_walk.animator),
+   .name = "Walk"
+};
+
+
+static void player_walk_drop_in_vector( v3f vec ){
+   v3f axis, init_dir;
+   v3_cross( (v3f){0.0f,1.0f,0.0f}, player_walk.state.drop_in_normal, axis );
+   v3_cross( axis, player_walk.state.drop_in_normal, init_dir );
+   v3_normalize( init_dir );
+   v3_muls( init_dir, 4.25f, vec );
+}
+
+static float player_xyspeed2(void){
+   return v3_length2( (v3f){localplayer.rb.v[0], 0.0f, localplayer.rb.v[2]} );
+}
+
+static void player_walk_generic_to_skate( enum skate_activity init, f32 yaw ){
+   localplayer.subsystem = k_player_subsystem_skate;
+
+   v3f v;
+
+   if( player_xyspeed2() < 0.1f * 0.1f )
+      q_mulv( localplayer.rb.q, (v3f){0.0f,0.0f,1.6f}, v );
+   else
+      v3_copy( localplayer.rb.v, v );
+
+   player_skate.state.activity_prev = k_skate_activity_ground;
+   player_skate.state.activity = init;
+
+   v3f dir;
+   v3_copy( v, dir );
+   v3_normalize( dir );
+
+   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
+                 atan2f(-dir[0],-dir[2]) );
+   q_normalize( localplayer.rb.q );
+
+   q_mulv( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, player_skate.state.cog );
+   v3_add( player_skate.state.cog, localplayer.rb.co, player_skate.state.cog );
+
+   v3_copy( v, player_skate.state.cog_v );
+   v3_copy( v, localplayer.rb.v );
+
+   player__begin_holdout( (v3f){0.0f,0.0f,0.0f} );
+   player__skate_reset_animator();
+   player__skate_clear_mechanics();
+   rb_update_matrices( &localplayer.rb );
+   v3_copy( (v3f){yaw,0.0f,0.0f}, player_skate.state.trick_euler );
+
+   if( init == k_skate_activity_air )
+      player__approximate_best_trajectory();
+}
+
+static void player_walk_drop_in_to_skate(void){
+   localplayer.immobile = 0;
+   localplayer.subsystem = k_player_subsystem_skate;
+
+   player_skate.state.activity_prev = k_skate_activity_ground;
+   player_skate.state.activity = k_skate_activity_ground;
+
+   player__begin_holdout( (v3f){0,0,0} );
+   player__skate_clear_mechanics();
+   player__skate_reset_animator();
+
+   v3f init_velocity;
+   player_walk_drop_in_vector( init_velocity );
+
+   rb_update_matrices( &localplayer.rb );
+   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 1.0f, 
+               player_skate.state.cog );
+   v3_copy( init_velocity, player_skate.state.cog_v );
+   v3_copy( init_velocity, localplayer.rb.v );
+   v3_copy( init_velocity, localplayer.cam_control.cam_velocity_smooth );
+   v3_copy( (v3f){player_walk.animator.board_yaw+1.0f,0,0}, 
+            player_skate.state.trick_euler );
+}
+
+static void player_walk_drop_in_overhang_transform( f32 t, v3f co, v4f q ){
+   v3f axis;
+   v3_cross( (v3f){0,1,0}, player_walk.state.drop_in_normal, axis );
+   v3_normalize( axis );
+
+   float a = acosf( player_walk.state.drop_in_normal[1] ) * t;
+   q_axis_angle( q, axis, a );
+
+   float l = t * 0.5f,
+         heading_angle = player_walk.state.drop_in_angle;
+
+   v3f overhang;
+   overhang[0] = sinf( heading_angle ) * l;
+   overhang[1] = 0.28f * l;
+   overhang[2] = cosf( heading_angle ) * l;
+
+   q_mulv( q, overhang, overhang );
+   v3_add( player_walk.state.drop_in_target, overhang, co );
+}
+
+static int player_walk_scan_for_drop_in(void){
+   world_instance *world = world_current_instance();
+
+   v3f dir, center;
+   q_mulv( localplayer.rb.q, (v3f){0.0f,0.0f,1.0f}, dir );
+   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], -1.0f, center );
+
+   ray_hit samples[20];
+   int sample_count = 0;
+
+   for( int i=0; i<20; i ++ ){
+      float t = (float)i * (1.0f/19.0f),
+            s = sinf( t * VG_PIf * 0.25f ),
+            c = cosf( t * VG_PIf * 0.25f );
+
+      v3f ray_dir, pos;
+      v3_muls   ( localplayer.rb.to_world[1], -c, ray_dir );
+      v3_muladds( ray_dir, dir, -s, ray_dir );
+      v3_muladds( center, ray_dir, -2.0f, pos );
+
+      ray_hit *ray = &samples[ sample_count ];
+      ray->dist = 2.0f;
+
+      if( ray_world( world, pos, ray_dir, ray, 0 ) ){
+         vg_line( pos, ray->pos, VG__RED );
+         vg_line_point( ray->pos, 0.025f, VG__BLACK );
+         
+         sample_count ++;
+      }
+   }
+
+   float min_a = 0.70710678118654752f;
+   ray_hit *candidate = NULL;
+
+   if( sample_count >= 2 ){
+      for( int i=0; i<sample_count-1; i++ ){
+         ray_hit *s0 = &samples[i],
+                 *s1 = &samples[i+1];
+
+         float a = v3_dot( s0->normal, s1->normal );
+
+         if( (a < min_a) && (a >= -0.1f) && (s0->normal[1]>s1->normal[1]) ){
+            min_a = a;
+            candidate = s0;
+         }
+      }
+   }
+
+   if( candidate ){
+      v4f pa, pb, pc;
+      
+      ray_hit *s0 = candidate,
+              *s1 = candidate+1;
+
+      vg_line( s0->pos, s1->pos, VG__WHITE );
+
+      v3_copy( s0->normal, pa );
+      v3_copy( s1->normal, pb );
+      v3_cross( localplayer.rb.to_world[1], dir, pc );
+      v3_normalize( pc );
+
+      pa[3] = v3_dot( pa, s0->pos );
+      pb[3] = v3_dot( pb, s1->pos );
+      pc[3] = v3_dot( pc, localplayer.rb.co );
+
+      v3f edge;
+      if( plane_intersect3( pa, pb, pc, edge ) ){
+         v3_copy( edge, player_walk.state.drop_in_target );
+         v3_copy( s1->normal, player_walk.state.drop_in_normal );
+         v3_copy( localplayer.rb.co, player_walk.state.drop_in_start );
+
+         player_walk.state.drop_in_start_angle = player_get_heading_yaw();
+         player_walk.state.drop_in_angle = 
+            atan2f( player_walk.state.drop_in_normal[0],
+                    player_walk.state.drop_in_normal[2] );
+
+         /* TODO: scan multiple of these? */
+         v3f oco;
+         v4f oq;
+         player_walk_drop_in_overhang_transform( 1.0f, oco, oq );
+
+         v3f va = {0.0f,0.0f,-k_board_length - 0.3f},
+             vb = {0.0f,0.0f, k_board_length + 0.3f};
+
+         q_mulv( oq, va, va );
+         q_mulv( oq, vb, vb );
+         v3_add( oco, va, va );
+         v3_add( oco, vb, vb );
+
+         v3f v0;
+         v3_sub( vb, va, v0 );
+         v3_normalize( v0 );
+
+         ray_hit ray;
+         ray.dist = k_board_length*2.0f + 0.6f;
+         
+         if( ray_world( world, va, v0, &ray, 0 ) ){
+            vg_line( va, vb, VG__RED );
+            vg_line_point( ray.pos, 0.1f, VG__RED );
+            vg_error( "invalidated\n" );
+            return 0;
+         }
+
+         v3_muls( v0, -1.0f, v0 );
+         if( ray_world( world, vb, v0, &ray, 0 ) ){
+            vg_line( va, vb, VG__RED );
+            vg_line_point( ray.pos, 0.1f, VG__RED );
+            vg_error( "invalidated\n" );
+            return 0;
+         }
+
+         player_walk_drop_in_vector( localplayer.rb.v );
+         return 1;
+      }
+      else{
+         vg_error( "failed to find intersection of drop in\n" );
+      }
+   }
+
+   return 0;
+}
+
+static bool player__preupdate_anim( struct skeleton_anim *anim, f32 *t, 
+                                    f32 speed ){
+   f32 length = (f32)(anim->length-1) / anim->rate;
+   *t += (vg.time_delta * speed) / length;
+
+   if( *t >= 1.0f ) return 1;
+   else             return 0;
+}
+
+static void player_walk_pre_sit(void){
+   struct player_walk *w = &player_walk;
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+
+   vg_slewf( &w->state.transition_t, 1.0f, vg.time_delta );
+
+   if( button_down(k_srbind_sit) || (v2_length2(steer)>0.2f) ||
+       button_down(k_srbind_jump) ){
+      w->state.activity = k_walk_activity_sit_up;
+   }
+   return;
+}
+
+static void player_walk_pre_sit_up(void){
+   struct player_walk *w = &player_walk;
+   
+   if( w->state.transition_t > 0.0f )
+      vg_slewf( &w->state.transition_t, 0.0f, vg.time_delta );
+   else 
+      w->state.activity = k_walk_activity_ground;
+
+   if( button_down(k_srbind_sit) )
+      w->state.activity = k_walk_activity_sit;
+
+   return;
+}
+
+static void player_walk_pre_ground(void){
+   struct player_walk *w = &player_walk;
+
+   if( button_down(k_srbind_sit) ){
+      v3_zero( localplayer.rb.v );
+      w->state.activity = k_walk_activity_sit;
+      w->state.transition_t = 0.0f;
+      return;
+   }
+
+   if( button_down( k_srbind_use ) ){
+      if( player_walk_scan_for_drop_in() ){
+         w->state.activity = k_walk_activity_odrop_in;
+      }
+      else{
+         w->state.activity = k_walk_activity_oregular;
+      }
+
+      w->state.transition_t = 0.0f;
+   }
+
+   if( button_down( k_srbind_jump ) ){
+      w->state.jump_queued = 1;
+      w->state.jump_input_time = vg.time;
+   }
+}
+
+static void player_walk_pre_air(void){
+   struct player_walk *w = &player_walk;
+   if( button_down( k_srbind_use ) ){
+      w->state.activity = k_walk_activity_oair;
+      w->state.transition_t = 0.0f;
+   }
+
+   if( button_down( k_srbind_jump ) ){
+      w->state.jump_queued = 1;
+      w->state.jump_input_time = vg.time;
+   }
+}
+
+static void player_walk_pre_drop_in(void){
+   struct player_walk *w = &player_walk;
+   bool finished = player__preupdate_anim( w->anim_drop_in, 
+                                          &w->state.transition_t, 1.0f );
+   if( finished )
+      player_walk_drop_in_to_skate();
+}
+
+static void player_walk_pre_caveman(void){
+   struct player_walk *w = &player_walk;
+   bool finished = player__preupdate_anim( w->anim_jump_to_air,
+                                          &w->state.transition_t, 1.0f );
+   if( finished ){
+      player_walk_generic_to_skate( k_skate_activity_air, 
+                                    player_walk.animator.board_yaw );
+   }
+}
+
+static void player_walk_pre_running_start(void){
+   struct player_walk *w = &player_walk;
+   bool finished = player__preupdate_anim( w->anim_intro, 
+                                          &w->state.transition_t, 1.0f );
+   if( finished ){
+      /* TODO: get the derivative of the last keyframes to calculate new
+       * velocity for player */
+      player_walk_generic_to_skate( k_skate_activity_ground, 
+                                    player_walk.animator.board_yaw+1.0f );
+   }
+}
+
+static void player_walk_pre_popoff(void){
+   struct player_walk *w = &player_walk;
+   bool finished = player__preupdate_anim( w->anim_popoff, 
+                                          &w->state.transition_t, 1.0f );
+
+   if( finished ){
+      w->state.activity = k_walk_activity_ground;
+      w->animator.board_yaw += 1.0f;
+   }
+}
+
+void player__walk_pre_update(void){
+   struct player_walk *w = &player_walk;
+
+   if( localplayer.immobile ) return;
+   else player_look( localplayer.angles, skaterift.time_rate );
+
+   enum walk_activity a = w->state.activity;
+
+   if     ( a == k_walk_activity_sit )       player_walk_pre_sit();
+   else if( a == k_walk_activity_sit_up )    player_walk_pre_sit_up();
+   else if( a == k_walk_activity_ground )    player_walk_pre_ground();
+   else if( a == k_walk_activity_air )       player_walk_pre_air();
+   else if( a == k_walk_activity_odrop_in )  player_walk_pre_drop_in();
+   else if( a == k_walk_activity_oair )      player_walk_pre_caveman();
+   else if( a == k_walk_activity_oregular )  player_walk_pre_running_start();
+   else if( a == k_walk_activity_ipopoff )   player_walk_pre_popoff();
+}
+
+static int player_walk_normal_standable( v3f n ){
+   return n[1] > 0.70710678118f;
+}
+
+static void player_accelerate( v3f v, v3f movedir, f32 speed, f32 accel ){
+   float currentspeed = v3_dot( v, movedir ),
+         addspeed     = speed - currentspeed;
+
+   if( addspeed <= 0 )
+      return;
+
+   float accelspeed = accel * vg.time_fixed_delta * speed;
+
+   if( accelspeed > addspeed )
+      accelspeed = addspeed;
+
+   v3_muladds( v, movedir, accelspeed, v );
+}
+
+static void player_friction( v3f v, f32 friction ){
+   float speed = v3_length( v ),
+         drop  = 0.0f,
+         control = vg_maxf( speed, k_stopspeed );
+
+   if( speed < 0.04f )
+      return;
+
+   drop += control * friction * vg.time_fixed_delta;
+
+   float newspeed = vg_maxf( 0.0f, speed - drop );
+   newspeed /= speed;
+
+   v3_muls( v, newspeed, v );
+}
+
+static void player_walk_custom_filter( world_instance *world,
+                                          rb_ct *man, int len, f32 w ){
+   for( int i=0; i<len; i++ ){
+      rb_ct *ci = &man[i];
+      if( ci->type == k_contact_type_disabled ||
+          ci->type == k_contact_type_edge ) 
+         continue;
+
+
+      float d1 = v3_dot( ci->co, ci->n );
+
+      for( int j=0; j<len; j++ ){
+         if( j == i )
+            continue;
+
+         rb_ct *cj = &man[j];
+         if( cj->type == k_contact_type_disabled ) 
+            continue;
+
+         struct world_surface *si = world_contact_surface( world, ci ),
+                              *sj = world_contact_surface( world, cj );
+
+         if(  (sj->info.flags & k_material_flag_walking) &&
+             !(si->info.flags & k_material_flag_walking)){
+            continue;
+         }
+         
+         float d2 = v3_dot( cj->co, ci->n ),
+               d  = d2-d1;
+
+         if( fabsf( d ) <= w ){
+            cj->type = k_contact_type_disabled;
+         }
+      }
+   }
+}
+
+static void player_walk_update_generic(void){
+   struct player_walk *w = &player_walk;
+
+   if( (w->state.activity != k_walk_activity_oregular) &&
+       (w->state.activity != k_walk_activity_oair) ){
+      joystick_state( k_srjoystick_steer, w->state.steer );
+      w->state.steer[2] = button_press(k_srbind_run)? k_runspeed: k_walkspeed;
+      if( v2_length2(w->state.steer)>1.0f )
+         v2_normalize(w->state.steer);
+   }
+
+   v3_copy( localplayer.rb.co, w->state.prev_pos );
+   v3_zero( localplayer.rb.w );
+
+   world_instance *world = world_current_instance();
+   if( !world_water_player_safe( world, 0.4f ) ) return;
+
+   enum walk_activity prev_state = w->state.activity;
+
+   w->collider.h = 2.0f;
+   w->collider.r = 0.3f;
+
+   m4x3f mtx;
+   m3x3_copy( localplayer.rb.to_world, mtx );
+   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
+
+   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__WHITE );
+
+   rb_ct manifold[64];
+   int len;
+
+   float yaw = localplayer.angles[0];
+
+   v3f forward_dir = { -sinf(yaw),         0.0f,  cosf(yaw) };
+   v3f right_dir   = {  forward_dir[2],    0.0f, -forward_dir[0] };
+
+   /* 
+    * Collision detection
+    */
+
+   len = rb_capsule__scene( mtx, &w->collider, NULL, 
+                            world->geo_bh, manifold, 0 );
+   player_walk_custom_filter( world, manifold, len, 0.01f );
+   len = rb_manifold_apply_filtered( manifold, len );
+
+   v3f surface_avg = { 0.0f, 0.0f, 0.0f };
+
+   w->state.activity = k_walk_activity_air;
+   w->surface = k_surface_prop_concrete;
+
+   for( int i=0; i<len; i++ ){
+      rb_ct *ct = &manifold[i];
+      rb_debug_contact( ct );
+
+      if( player_walk_normal_standable( ct->n ) ){
+         w->state.activity = k_walk_activity_ground;
+
+         v3_add( surface_avg, ct->n, surface_avg );
+
+         struct world_surface *surf = world_contact_surface( world, ct );
+         if( surf->info.surface_prop > w->surface )
+            w->surface = surf->info.surface_prop;
+      }
+
+      rb_prepare_contact( ct, vg.time_fixed_delta );
+   }
+
+   /* 
+    * Move & Friction
+    */
+   float accel_speed = 0.0f, nominal_speed = 0.0f;
+   v3f movedir;
+
+   v3_muls( right_dir, w->state.steer[0], movedir );
+   v3_muladds( movedir, forward_dir, w->state.steer[1], movedir );
+
+   if( w->state.activity == k_walk_activity_ground ){
+      v3_normalize( surface_avg );
+
+      v3f tx, ty;
+      v3_tangent_basis( surface_avg, tx, ty );
+
+      if( v2_length2(w->state.steer) > 0.001f ){
+         /* clip movement to the surface */
+         float d = v3_dot(surface_avg,movedir);
+         v3_muladds( movedir, surface_avg, -d, movedir );
+      }
+
+      accel_speed = k_walk_accel;
+      nominal_speed = w->state.steer[2];
+
+      /* jump */
+      if( w->state.jump_queued ){
+         w->state.jump_queued = 0;
+
+         f32 t = vg.time - w->state.jump_input_time;
+         if( t < PLAYER_JUMP_EPSILON ){
+            localplayer.rb.v[1] = 5.0f;
+            w->state.activity = k_walk_activity_air;
+            prev_state = k_walk_activity_air;
+            accel_speed = k_walk_air_accel;
+            nominal_speed = k_airspeed;
+         }
+      }
+      else{
+         player_friction( localplayer.rb.v, k_walk_friction );
+      }
+   }
+   else{
+      accel_speed = k_walk_air_accel;
+      nominal_speed = k_airspeed;
+   }
+
+   if( v2_length2( w->state.steer ) > 0.001f ){
+      player_accelerate( localplayer.rb.v, movedir, 
+                         nominal_speed, accel_speed );
+      v3_normalize( movedir );
+   }
+   
+   /*
+    * Resolve velocity constraints
+    */
+   for( int j=0; j<5; j++ ){
+      for( int i=0; i<len; i++ ){
+         rb_ct *ct = &manifold[i];
+         
+         /*normal */
+         float vn = -v3_dot( localplayer.rb.v, ct->n );
+
+         float temp = ct->norm_impulse;
+         ct->norm_impulse = vg_maxf( temp + vn, 0.0f );
+         vn = ct->norm_impulse - temp;
+
+         v3_muladds( localplayer.rb.v, ct->n, vn, localplayer.rb.v );
+      }
+   }
+
+   /* stepping */
+   if( w->state.activity == k_walk_activity_ground||
+       prev_state == k_walk_activity_ground ){
+      float max_dist = 0.4f;
+
+      v3f pa, pb;
+      v3_copy( localplayer.rb.co, pa );
+      pa[1] += w->collider.r + max_dist;
+      v3_add( pa, (v3f){0, -max_dist * 2.0f, 0}, pb );
+      vg_line( pa, pb, 0xff000000 );
+
+      v3f n;
+      float t;
+      if( spherecast_world( world, pa, pb, 
+                            w->collider.r, &t, n, 0 ) != -1 ){
+         if( player_walk_normal_standable(n) ){
+            v3_lerp( pa, pb, t, localplayer.rb.co );
+            localplayer.rb.co[1] += -w->collider.r - k_penetration_slop;
+            w->state.activity = k_walk_activity_ground;
+
+            float d = -v3_dot(n,localplayer.rb.v);
+            v3_muladds( localplayer.rb.v, n, d, localplayer.rb.v );
+            localplayer.rb.v[1] += -k_gravity * vg.time_fixed_delta;
+         }
+      }
+   }
+
+   /* 
+    * Depenetrate
+    */
+   v3f dt;
+   rb_depenetrate( manifold, len, dt );
+   v3_add( dt, localplayer.rb.co, localplayer.rb.co );
+
+   /* integrate */
+   if( w->state.activity == k_walk_activity_air ){
+      localplayer.rb.v[1] += -k_gravity*vg.time_fixed_delta;
+   }
+
+   if( localplayer.immobile ){
+      localplayer.rb.v[0] = 0.0f;
+      localplayer.rb.v[2] = 0.0f;
+   }
+
+   v3_muladds( localplayer.rb.co, localplayer.rb.v, vg.time_fixed_delta, 
+               localplayer.rb.co );
+   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
+   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__GREEN );
+
+   /* 
+    * CCD routine 
+    * ---------------------------------------------------
+    *
+    */
+   v3f lwr_prev,
+       lwr_now,
+       lwr_offs = { 0.0f, w->collider.r, 0.0f };
+
+   v3_add( lwr_offs, w->state.prev_pos, lwr_prev );
+   v3_add( lwr_offs, localplayer.rb.co, lwr_now );
+
+   v3f movedelta;
+   v3_sub( localplayer.rb.co, w->state.prev_pos, movedelta );
+
+   float movedist = v3_length( movedelta );
+
+   if( movedist > 0.3f ){
+      float t, sr = w->collider.r-0.04f;
+      v3f n;
+
+      if( spherecast_world( world, lwr_prev, lwr_now, sr, &t, n, 0 ) != -1 ){
+         v3_lerp( lwr_prev, lwr_now, vg_maxf(0.01f,t), localplayer.rb.co );
+         localplayer.rb.co[1] -= w->collider.r;
+         rb_update_matrices( &localplayer.rb );
+         v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
+         vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__RED);
+      }
+   }
+
+   u32 id = world_intersect_gates(world, localplayer.rb.co, w->state.prev_pos);
+   if( id ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, mdl_entity_id_id(id) );
+      m4x3_mulv( gate->transport, localplayer.rb.co, localplayer.rb.co );
+      m3x3_mulv( gate->transport, localplayer.rb.v,  localplayer.rb.v );
+
+      v4f transport_rotation;
+      m3x3_q( gate->transport, transport_rotation );
+      q_mul( transport_rotation, localplayer.rb.q, localplayer.rb.q );
+      q_normalize( localplayer.rb.q );
+      rb_update_matrices( &localplayer.rb );
+      player__pass_gate( id );
+   }
+   rb_update_matrices( &localplayer.rb );
+
+   if( (prev_state == k_walk_activity_oregular) ||
+       (prev_state == k_walk_activity_oair) ||
+       (prev_state == k_walk_activity_ipopoff) ){
+      w->state.activity = prev_state;
+   }
+
+   w->move_speed = vg_minf( v2_length( (v2f){ localplayer.rb.v[0], 
+                                              localplayer.rb.v[2] } ),
+                            k_runspeed );
+}
+
+void player__walk_post_update(void){
+   struct player_walk *w = &player_walk;
+
+   m4x3f mtx;
+   m3x3_copy( localplayer.rb.to_world, mtx );
+   v3_add( localplayer.rb.co, (v3f){0,1,0}, mtx[3] );
+
+   float substep = vg.time_fixed_extrapolate;
+   v3_muladds( mtx[3], localplayer.rb.v, vg.time_fixed_delta*substep, mtx[3] );
+   vg_line_capsule( mtx, w->collider.r, w->collider.h, VG__YELOW );
+
+   /* Calculate header */
+   v3f v;
+   if( (player_xyspeed2() > 0.1f*0.1f) ){
+      f32 r = 0.3f;
+      if( (w->state.activity == k_walk_activity_ground) ||
+          (w->state.activity == k_walk_activity_ipopoff) ||
+          (w->state.activity == k_walk_activity_oregular) ){
+         r = 0.07f;
+      }
+
+      f32 ta = atan2f( localplayer.rb.v[0], localplayer.rb.v[2] );
+      v4f qt;
+      q_axis_angle( qt, (v3f){0,1,0}, ta );
+      q_nlerp( localplayer.rb.q, qt, vg.time_delta/r, localplayer.rb.q );
+   }
+
+   vg_line_point( w->state.drop_in_target, 0.1f, VG__GREEN );
+   v3f p1;
+   v3_muladds( w->state.drop_in_target, w->state.drop_in_normal, 0.3f, p1 );
+   vg_line( w->state.drop_in_target, p1, VG__GREEN );
+   v3_muladds( w->state.drop_in_target, localplayer.rb.to_world[1], 0.3f, p1 );
+   vg_line( w->state.drop_in_target, p1, VG__GREEN );
+
+   float a = player_get_heading_yaw();
+   p1[0] = sinf( a );
+   p1[1] = 0.0f;
+   p1[2] = cosf( a );
+
+   v3_add( localplayer.rb.co, p1, p1 );
+   vg_line( localplayer.rb.co, p1, VG__PINK );
+
+   int walk_phase = 0;
+   if( vg_fractf(w->state.walk_timer) > 0.5f )
+      walk_phase = 1;
+   else
+      walk_phase = 0;
+
+   if( (w->state.step_phase != walk_phase) && 
+       (w->state.activity == k_walk_activity_ground ) )
+   {
+      audio_lock();
+      if( w->surface == k_surface_prop_concrete ){
+         audio_oneshot_3d( 
+               &audio_footsteps[vg_randu32(&vg.rand) % 4],
+               localplayer.rb.co, 40.0f, 1.0f 
+         );
+      }
+      else if( w->surface == k_surface_prop_grass ){
+         audio_oneshot_3d( 
+           &audio_footsteps_grass[ vg_randu32(&vg.rand) % 6 ],
+            localplayer.rb.co, 40.0f, 1.0f 
+         );
+      }
+      else if( w->surface == k_surface_prop_wood ){
+         audio_oneshot_3d( 
+           &audio_footsteps_wood[ vg_randu32(&vg.rand) % 6 ],
+            localplayer.rb.co, 40.0f, 1.0f 
+         );
+      }
+      audio_unlock();
+   }
+
+   w->state.step_phase = walk_phase;
+}
+
+void player__walk_update(void){
+   struct player_walk *w = &player_walk;
+
+   if( (w->state.activity == k_walk_activity_air) ||
+       (w->state.activity == k_walk_activity_ground) ||
+       (w->state.activity == k_walk_activity_oair) ||
+       (w->state.activity == k_walk_activity_oregular) ||
+       (w->state.activity == k_walk_activity_ipopoff) ){
+      player_walk_update_generic();
+   }
+}
+
+static void player_walk_animate_drop_in(void){
+   struct player_walk *w = &player_walk;
+   struct player_walk_animator *animator = &w->animator;
+   struct skeleton_anim *anim = w->anim_drop_in;
+
+   f32 length = (f32)(anim->length-1) / anim->rate,
+       time   = w->state.transition_t;
+
+   f32 walk_yaw = vg_alerpf( w->state.drop_in_start_angle, 
+                             w->state.drop_in_angle, animator->transition_t );
+   v3_lerp( w->state.drop_in_start, w->state.drop_in_target,
+            animator->transition_t, localplayer.rb.co );
+
+   q_axis_angle( localplayer.rb.q, (v3f){0,1,0}, walk_yaw + VG_PIf );
+
+   /* the drop in bit */
+   v3f final_co;
+   v4f final_q;
+   player_walk_drop_in_overhang_transform( animator->transition_t, 
+                                           final_co, final_q );
+
+   q_mul( final_q, localplayer.rb.q, localplayer.rb.q );
+   v3_lerp( localplayer.rb.co, final_co, animator->transition_t, 
+            localplayer.rb.co );
+
+   rb_update_matrices( &localplayer.rb );
+
+   v3_muladds( localplayer.rb.co, localplayer.rb.to_world[1], 
+               -0.1f*animator->transition_t, localplayer.rb.co );
+
+   v3_copy( localplayer.rb.co, animator->root_co );
+   v4_copy( localplayer.rb.q, animator->root_q );
+
+   /* for the camera purposes only */
+   v3f init_velocity;
+   player_walk_drop_in_vector( init_velocity );
+   v3_muls( init_velocity, animator->transition_t, localplayer.rb.v );
+   v3_copy( localplayer.rb.v, 
+            localplayer.cam_control.cam_velocity_smooth );
+}
+
+static void player_walk_animate_generic(void){
+   struct player_walk *w = &player_walk;
+   struct player_walk_animator *animator = &w->animator;
+
+   v4f _null;
+   rb_extrapolate( &localplayer.rb, animator->root_co, _null );
+
+   f32 walk_yaw = player_get_heading_yaw(),
+       head_yaw = localplayer.angles[0] + VG_PIf,
+       y = vg_angle_diff( head_yaw, -walk_yaw ),
+       p = vg_clampf( localplayer.angles[1], 
+                        -k_sit_pitch_limit, k_sit_pitch_limit );
+
+   if( fabsf(y) > k_sit_yaw_limit ){
+      y = 0.0f;
+      p = 0.0f;
+   }
+
+   animator->yaw = vg_lerpf( animator->yaw, y, vg.time_delta*2.0f );
+   animator->pitch = vg_lerpf( animator->pitch, p, vg.time_delta*2.8f );
+   q_axis_angle( animator->root_q, (v3f){0,1,0}, walk_yaw + VG_PIf );
+
+   v4f qrev;
+   q_axis_angle( qrev, (v3f){0,1,0}, VG_TAUf*0.5f );
+   q_mul( localplayer.rb.q, qrev, animator->root_q );
+}
+
+void player__walk_animate(void){
+   struct player_walk *w = &player_walk;
+   player_pose *pose = &localplayer.pose;
+   struct player_walk_animator *animator = &w->animator;
+
+   animator->activity = w->state.activity;
+   animator->transition_t = w->state.transition_t;
+
+   {
+      f32 fly = (w->state.activity == k_walk_activity_air)? 1.0f: 0.0f, 
+          rate;
+
+      if( w->state.activity == k_walk_activity_air ) rate = 2.4f;
+      else rate = 9.0f;
+
+      animator->fly = vg_lerpf( animator->fly, fly, rate*vg.time_delta );
+      animator->run = vg_lerpf( animator->run, w->move_speed, 
+                                8.0f*vg.time_delta);
+   }
+
+   if( animator->run > 0.025f ){
+      f32 walk_norm = 30.0f/(float)w->anim_walk->length,
+          run_norm  = 30.0f/(float)w->anim_run->length,
+          l;
+
+      if( animator->run <= k_walkspeed )
+         l = (animator->run / k_walkspeed) * walk_norm;
+      else {
+         l = vg_lerpf( walk_norm, run_norm, 
+                      (animator->run-k_walkspeed) / (k_runspeed-k_walkspeed) );
+      }
+      w->state.walk_timer += l * vg.time_delta;
+   }
+   else
+      w->state.walk_timer = 0.0f;
+
+   animator->walk_timer = w->state.walk_timer;
+
+   player_walk_animate_generic();
+   if( w->state.activity == k_walk_activity_odrop_in ){
+      player_walk_animate_drop_in();
+   }
+
+   if( (w->state.activity == k_walk_activity_odrop_in) ||
+       (w->state.activity == k_walk_activity_oregular) ||
+       (w->state.activity == k_walk_activity_oair) ){
+      localplayer.cam_velocity_influence = w->animator.transition_t;
+   }
+   else if( w->state.activity == k_walk_activity_ipopoff ){
+      localplayer.cam_velocity_influence = 1.0f-w->animator.transition_t;
+   }
+   else
+      localplayer.cam_velocity_influence = 0.0f;
+
+   if( w->state.activity == k_walk_activity_sit ){
+      localplayer.cam_dist = 3.8f;
+   }
+   else {
+      localplayer.cam_dist = 1.8f;
+   }
+}
+
+static void player_walk_pose_sit( struct player_walk_animator *animator,
+                                  player_pose *pose )
+{
+   mdl_keyframe bpose[32];
+
+   struct player_walk *w = &player_walk;
+   struct skeleton *sk = &localplayer.skeleton;
+
+   f32 t  = animator->transition_t,
+       st = t * ((f32)(w->anim_sit->length-1)/30.0f);
+   skeleton_sample_anim( sk, w->anim_sit, st, bpose );
+
+   v4f qy,qp;
+   f32 *qh = bpose[localplayer.id_head-1].q;
+   q_axis_angle( qy, (v3f){0,1,0}, animator->yaw*0.5f*t );
+   q_axis_angle( qp, (v3f){0,0,1}, animator->pitch*t );
+   q_mul( qy, qh, qh );
+   q_mul( qh, qp, qh );
+   q_normalize( qh );
+
+   qh = bpose[localplayer.id_chest-1].q;
+   q_axis_angle( qy, (v3f){0,1,0}, animator->yaw*0.5f*t );
+   q_mul( qy, qh, qh );
+   q_normalize( qh );
+
+   skeleton_lerp_pose( sk, pose->keyframes, bpose, 
+                       vg_minf(1.0f,t*10.0f), pose->keyframes );
+}
+
+enum walk_transition_type {
+   k_walk_transition_in,
+   k_walk_transition_out,
+   k_walk_transition_outin,
+};
+
+static void player_walk_pose_transition( 
+      struct player_walk_animator *animator, struct skeleton_anim *anim,
+      enum walk_transition_type type,
+      mdl_keyframe apose[32], f32 *mask, player_pose *pose ){
+
+   mdl_keyframe bpose[32];
+
+   struct player_walk *w = &player_walk;
+   struct skeleton *sk = &localplayer.skeleton;
+   
+   f32 length   = (f32)(anim->length-1) / anim->rate,
+       t        = animator->transition_t * length,
+       blend    = 1.0f;
+
+   if( type == k_walk_transition_in || type == k_walk_transition_outin )
+      blend = vg_minf( blend, length-t );
+
+   if( type == k_walk_transition_out || type == k_walk_transition_outin )
+      blend = vg_minf( blend, t );
+
+   blend = vg_smoothstepf( vg_minf(1,blend/k_anim_transition) );
+
+   skeleton_sample_anim_clamped( sk, anim, t, bpose );
+
+   mdl_keyframe *kf_board = &bpose[localplayer.id_board-1];
+   f32 yaw = animator->board_yaw * VG_TAUf * 0.5f;
+
+   v4f qyaw;
+   q_axis_angle( qyaw, (v3f){0,1,0}, yaw );
+   q_mul( kf_board->q, qyaw, kf_board->q );
+   q_normalize( kf_board->q );
+
+   if( mask ){
+      for( i32 i=0; i<sk->bone_count-1; i ++ )
+         keyframe_lerp( apose+i, bpose+i, blend*mask[i], pose->keyframes+i );
+   }
+   else
+      skeleton_lerp_pose( sk, apose, bpose, blend, pose->keyframes );
+}
+
+void player__walk_pose( void *_animator, player_pose *pose ){
+   struct player_walk *w = &player_walk;
+   struct player_walk_animator *animator = _animator;
+   struct skeleton *sk = &localplayer.skeleton;
+
+   v3_copy( animator->root_co, pose->root_co );
+   v4_copy( animator->root_q, pose->root_q );
+   pose->board.lean = 0.0f;
+   pose->type = k_player_pose_type_ik;
+
+   float walk_norm = (float)w->anim_walk->length/30.0f,
+         run_norm  = (float)w->anim_run->length/30.0f,
+         t = animator->walk_timer;
+
+   /* walk/run */
+   mdl_keyframe apose[32], bpose[32];
+   if( animator->run <= k_walkspeed ){ 
+      /* walk / idle */
+      f32 l = vg_minf( 1, (animator->run/k_walkspeed)*6.0f );
+      skeleton_sample_anim( sk, w->anim_idle, vg.time*0.1f, apose );
+      skeleton_sample_anim( sk, w->anim_walk, t*walk_norm, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, l, apose );
+   }
+   else { 
+      /* walk / run */
+      f32 l = (animator->run-k_walkspeed) / (k_runspeed-k_walkspeed);
+      skeleton_sample_anim( sk, w->anim_walk, t*walk_norm, apose );
+      skeleton_sample_anim( sk, w->anim_run, t*run_norm, bpose );
+      skeleton_lerp_pose( sk, apose, bpose, l, apose );
+   }
+
+   /* air */
+   skeleton_sample_anim( sk, w->anim_jump, vg.time*0.6f, bpose );
+   skeleton_lerp_pose( sk, apose, bpose, animator->fly, apose );
+
+   mdl_keyframe *kf_board = &apose[localplayer.id_board-1];
+   f32 yaw = animator->board_yaw;
+
+   if( animator->activity == k_walk_activity_ipopoff )
+      if( animator->transition_t > 0.5f )
+         yaw += 1.0f;
+
+   v4f qyaw;
+   q_axis_angle( qyaw, (v3f){0,1,0}, yaw * VG_TAUf * 0.5f );
+   q_mul( kf_board->q, qyaw, kf_board->q );
+   q_normalize( kf_board->q );
+
+   /* sit */
+   if( (animator->activity == k_walk_activity_sit) ||
+       (animator->activity == k_walk_activity_sit_up) )
+   {
+      skeleton_copy_pose( sk, apose, pose->keyframes );
+      player_walk_pose_sit( animator, pose );
+   }
+   else if( animator->activity == k_walk_activity_odrop_in ){
+      player_walk_pose_transition( 
+            animator, w->anim_drop_in, k_walk_transition_out, apose, 
+            NULL, pose );
+   }
+   else if( animator->activity == k_walk_activity_oair ){
+      player_walk_pose_transition(
+            animator, w->anim_jump_to_air, k_walk_transition_out, apose, 
+            NULL, pose );
+   }
+   else if( animator->activity == k_walk_activity_oregular ){
+      player_walk_pose_transition( 
+            animator, w->anim_intro, k_walk_transition_out, apose, 
+            NULL, pose );
+   }
+   else if( animator->activity == k_walk_activity_ipopoff ){
+      if( animator->run > 0.2f ){
+         f32 t = 1.0f-vg_minf( animator->run-0.2f, 1.0f ),
+             mask[ 32 ];
+
+         for( u32 i=0; i<32; i ++ ) 
+            mask[i] = 1.0f;
+
+         mask[ localplayer.id_ik_foot_l-1 ] = t;
+         mask[ localplayer.id_ik_foot_r-1 ] = t;
+         mask[ localplayer.id_ik_knee_l-1 ] = t;
+         mask[ localplayer.id_ik_knee_r-1 ] = t;
+         mask[ localplayer.id_hip-1 ] = t;
+         player_walk_pose_transition( 
+               animator, w->anim_popoff, k_walk_transition_in, apose,
+               mask, pose );
+      }
+      else{
+         player_walk_pose_transition( 
+               animator, w->anim_popoff, k_walk_transition_in, apose,
+               NULL, pose );
+      }
+   }
+   else {
+      skeleton_copy_pose( sk, apose, pose->keyframes );
+   }
+}
+
+void player__walk_post_animate(void){
+   /* 
+    * Camera 
+    */
+   struct player_walk *w = &player_walk;
+
+}
+
+void player__walk_im_gui( ui_context *ctx )
+{
+   struct player_walk *w = &player_walk;
+   player__debugtext( ctx, 1, "V:  %5.2f %5.2f %5.2f (%5.2fm/s)",
+      localplayer.rb.v[0], localplayer.rb.v[1], localplayer.rb.v[2],
+      v3_length(localplayer.rb.v) );
+   player__debugtext( ctx,
+                      1, "CO: %5.2f %5.2f %5.2f",localplayer.rb.co[0],
+                                                 localplayer.rb.co[1],
+                                                 localplayer.rb.co[2] );
+   player__debugtext( ctx, 1, "transition: %5.2f ", w->state.transition_t );
+   player__debugtext( ctx, 1, "activity: %s\n",
+                           (const char *[]){ "air",
+                                             "ground",
+                                             "sit",
+                                             "sit_up",
+                                             "inone",
+                                             "ipopoff",
+                                             "oair",
+                                             "odrop_in",
+                                             "oregular" }
+                                             [w->state.activity] );
+   player__debugtext( ctx, 1, "surface: %s\n",
+                           (const char *[]){ "concrete",
+                                             "wood",
+                                             "grass",
+                                             "tiles",
+                                             "metal",
+                                             "snow",
+                                             "sand" }
+                                             [w->surface] );
+}
+
+void player__walk_bind(void){
+   struct player_walk *w = &player_walk;
+   struct skeleton *sk = &localplayer.skeleton;
+
+   w->anim_idle         = skeleton_get_anim( sk, "idle_cycle+y" );
+   w->anim_walk         = skeleton_get_anim( sk, "walk+y" );
+   w->anim_run          = skeleton_get_anim( sk, "run+y" );
+   w->anim_jump         = skeleton_get_anim( sk, "jump+y" );
+   w->anim_jump_to_air  = skeleton_get_anim( sk, "jump_to_air" );
+   w->anim_drop_in      = skeleton_get_anim( sk, "drop_in" );
+   w->anim_intro        = skeleton_get_anim( sk, "into_skate" );
+   w->anim_sit          = skeleton_get_anim( sk, "sit" );
+   w->anim_popoff       = skeleton_get_anim( sk, "pop_off_short" );
+}
+
+void player__walk_transition( bool grounded, f32 board_yaw ){
+   struct player_walk *w = &player_walk;
+   w->state.activity = k_walk_activity_air;
+
+   if( grounded ){
+      w->state.activity = k_walk_activity_ipopoff;
+   }
+
+   w->state.transition_t = 0.0f;
+   w->state.jump_queued = 0;
+   w->state.jump_input_time = 0.0;
+   w->state.walk_timer = 0.0f;
+   w->state.step_phase = 0;
+   w->animator.board_yaw = fmodf( board_yaw, 2.0f );
+   rb_update_matrices( &localplayer.rb );
+}
+
+void player__walk_reset(void)
+{
+   struct player_walk *w = &player_walk;
+   w->state.activity = k_walk_activity_air;
+   w->state.transition_t = 0.0f;
+
+   v3f fwd = { 0.0f, 0.0f, 1.0f };
+   q_mulv( localplayer.rb.q, fwd, fwd );
+   q_axis_angle( localplayer.rb.q, (v3f){0.0f,1.0f,0.0f}, 
+                 atan2f(fwd[0], fwd[2]) );
+
+   rb_update_matrices( &localplayer.rb );
+}
+
+void player__walk_animator_exchange( bitpack_ctx *ctx, void *data ){
+   struct player_walk_animator *animator = data;
+
+   bitpack_qv3f( ctx, 24, -1024.0f, 1024.0f, animator->root_co );
+   bitpack_qquat( ctx, animator->root_q );
+   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->fly );
+   bitpack_qf32( ctx, 8, 0.0f, k_runspeed, &animator->run );
+   bitpack_qf32( ctx, 16, 0.0f, 120.0f, &animator->walk_timer );
+
+   for( int i=0; i<1; i++ ){ /* without this you get a warning from gcc. lol */
+      bitpack_bytes( ctx, 8, &animator->activity );
+   }
+
+   bitpack_qf32( ctx, 8, 0.0f, 1.0f, &animator->transition_t );
+
+   if( (animator->activity == k_walk_activity_sit) || 
+       (animator->activity == k_walk_activity_sit_up) ){
+      bitpack_qf32( ctx, 8, -k_sit_yaw_limit, k_sit_yaw_limit, &animator->yaw );
+      bitpack_qf32( ctx, 8, -k_sit_pitch_limit, k_sit_pitch_limit, 
+                     &animator->pitch );
+   }
+
+   bitpack_qf32( ctx, 16, -100.0f, 100.0f, &animator->board_yaw );
+}
+
+void player__walk_sfx_oneshot( u8 id, v3f pos, f32 volume )
+{
+   audio_lock();
+
+   if( id == k_player_walk_soundeffect_splash ){
+      audio_oneshot_3d( &audio_splash, pos, 40.0f, 1.0f );
+   }
+
+   audio_unlock();
+}
diff --git a/src/player_walk.h b/src/player_walk.h
new file mode 100644 (file)
index 0000000..5da0350
--- /dev/null
@@ -0,0 +1,113 @@
+#pragma once
+#include "player.h"
+#include "player_api.h"
+#include "vg/vg_rigidbody.h"
+
+#define PLAYER_JUMP_EPSILON 0.1     /* 100ms jump allowance */
+
+struct player_walk
+{
+   rb_capsule collider;
+
+   struct player_walk_state{
+      v3f prev_pos;
+      v3f drop_in_target,
+          drop_in_start,
+          drop_in_normal;
+
+      float drop_in_start_angle,
+            drop_in_angle;
+
+      enum walk_activity{
+         k_walk_activity_air,
+         k_walk_activity_ground,
+         k_walk_activity_sit,
+         k_walk_activity_sit_up,
+
+         /* transitions */
+         k_walk_activity_inone,
+         k_walk_activity_ipopoff,
+         k_walk_activity_oair,
+         k_walk_activity_odrop_in,
+         k_walk_activity_oregular,
+
+         k_walk_activity_max,
+      }
+      activity;
+
+      f32 transition_t;
+
+      int jump_queued;
+      f64 jump_input_time;
+
+      f32 walk_timer;
+      int step_phase;
+      v3f steer;
+   }
+   state;
+
+   f32 move_speed;
+
+   enum mdl_surface_prop surface;
+   struct skeleton_anim *anim_walk, *anim_run, *anim_idle, *anim_jump,
+                        *anim_jump_to_air, *anim_drop_in, *anim_intro,
+                        *anim_sit, *anim_popoff;
+
+   struct player_walk_animator {
+      v3f root_co;
+      v4f root_q;
+      f32 fly,
+          run,
+          walk;
+
+      f32 walk_timer, yaw, pitch, board_yaw;
+
+      enum walk_activity activity;
+      f32 transition_t;
+   }
+   animator;
+}
+extern player_walk;
+extern struct player_subsystem_interface player_subsystem_walk;
+
+enum player_walk_soundeffect {
+   k_player_walk_soundeffect_splash
+};
+
+static f32
+   k_walkspeed             = 4.4f,
+   k_runspeed              = 10.0f,
+   k_airspeed              = 1.2f,
+   k_stopspeed             = 4.0f,
+   k_walk_accel            = 10.0f,
+   k_walk_air_accel        = 7.0f,
+   k_walk_friction         = 6.0f,
+   k_walk_step_height      = 0.2f,
+
+   k_sit_yaw_limit         = VG_PIf/1.7f,
+   k_sit_pitch_limit       = VG_PIf/4.0f;
+
+static void player__walk_register(void)
+{
+   VG_VAR_F32( k_walkspeed,      flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_runspeed,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_stopspeed,      flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_airspeed,       flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_walk_friction,  flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_walk_air_accel, flags=VG_VAR_CHEAT );
+   VG_VAR_F32( k_walk_accel,     flags=VG_VAR_CHEAT );
+}
+
+void player__walk_pre_update  (void);
+void player__walk_update      (void);
+void player__walk_post_update (void);
+void player__walk_animate     (void);
+void player__walk_pose        (void *animator, player_pose *pose);
+void player__walk_post_animate(void);
+void player__walk_im_gui      ( ui_context *ctx );
+void player__walk_bind        (void);
+void player__walk_reset       (void);
+void player__walk_restore     (void);
+void player__walk_animator_exchange( bitpack_ctx *ctx, void *data );
+void player__walk_transition( bool grounded, f32 board_yaw );
+void player__walk_sfx_oneshot( u8 id, v3f pos, f32 volume );
diff --git a/src/render.c b/src/render.c
new file mode 100644 (file)
index 0000000..fada4a5
--- /dev/null
@@ -0,0 +1,258 @@
+#include "render.h"
+#include "vg/vg_engine.h"
+#include "vg/vg_platform.h"
+#include "vg/vg_framebuffer.h"
+
+static void async_render_init( void *payload, u32 size )
+{
+   f32 rh = 0x1p-4f, ih = 0.3f;
+
+   float quad[] = { 
+      0.00f,0.00f, 1.00f,1.00f, 0.00f,1.00f,    /* fsquad */
+      0.00f,0.00f, 1.00f,0.00f, 1.00f,1.00f,    
+
+      0.00f,0.00f, 1.00f,rh,     0.00f,rh,    /* fsquad1 */
+      0.00f,0.00f, 1.00f,0.00f,  1.00f,rh,
+      0.00f,1.00f, 0.00f,1.0f-rh,1.00f,1.0f-rh,
+      0.00f,1.00f, 1.00f,1.0f-rh,1.00f,1.0f,   
+
+      /* 9x9 debug grid */
+      /* row0 */
+      0.00f,0.00f, 0.30f,0.30f, 0.00f,0.30f,
+      0.00f,0.00f, 0.30f,0.00f, 0.30f,0.30f,
+      0.30f,0.00f, 0.60f,0.30f, 0.30f,0.30f,
+      0.30f,0.00f, 0.60f,0.00f, 0.60f,0.30f,
+      0.60f,0.00f, 0.90f,0.30f, 0.60f,0.30f,
+      0.60f,0.00f, 0.90f,0.00f, 0.90f,0.30f,
+      /* row1 */
+      0.00f,0.30f, 0.30f,0.60f, 0.00f,0.60f,
+      0.00f,0.30f, 0.30f,0.30f, 0.30f,0.60f,
+      0.30f,0.30f, 0.60f,0.60f, 0.30f,0.60f,
+      0.30f,0.30f, 0.60f,0.30f, 0.60f,0.60f,
+      0.60f,0.30f, 0.90f,0.60f, 0.60f,0.60f,
+      0.60f,0.30f, 0.90f,0.30f, 0.90f,0.60f,
+      /* row2 */
+      0.00f,0.60f, 0.30f,0.90f, 0.00f,0.90f,
+      0.00f,0.60f, 0.30f,0.60f, 0.30f,0.90f,
+      0.30f,0.60f, 0.60f,0.90f, 0.30f,0.90f,
+      0.30f,0.60f, 0.60f,0.60f, 0.60f,0.90f,
+      0.60f,0.60f, 0.90f,0.90f, 0.60f,0.90f,
+      0.60f,0.60f, 0.90f,0.60f, 0.90f,0.90f,
+
+      0.00f,ih, 1.00f,ih+rh, 0.00f,ih+rh,    /* fsquad2 */
+      0.00f,ih, 1.00f,ih,    1.00f,ih+rh,
+   };
+
+   glGenVertexArrays( 1, &g_render.fsquad.vao );
+   glGenBuffers( 1, &g_render.fsquad.vbo );
+   glBindVertexArray( g_render.fsquad.vao );
+   glBindBuffer( GL_ARRAY_BUFFER, g_render.fsquad.vbo );
+   glBufferData( GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW );
+   glBindVertexArray( g_render.fsquad.vao );
+   glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 
+                          sizeof(float)*2, (void*)0 );
+   glEnableVertexAttribArray( 0 );
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   g_render.ready = 1;
+}
+
+void render_init(void)
+{
+   vg_console_reg_var( "blur_strength", &k_blur_strength, k_var_dtype_f32, 0 );
+   vg_console_reg_var( "render_scale", &k_render_scale,
+                       k_var_dtype_f32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "fov", &k_fov, k_var_dtype_f32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "cam_height", &k_cam_height, 
+                        k_var_dtype_f32, VG_VAR_PERSISTENT );
+   vg_console_reg_var( "blur_effect", &k_blur_effect, 
+                        k_var_dtype_i32, VG_VAR_PERSISTENT );
+
+   void *alloc = vg_mem.rtmemory;
+   
+   /* 
+    * Main framebuffer
+    */
+   g_render.fb_main = vg_framebuffer_allocate( alloc, 3, 1 );
+   g_render.fb_main->display_name = "main";
+   g_render.fb_main->resolution_div = 1;
+   g_render.fb_main->attachments[0] = (vg_framebuffer_attachment)
+   {
+      "colour", k_framebuffer_attachment_type_texture,
+
+      .internalformat = GL_RGB,
+      .format         = GL_RGB,
+      .type           = GL_UNSIGNED_BYTE,
+      .attachment     = GL_COLOR_ATTACHMENT0
+   };
+   g_render.fb_main->attachments[1] = (vg_framebuffer_attachment)
+   {
+      "motion", k_framebuffer_attachment_type_texture,
+
+      .quality        = k_framebuffer_quality_high_only,
+      .internalformat = GL_RG16F,
+      .format         = GL_RG,
+      .type           = GL_FLOAT,
+      .attachment     = GL_COLOR_ATTACHMENT1
+   };
+   g_render.fb_main->attachments[2] = (vg_framebuffer_attachment)
+   {
+      "depth_stencil", k_framebuffer_attachment_type_texture_depth,
+      .internalformat = GL_DEPTH24_STENCIL8,
+      .format         = GL_DEPTH_STENCIL,
+      .type           = GL_UNSIGNED_INT_24_8,
+      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
+   };
+   vg_framebuffer_create( g_render.fb_main );
+   
+   /* 
+    * Water reflection
+    */
+   g_render.fb_water_reflection = vg_framebuffer_allocate( alloc, 2, 1 );
+   g_render.fb_water_reflection->display_name = "water_reflection";
+   g_render.fb_water_reflection->resolution_div = 2;
+   g_render.fb_water_reflection->attachments[0] = (vg_framebuffer_attachment) 
+   {
+      "colour", k_framebuffer_attachment_type_texture,
+      .internalformat = GL_RGB,
+      .format         = GL_RGB,
+      .type           = GL_UNSIGNED_BYTE,
+      .attachment     = GL_COLOR_ATTACHMENT0
+   };
+   g_render.fb_water_reflection->attachments[1] = (vg_framebuffer_attachment) 
+   {
+      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+      .internalformat = GL_DEPTH24_STENCIL8,
+      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
+   };
+   vg_framebuffer_create( g_render.fb_water_reflection );
+
+   /*
+    * Thid rendered view from the perspective of the camera, but just 
+    * captures stuff thats under the water
+    */
+   g_render.fb_water_beneath = vg_framebuffer_allocate( alloc, 2, 1 );
+   g_render.fb_water_beneath->display_name = "water_beneath";
+   g_render.fb_water_beneath->resolution_div = 2;
+   g_render.fb_water_beneath->attachments[0] = (vg_framebuffer_attachment) 
+   {
+      "colour", k_framebuffer_attachment_type_texture,
+      .internalformat = GL_RED,
+      .format         = GL_RED,
+      .type           = GL_UNSIGNED_BYTE,
+      .attachment     = GL_COLOR_ATTACHMENT0
+   };
+   g_render.fb_water_beneath->attachments[1] = (vg_framebuffer_attachment) 
+   {
+      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+      .internalformat = GL_DEPTH24_STENCIL8,
+      .attachment     = GL_DEPTH_STENCIL_ATTACHMENT
+   };
+   vg_framebuffer_create( g_render.fb_water_beneath );
+
+   /* 
+    * Workshop preview
+    */
+   g_render.fb_workshop_preview = vg_framebuffer_allocate( alloc, 2, 1 );
+   g_render.fb_workshop_preview->display_name = "workshop_preview";
+   g_render.fb_workshop_preview->resolution_div = 0;
+   g_render.fb_workshop_preview->fixed_w = WORKSHOP_PREVIEW_WIDTH;
+   g_render.fb_workshop_preview->fixed_h = WORKSHOP_PREVIEW_HEIGHT;
+   g_render.fb_workshop_preview->attachments[0] = (vg_framebuffer_attachment) 
+   {
+      "colour", k_framebuffer_attachment_type_texture,
+      .internalformat = GL_RGB,
+      .format         = GL_RGB,
+      .type           = GL_UNSIGNED_BYTE,
+      .attachment     = GL_COLOR_ATTACHMENT0
+   };
+   g_render.fb_workshop_preview->attachments[1] = (vg_framebuffer_attachment) 
+   {
+      "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+      .internalformat = GL_DEPTH24_STENCIL8,
+      .attachment = GL_DEPTH_STENCIL_ATTACHMENT
+   };
+   vg_framebuffer_create( g_render.fb_workshop_preview );
+   
+   /*
+    * Network status
+    */
+   g_render.fb_network_status = vg_framebuffer_allocate( alloc, 1, 1 );
+   g_render.fb_network_status->display_name = "network_status_ui";
+   g_render.fb_network_status->resolution_div = 0;
+   g_render.fb_network_status->fixed_w = 128;
+   g_render.fb_network_status->fixed_h = 48;
+   g_render.fb_network_status->attachments[0] = (vg_framebuffer_attachment) 
+   {
+      "colour", k_framebuffer_attachment_type_texture,
+      .internalformat = GL_RGB,
+      .format         = GL_RGB,
+      .type           = GL_UNSIGNED_BYTE,
+      .attachment     = GL_COLOR_ATTACHMENT0
+   };
+   vg_framebuffer_create( g_render.fb_network_status );
+
+   vg_async_call( async_render_init, NULL, 0 );
+}
+
+/*
+ * Utility
+ */
+void render_fsquad(void)
+{
+   glBindVertexArray( g_render.fsquad.vao );
+   glDrawArrays( GL_TRIANGLES, 0, 6 );
+}
+
+void render_fsquad1(void)
+{
+   glBindVertexArray( g_render.fsquad.vao );
+   glDrawArrays( GL_TRIANGLES, 6, 6+6 );
+}
+
+void render_fsquad2(void)
+{
+   glBindVertexArray( g_render.fsquad.vao );
+   glDrawArrays( GL_TRIANGLES, 66+6,6 );
+}
+
+void postprocess_to_screen( vg_framebuffer *fb )
+{
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+
+   glEnable(GL_BLEND);
+   glDisable(GL_DEPTH_TEST);
+   glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
+   glBlendEquation(GL_FUNC_ADD);
+
+   v2f inverse;
+   vg_framebuffer_inverse_ratio( fb, inverse );
+
+   if( k_blur_effect )
+   {
+      shader_blitblur_use();
+      shader_blitblur_uTexMain( 0 );
+      shader_blitblur_uTexMotion( 1 );
+      shader_blitblur_uBlurStrength( k_blur_strength / 
+                                     (vg.time_frame_delta*60.0) );
+      shader_blitblur_uInverseRatio( inverse );
+
+      inverse[0] -= 0.0001f;
+      inverse[1] -= 0.0001f;
+      shader_blitblur_uClampUv( inverse );
+      shader_blitblur_uOverrideDir( g_render.blur_override );
+
+      vg_framebuffer_bind_texture( fb, 0, 0 );
+      vg_framebuffer_bind_texture( fb, 1, 1 );
+   }
+   else
+   {
+      shader_blit_use();
+      shader_blit_uTexMain( 0 );
+      shader_blit_uInverseRatio( inverse );
+      vg_framebuffer_bind_texture( fb, 0, 0 );
+   }
+
+   render_fsquad();
+}
diff --git a/src/render.h b/src/render.h
new file mode 100644 (file)
index 0000000..765f41b
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+#pragma once
+#include "common.h"
+#include "model.h"
+#include "shader_props.h"
+#include "vg/vg_framebuffer.h"
+#include "vg/vg_camera.h"
+
+#include "shaders/blit.h"
+#include "shaders/blitblur.h"
+#include "shaders/blitcolour.h"
+#include "shaders/blit_transition.h"
+
+#define WORKSHOP_PREVIEW_WIDTH  504
+#define WORKSHOP_PREVIEW_HEIGHT 336
+
+static f32 k_render_scale  = 1.0f;
+static i32 k_blur_effect   = 1;
+static f32 k_blur_strength = 0.3f;
+static f32 k_fov           = 0.86f;
+static f32 k_cam_height    = 0.8f;
+
+/* 
+ * All standard buffers used in rendering
+ */
+struct pipeline
+{
+   glmesh fsquad;
+
+   vg_framebuffer *fb_main,
+                  *fb_water_reflection,
+                  *fb_water_beneath,
+                  *fb_workshop_preview,
+                  *fb_network_status;
+   int ready;
+
+   v2f blur_override;
+   vg_camera cam;
+}
+static g_render;
+
+void render_init(void);
+void render_fsquad(void);
+void render_fsquad1(void);
+void render_fsquad2(void);
+void postprocess_to_screen( vg_framebuffer *fb );
diff --git a/src/save.c b/src/save.c
new file mode 100644 (file)
index 0000000..73eaa49
--- /dev/null
@@ -0,0 +1,195 @@
+#include "skaterift.h"
+#include "save.h"
+#include "addon.h"
+#include "vg/vg_msg.h"
+#include "vg/vg_log.h"
+#include "vg/vg_loader.h"
+#include "world.h"
+#include "player.h"
+
+static const char *str_skaterift_main_save = "save.bkv";
+static f64 last_autosave;
+
+void savedata_file_write( savedata_file *file )
+{
+   savedata_file *sav = file;
+   FILE *fp = fopen( sav->path, "wb" );
+   if( fp ){
+      fwrite( sav->buf, sav->len, 1, fp );
+      fclose( fp );
+      vg_success( "savedata written to '%s'\n", sav->path );
+   }
+   else {
+      vg_error( "Error writing savedata (%s)\n", sav->path );
+   }
+}
+
+void savedata_group_write( savedata_group *group )
+{
+   for( u32 i=0; i<group->file_count; i++ ){
+      savedata_file_write( &group->files[i] );
+   }
+}
+
+void savedata_file_read( savedata_file *file )
+{
+   FILE *fp = fopen( file->path, "rb" );
+   if( fp ){
+      file->len = fread( file->buf, 1, sizeof(file->buf), fp );
+      fclose( fp );
+   }
+   else{
+      file->len = 0;
+      vg_warn( "Error reading savedata (%s)\n", file->path );
+   }
+}
+
+static void skaterift_write_addon_alias( vg_msg *msg, const char *key,
+                                         addon_alias *alias ){
+   if( alias->workshop_id ) 
+      vg_msg_wkvnum( msg, key, k_vg_msg_u64, 1, &alias->workshop_id );
+   else
+      vg_msg_wkvstr( msg, key, alias->foldername );
+}
+
+static void skaterift_write_viewslot( vg_msg *msg, const char *key,
+                                      enum addon_type type, u16 cache_id ){
+   if( !cache_id ) return;
+
+   struct addon_cache *cache = &addon_system.cache[type];
+   addon_cache_entry *entry = vg_pool_item( &cache->pool, cache_id );
+   addon_reg *reg = entry->reg_ptr;
+
+   if( reg )
+      skaterift_write_addon_alias( msg, key, &reg->alias );
+}
+
+void skaterift_read_addon_alias( vg_msg *msg, const char *key,
+                                 enum addon_type type, 
+                                 addon_alias *alias )
+{
+   alias->foldername[0] = '\0';
+   alias->workshop_id = 0;
+   alias->type = type;
+
+   vg_msg_cmd kv;
+   if( vg_msg_getkvcmd( msg, key, &kv ) ){
+      if( kv.code == k_vg_msg_kvstring ){
+         vg_strncpy( kv.value, alias->foldername, sizeof(alias->foldername),
+                     k_strncpy_allow_cutoff );
+      }
+      else
+         vg_msg_cast( kv.value, kv.code, &alias->workshop_id, k_vg_msg_u64 );
+   }
+}
+
+static void skaterift_populate_world_savedata( savedata_file *file,
+                                               enum world_purpose which ){
+   file->path[0] = '\0';
+   file->len = 0;
+   addon_reg *reg = world_static.instance_addons[ which ];
+
+   if( !reg ){
+      vg_error( "Tried to save unspecified world (reg was null)\n" );
+      return;
+   }
+
+   skaterift_world_get_save_path( which, file->path );
+
+   vg_msg sav;
+   vg_msg_init( &sav, file->buf, sizeof(file->buf) );
+
+   world_instance *instance = &world_static.instances[which];
+   world_entity_serialize( instance, &sav );
+
+   vg_msg_frame( &sav, "player" );
+   {
+      vg_msg_wkvnum( &sav, "position", k_vg_msg_float|k_vg_msg_32b, 3, 
+                     (which == world_static.active_instance)? 
+                           localplayer.rb.co:
+                           instance->player_co );
+   }
+   vg_msg_end_frame( &sav );
+
+   file->len = sav.cur.co;
+}
+
+static void skaterift_populate_main_savedata( savedata_file *file )
+{
+   strcpy( file->path, str_skaterift_main_save );
+
+   vg_msg sav;
+   vg_msg_init( &sav, file->buf, sizeof(file->buf) );
+   vg_msg_wkvnum( &sav, "ach", k_vg_msg_u32, 1, &skaterift.achievements );
+
+   vg_msg_frame( &sav, "player" );
+   {
+      skaterift_write_viewslot( &sav, "board", k_addon_type_board, 
+                                localplayer.board_view_slot );
+      skaterift_write_viewslot( &sav, "playermodel", k_addon_type_player,
+                                localplayer.playermodel_view_slot );
+   }
+   vg_msg_end_frame( &sav );
+
+   file->len = sav.cur.co;
+}
+
+void skaterift_read_main_savedata( savedata_file *file )
+{
+   strcpy( file->path, str_skaterift_main_save );
+   savedata_file_read( file );
+}
+
+int skaterift_autosave( int async )
+{
+   if( async )
+      if( !vg_loader_availible() ) return 0;
+
+   u32 save_files = 2;
+   if( world_static.instances[k_world_purpose_client].status 
+         == k_world_status_loaded ){
+      save_files ++;
+   }
+
+   vg_linear_clear( vg_async.buffer );
+   u32 size = sizeof(savedata_group) + sizeof(savedata_file) * save_files;
+
+   savedata_group *group;
+   if( async ){
+      size = vg_align8( size );
+      group = vg_linear_alloc( vg_async.buffer, size );
+   }
+   else
+      group = alloca( size );
+
+   group->file_count = save_files;
+   skaterift_populate_main_savedata( &group->files[0] );
+   skaterift_populate_world_savedata( &group->files[1], k_world_purpose_hub );
+
+   if( world_static.instances[ k_world_purpose_client ].status 
+         == k_world_status_loaded ){
+      skaterift_populate_world_savedata( &group->files[2], 
+                                          k_world_purpose_client );
+   }
+
+   if( async )
+      vg_loader_start( (void *)savedata_group_write, group );
+   else
+      savedata_group_write( group );
+
+   return 1;
+}
+
+void skaterift_autosave_synchronous(void)
+{
+   skaterift_autosave(0);
+}
+
+void skaterift_autosave_update(void)
+{
+   if( vg.time - last_autosave > 20.0 ){
+      if( skaterift_autosave(1) ){
+         last_autosave = vg.time;
+      }
+   }
+}
diff --git a/src/save.h b/src/save.h
new file mode 100644 (file)
index 0000000..acda301
--- /dev/null
@@ -0,0 +1,29 @@
+#pragma once
+#include "vg/vg_platform.h"
+#include "vg/vg_msg.h"
+#include "addon.h"
+
+typedef struct savedata_file savedata_file;
+typedef struct savedata_group savedata_group;
+
+struct savedata_group {
+   u32 file_count;
+   struct savedata_file {
+      char path[128];
+      u8  buf[2048];
+      u32 len;
+   }
+   files[];
+};
+
+void savedata_file_read( savedata_file *file );
+void savedata_file_write( savedata_file *file );
+void savedata_group_write( savedata_group *group );
+int skaterift_autosave(int async);
+void skaterift_autosave_synchronous(void);
+void skaterift_autosave_update(void);
+void skaterift_read_addon_alias( vg_msg *msg, const char *key,
+                                 enum addon_type type, 
+                                 addon_alias *alias );
+
+void skaterift_read_main_savedata( savedata_file *file );
diff --git a/src/scene.c b/src/scene.c
new file mode 100644 (file)
index 0000000..a94fbca
--- /dev/null
@@ -0,0 +1,402 @@
+#include "scene.h"
+
+u32 scene_mem_required( scene_context *ctx )
+{
+   u32 vertex_length = vg_align8(ctx->max_vertices * sizeof(scene_vert)),
+       index_length  = vg_align8(ctx->max_indices  * sizeof(u32));
+
+   return vertex_length + index_length;
+}
+
+void scene_init( scene_context *ctx, u32 max_vertices, u32 max_indices )
+{
+   ctx->vertex_count = 0;
+   ctx->indice_count = 0;
+   ctx->max_vertices = max_vertices;
+   ctx->max_indices = max_indices;
+   ctx->arrindices = NULL; /* must be filled out by user */
+   ctx->arrvertices = NULL;
+
+   memset( &ctx->submesh, 0, sizeof(mdl_submesh) );
+
+   v3_fill( ctx->bbx[0],  999999.9f );
+   v3_fill( ctx->bbx[1], -999999.9f );
+}
+
+void scene_supply_buffer( scene_context *ctx, void *buffer )
+{
+   u32 vertex_length = vg_align8( ctx->max_vertices * sizeof(scene_vert) );
+
+   ctx->arrvertices = buffer;
+   ctx->arrindices  = (u32*)(((u8*)buffer) + vertex_length);
+}
+
+void scene_vert_pack_norm( scene_vert *vert, v3f norm, f32 blend )
+{
+   v3f n;
+   v3_muls( norm, 127.0f, n );
+   v3_minv( n, (v3f){  127.0f,  127.0f,  127.0f }, n );
+   v3_maxv( n, (v3f){ -127.0f, -127.0f, -127.0f }, n );
+   vert->norm[0] = n[0];
+   vert->norm[1] = n[1];
+   vert->norm[2] = n[2];
+   vert->norm[3] = blend * 127.0f;
+}
+
+/* 
+ * Append a model into the scene with a given transform
+ */
+void scene_add_mdl_submesh( scene_context *ctx, mdl_context *mdl, 
+                            mdl_submesh *sm, m4x3f transform )
+{
+   if( ctx->vertex_count + sm->vertex_count > ctx->max_vertices ){
+      vg_fatal_error( "Scene vertex buffer overflow (%u exceeds %u)\n",
+                        ctx->vertex_count + sm->vertex_count, 
+                        ctx->max_vertices );
+   }
+
+   if( ctx->indice_count + sm->indice_count > ctx->max_indices ){
+      vg_fatal_error( "Scene index buffer overflow (%u exceeds %u)\n",
+                        ctx->indice_count + sm->indice_count,
+                        ctx->max_indices );
+   }
+
+   mdl_vert   *src_verts = mdl_arritm( &mdl->verts, sm->vertex_start );
+   scene_vert *dst_verts = &ctx->arrvertices[ ctx->vertex_count ];
+
+   u32 *src_indices    =  mdl_arritm( &mdl->indices, sm->indice_start ),
+       *dst_indices    = &ctx->arrindices[ ctx->indice_count ];
+   
+   /* Transform and place vertices */
+   boxf bbxnew;
+   box_init_inf( bbxnew );
+   m4x3_expand_aabb_aabb( transform, bbxnew, sm->bbx );
+   box_concat( ctx->bbx, bbxnew );
+
+   m3x3f normal_matrix;
+   m3x3_copy( transform, normal_matrix );
+   v3_normalize( normal_matrix[0] );
+   v3_normalize( normal_matrix[1] );
+   v3_normalize( normal_matrix[2] );
+   
+   for( u32 i=0; i<sm->vertex_count; i++ ){
+      mdl_vert   *src   = &src_verts[i];
+      scene_vert *pvert = &dst_verts[i];
+
+      m4x3_mulv( transform, src->co, pvert->co );
+
+      v3f normal;
+      m3x3_mulv( normal_matrix, src->norm, normal );
+      scene_vert_pack_norm( pvert, normal, src->colour[0]*(1.0f/255.0f) );
+      
+      v2_copy( src->uv, pvert->uv );
+   }
+
+   u32 real_indices = 0;
+   for( u32 i=0; i<sm->indice_count/3; i++ ){
+      u32 *src = &src_indices[i*3],
+          *dst = &dst_indices[real_indices];
+
+      v3f ab, ac, tn;
+      v3_sub( src_verts[src[2]].co, src_verts[src[0]].co, ab );
+      v3_sub( src_verts[src[1]].co, src_verts[src[0]].co, ac );
+      v3_cross( ac, ab, tn );
+
+#if 0
+      if( v3_length2( tn ) <= 0.00001f )
+         continue;
+#endif
+
+      dst[0] = src[0] + ctx->vertex_count;
+      dst[1] = src[1] + ctx->vertex_count;
+      dst[2] = src[2] + ctx->vertex_count;
+
+      real_indices += 3;
+   }
+
+   if( real_indices != sm->indice_count )
+      vg_warn( "Zero area triangles in model\n" );
+
+   ctx->vertex_count += sm->vertex_count;
+   ctx->indice_count += real_indices;
+}
+
+/*
+ * One by one adders for simplified access (mostly procedural stuff)
+ */
+void scene_push_tri( scene_context *ctx, u32 tri[3] )
+{
+   if( ctx->indice_count + 3 > ctx->max_indices )
+      vg_fatal_error( "Scene indice buffer overflow (%u exceeds %u)\n",
+                        ctx->indice_count+3, ctx->max_indices );
+
+   u32 *dst = &ctx->arrindices[ ctx->indice_count ];
+
+   dst[0] = tri[0];
+   dst[1] = tri[1];
+   dst[2] = tri[2];
+
+   ctx->indice_count += 3;
+}
+
+void scene_push_vert( scene_context *ctx, scene_vert *v )
+{
+   if( ctx->vertex_count + 1 > ctx->max_vertices )
+      vg_fatal_error( "Scene vertex buffer overflow (%u exceeds %u)\n",
+                        ctx->vertex_count+1, ctx->max_vertices );
+
+   scene_vert *dst = &ctx->arrvertices[ ctx->vertex_count ];
+   *dst = *v;
+
+   ctx->vertex_count ++;
+}
+
+void scene_copy_slice( scene_context *ctx, mdl_submesh *sm )
+{
+   sm->indice_start = ctx->submesh.indice_start;
+   sm->indice_count = ctx->indice_count - sm->indice_start;
+
+   sm->vertex_start = ctx->submesh.vertex_start;
+   sm->vertex_count = ctx->vertex_count - sm->vertex_start;
+   
+   ctx->submesh.indice_start = ctx->indice_count;
+   ctx->submesh.vertex_start = ctx->vertex_count;
+}
+
+void scene_set_vertex_flags( scene_context *ctx, 
+                             u32 start, u32 count, u16 flags )
+{
+   for( u32 i=0; i<count; i++ )
+      ctx->arrvertices[ start + i ].flags = flags;
+}
+
+struct scene_upload_info{
+   scene_context *ctx;
+   glmesh *mesh;
+};
+
+void async_scene_upload( void *payload, u32 size )
+{
+   struct scene_upload_info *info = payload;
+
+   //assert( mesh->loaded == 0 );
+   
+   glmesh *mesh = info->mesh;
+   scene_context *ctx = info->ctx;
+
+   glGenVertexArrays( 1, &mesh->vao );
+   glGenBuffers( 1, &mesh->vbo );
+   glGenBuffers( 1, &mesh->ebo );
+   glBindVertexArray( mesh->vao );
+
+   size_t stride = sizeof(scene_vert);
+
+   glBindBuffer( GL_ARRAY_BUFFER, mesh->vbo );
+   glBufferData( GL_ARRAY_BUFFER, ctx->vertex_count*stride, 
+                 ctx->arrvertices, GL_STATIC_DRAW );
+
+   glBindVertexArray( mesh->vao );
+   glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh->ebo );
+   glBufferData( GL_ELEMENT_ARRAY_BUFFER, ctx->indice_count*sizeof(u32),
+                 ctx->arrindices, GL_STATIC_DRAW );
+   
+   /* 0: coordinates */
+   glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0 );
+   glEnableVertexAttribArray( 0 );
+
+   /* 1: normal */
+   glVertexAttribPointer( 1, 4, GL_BYTE, GL_TRUE,
+         stride, (void *)offsetof(scene_vert, norm) );
+   glEnableVertexAttribArray( 1 );
+
+   /* 2: uv */
+   glVertexAttribPointer( 2, 2, GL_FLOAT, GL_FALSE, 
+         stride, (void *)offsetof(scene_vert, uv) );
+   glEnableVertexAttribArray( 2 );
+
+   mesh->indice_count = ctx->indice_count;
+   mesh->loaded = 1;
+
+   vg_info( "Scene upload ( XYZ_f32 UV_f32 XYZW_i8 )[ u32 ]\n" );
+   vg_info( "   indices:%u\n", ctx->indice_count );
+   vg_info( "   verts:%u\n",   ctx->vertex_count );
+}
+
+void scene_upload_async( scene_context *ctx, glmesh *mesh )
+{
+   vg_async_item *call = vg_async_alloc( sizeof(struct scene_upload_info) );
+
+   struct scene_upload_info *info = call->payload;
+   info->mesh = mesh;
+   info->ctx = ctx;
+
+   vg_async_dispatch( call, async_scene_upload );
+}
+
+vg_async_item *scene_alloc_async( scene_context *scene, glmesh *mesh,
+                                  u32 max_vertices, u32 max_indices )
+{
+   scene_init( scene, max_vertices, max_indices );
+   u32 buf_size = scene_mem_required( scene );
+
+   u32 hdr_size = vg_align8(sizeof(struct scene_upload_info));
+   vg_async_item *call = vg_async_alloc( hdr_size + buf_size );
+
+   struct scene_upload_info *info = call->payload;
+
+   info->mesh = mesh;
+   info->ctx = scene;
+
+   void *buffer = ((u8*)call->payload)+hdr_size;
+   scene_supply_buffer( scene, buffer );
+
+   return call;
+}
+
+
+/*
+ * BVH implementation
+ */
+
+static void scene_bh_expand_bound( void *user, boxf bound, u32 item_index )
+{
+   scene_context *s = user;
+   scene_vert *pa = &s->arrvertices[ s->arrindices[item_index*3+0] ],
+              *pb = &s->arrvertices[ s->arrindices[item_index*3+1] ],
+              *pc = &s->arrvertices[ s->arrindices[item_index*3+2] ];
+   
+  box_addpt( bound, pa->co );
+  box_addpt( bound, pb->co );
+  box_addpt( bound, pc->co );
+}
+
+static float scene_bh_centroid( void *user, u32 item_index, int axis )
+{
+   scene_context *s = user;
+   scene_vert *pa = &s->arrvertices[ s->arrindices[item_index*3+0] ],
+              *pb = &s->arrvertices[ s->arrindices[item_index*3+1] ],
+              *pc = &s->arrvertices[ s->arrindices[item_index*3+2] ];
+
+   #if 0
+
+   float min, max;
+
+   min = vg_minf( pa->co[axis], pb->co[axis] );
+   max = vg_maxf( pa->co[axis], pb->co[axis] );
+   min = vg_minf( min, pc->co[axis] );
+   max = vg_maxf( max, pc->co[axis] );
+
+   return (min+max) * 0.5f;
+
+   #else
+   return (pa->co[axis] + pb->co[axis] + pc->co[axis]) * (1.0f/3.0f);
+   #endif
+}
+
+static void scene_bh_swap( void *user, u32 ia, u32 ib )
+{
+   scene_context *s = user;
+
+   u32 *ti = &s->arrindices[ia*3];
+   u32 *tj = &s->arrindices[ib*3];
+
+   u32 temp[3];
+   temp[0] = ti[0];
+   temp[1] = ti[1];
+   temp[2] = ti[2];
+
+   ti[0] = tj[0];
+   ti[1] = tj[1];
+   ti[2] = tj[2];
+
+   tj[0] = temp[0];
+   tj[1] = temp[1];
+   tj[2] = temp[2];
+}
+
+static void scene_bh_debug( void *user, u32 item_index )
+{
+   scene_context *s = user;
+   u32 idx = item_index*3;
+   scene_vert *pa = &s->arrvertices[ s->arrindices[ idx+0 ] ],
+              *pb = &s->arrvertices[ s->arrindices[ idx+1 ] ],
+              *pc = &s->arrvertices[ s->arrindices[ idx+2 ] ];
+
+   vg_line( pa->co, pb->co, 0xff0000ff );
+   vg_line( pb->co, pc->co, 0xff0000ff );
+   vg_line( pc->co, pa->co, 0xff0000ff );
+}
+
+static void scene_bh_closest( void *user, u32 index, v3f point, v3f closest )
+{
+   scene_context *s = user;
+
+   v3f positions[3];
+   u32 *tri = &s->arrindices[ index*3 ];
+   for( int i=0; i<3; i++ )
+      v3_copy( s->arrvertices[tri[i]].co, positions[i] );
+
+   closest_on_triangle_1( point, positions, closest );
+}
+
+bh_system bh_system_scene = 
+{
+   .expand_bound = scene_bh_expand_bound,
+   .item_centroid = scene_bh_centroid,
+   .item_closest = scene_bh_closest,
+   .item_swap = scene_bh_swap,
+   .item_debug = scene_bh_debug,
+};
+
+/*
+ * An extra step is added onto the end to calculate the hit normal
+ */
+int scene_raycast( scene_context *s, bh_tree *bh, 
+                   v3f co, v3f dir, ray_hit *hit, u16 ignore )
+{
+   hit->tri = NULL;
+
+   bh_iter it;
+   bh_iter_init_ray( 0, &it, co, dir, hit->dist );
+   i32 idx;
+
+   while( bh_next( bh, &it, &idx ) ){
+      u32 *tri = &s->arrindices[ idx*3 ];
+
+      if( s->arrvertices[tri[0]].flags & ignore )  continue;
+
+      v3f vs[3];
+      for( u32 i=0; i<3; i++ )
+         v3_copy( s->arrvertices[tri[i]].co, vs[i] );
+      
+      f32 t;
+      if( ray_tri( vs, co, dir, &t, 0 ) ){
+         if( t < hit->dist ){
+            hit->dist = t;
+            hit->tri = tri;
+         }
+      }
+   }
+
+   if( hit->tri ){
+      v3f v0, v1;
+      
+      float *pa = s->arrvertices[hit->tri[0]].co,
+            *pb = s->arrvertices[hit->tri[1]].co,
+            *pc = s->arrvertices[hit->tri[2]].co;
+
+      v3_sub( pa, pb, v0 );
+      v3_sub( pc, pb, v1 );
+      v3_cross( v1, v0, hit->normal );
+      v3_normalize( hit->normal );
+      v3_muladds( co, dir, hit->dist, hit->pos );
+   }
+
+   return hit->tri?1:0;
+}
+
+bh_tree *scene_bh_create( void *lin_alloc, scene_context *s )
+{
+   u32 triangle_count = s->indice_count / 3;
+   return bh_create( lin_alloc, &bh_system_scene, s, triangle_count, 2 );
+}
diff --git a/src/scene.h b/src/scene.h
new file mode 100644 (file)
index 0000000..8d6f57a
--- /dev/null
@@ -0,0 +1,61 @@
+#pragma once
+#include "vg/vg_bvh.h"
+#include "vg/vg_async.h"
+#include "common.h"
+#include "model.h"
+
+typedef struct scene_context scene_context;
+typedef struct scene_vert scene_vert;
+
+#pragma pack(push,1)
+
+/* 32 byte vertexs, we don't care about the normals too much,
+ *    maybe possible to bring down uv to i16s too */
+struct scene_vert
+{
+   v3f co;        /* 3*32 */
+   v2f uv;        /* 2*32 */
+   i8  norm[4];   /* 4*8 */
+   u16 flags; /* only for the cpu. its junk on the gpu */
+   u16 unused[3];
+};
+
+#pragma pack(pop)
+
+/* 
+ * 1. this should probably be a CONTEXT based approach unlike this mess.
+ * take a bit of the mdl_context ideas and redo this header. its messed up
+ * pretty bad right now.
+ */
+
+struct scene_context
+{
+   scene_vert *arrvertices;
+   u32 *arrindices;
+
+   u32 vertex_count, indice_count,
+       max_vertices, max_indices;
+
+   boxf bbx;
+   mdl_submesh submesh;
+};
+
+extern bh_system bh_system_scene;
+bh_tree *scene_bh_create( void *lin_alloc, scene_context *s );
+int scene_raycast( scene_context *s, bh_tree *bh, 
+                   v3f co, v3f dir, ray_hit *hit, u16 ignore );
+vg_async_item *scene_alloc_async( scene_context *scene, glmesh *mesh,
+                                  u32 max_vertices, u32 max_indices );
+void scene_copy_slice( scene_context *ctx, mdl_submesh *sm );
+void scene_push_vert( scene_context *ctx, scene_vert *v );
+void scene_vert_pack_norm( scene_vert *vert, v3f norm, f32 blend );
+void scene_push_tri( scene_context *ctx, u32 tri[3] );
+void scene_add_mdl_submesh( scene_context *ctx, mdl_context *mdl, 
+                            mdl_submesh *sm, m4x3f transform );
+void scene_set_vertex_flags( scene_context *ctx, 
+                             u32 start, u32 count, u16 flags );
+void scene_supply_buffer( scene_context *ctx, void *buffer );
+void scene_init( scene_context *ctx, u32 max_vertices, u32 max_indices );
+u32 scene_mem_required( scene_context *ctx );
+void async_scene_upload( void *payload, u32 size );
+void scene_upload_async( scene_context *ctx, glmesh *mesh );
diff --git a/src/scene_rigidbody.h b/src/scene_rigidbody.h
new file mode 100644 (file)
index 0000000..57ff1ff
--- /dev/null
@@ -0,0 +1,247 @@
+#pragma once
+
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
+ *
+ * Describes intereactions between vg rigidbody objects and skaterift's scene
+ * description
+ */
+
+#include "scene.h"
+#include "vg/vg_rigidbody.h"
+#include "vg/vg_rigidbody_collision.h"
+
+static int rb_sphere__scene( m4x3f mtxA, f32 r,
+                             m4x3f mtxB, bh_tree *scene_bh, rb_ct *buf, 
+                             u16 ignore ){
+   scene_context *sc = scene_bh->user;
+
+   int count = 0;
+
+   boxf box;
+   v3_sub( mtxA[3], (v3f){ r,r,r }, box[0] );
+   v3_add( mtxA[3], (v3f){ r,r,r }, box[1] );
+
+   bh_iter it;
+   i32 idx;
+   bh_iter_init_box( 0, &it, box );
+   
+   while( bh_next( scene_bh, &it, &idx ) ){
+      u32 *ptri = &sc->arrindices[ idx*3 ];
+      v3f tri[3];
+
+      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
+
+      for( int j=0; j<3; j++ )
+         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
+      
+      buf[ count ].element_id = ptri[0];
+
+      vg_line( tri[0],tri[1],0x70ff6000 );
+      vg_line( tri[1],tri[2],0x70ff6000 );
+      vg_line( tri[2],tri[0],0x70ff6000 );
+
+      int contact = rb_sphere__triangle( mtxA, r, tri, &buf[count] );
+      count += contact;
+
+      if( count == 16 ){
+         vg_warn( "Exceeding sphere_vs_scene capacity. Geometry too dense!\n" );
+         return count;
+      }
+   }
+
+   return count;
+}
+
+static int rb_box__scene( m4x3f mtxA, boxf bbx,
+                          m4x3f mtxB, bh_tree *scene_bh, 
+                          rb_ct *buf, u16 ignore ){
+   scene_context *sc = scene_bh->user;
+   v3f tri[3];
+
+   v3f extent, center;
+   v3_sub( bbx[1], bbx[0], extent );
+   v3_muls( extent, 0.5f, extent );
+   v3_add( bbx[0], extent, center );
+
+   f32 r = v3_length(extent);
+   boxf world_bbx;
+   v3_fill( world_bbx[0], -r );
+   v3_fill( world_bbx[1],  r );
+   for( int i=0; i<2; i++ ){
+      v3_add( center, world_bbx[i], world_bbx[i] );
+      v3_add( mtxA[3], world_bbx[i], world_bbx[i] );
+   }
+
+   m4x3f to_local;
+   m4x3_invert_affine( mtxA, to_local );
+
+   bh_iter it;
+   bh_iter_init_box( 0, &it, world_bbx );
+   int idx;
+   int count = 0;
+
+   vg_line_boxf( world_bbx, VG__RED );
+   
+   while( bh_next( scene_bh, &it, &idx ) ){
+      u32 *ptri = &sc->arrindices[ idx*3 ];
+      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
+
+      for( int j=0; j<3; j++ )
+         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
+
+      if( rb_box_triangle_sat( extent, center, to_local, tri ) ){
+         vg_line(tri[0],tri[1],0xff50ff00 );
+         vg_line(tri[1],tri[2],0xff50ff00 );
+         vg_line(tri[2],tri[0],0xff50ff00 );
+      }
+      else{
+         vg_line(tri[0],tri[1],0xff0000ff );
+         vg_line(tri[1],tri[2],0xff0000ff );
+         vg_line(tri[2],tri[0],0xff0000ff );
+         continue;
+      }
+
+      v3f v0,v1,n;
+      v3_sub( tri[1], tri[0], v0 );
+      v3_sub( tri[2], tri[0], v1 );
+      v3_cross( v0, v1, n );
+
+      if( v3_length2( n ) <= 0.00001f ){
+#ifdef RIGIDBODY_CRY_ABOUT_EVERYTHING
+         vg_error( "Zero area triangle!\n" );
+#endif
+         return 0;
+      }
+
+      v3_normalize( n );
+
+      /* find best feature */
+      f32 best = v3_dot( mtxA[0], n );
+      int axis = 0;
+
+      for( int i=1; i<3; i++ ){
+         f32 c = v3_dot( mtxA[i], n );
+
+         if( fabsf(c) > fabsf(best) ){
+            best = c;
+            axis = i;
+         }
+      }
+
+      v3f manifold[4];
+
+      if( axis == 0 ){
+         f32 px = best > 0.0f? bbx[0][0]: bbx[1][0];
+         manifold[0][0] = px;
+         manifold[0][1] = bbx[0][1];
+         manifold[0][2] = bbx[0][2];
+         manifold[1][0] = px;
+         manifold[1][1] = bbx[1][1];
+         manifold[1][2] = bbx[0][2];
+         manifold[2][0] = px;
+         manifold[2][1] = bbx[1][1];
+         manifold[2][2] = bbx[1][2];
+         manifold[3][0] = px;
+         manifold[3][1] = bbx[0][1];
+         manifold[3][2] = bbx[1][2];
+      }
+      else if( axis == 1 ){
+         f32 py = best > 0.0f? bbx[0][1]: bbx[1][1];
+         manifold[0][0] = bbx[0][0];
+         manifold[0][1] = py;
+         manifold[0][2] = bbx[0][2];
+         manifold[1][0] = bbx[1][0];
+         manifold[1][1] = py;
+         manifold[1][2] = bbx[0][2];
+         manifold[2][0] = bbx[1][0];
+         manifold[2][1] = py;
+         manifold[2][2] = bbx[1][2];
+         manifold[3][0] = bbx[0][0];
+         manifold[3][1] = py;
+         manifold[3][2] = bbx[1][2];
+      }
+      else{
+         f32 pz = best > 0.0f? bbx[0][2]: bbx[1][2];
+         manifold[0][0] = bbx[0][0];
+         manifold[0][1] = bbx[0][1];
+         manifold[0][2] = pz;
+         manifold[1][0] = bbx[1][0];
+         manifold[1][1] = bbx[0][1];
+         manifold[1][2] = pz;
+         manifold[2][0] = bbx[1][0];
+         manifold[2][1] = bbx[1][1];
+         manifold[2][2] = pz;
+         manifold[3][0] = bbx[0][0];
+         manifold[3][1] = bbx[1][1];
+         manifold[3][2] = pz;
+      }
+   
+      for( int j=0; j<4; j++ )
+         m4x3_mulv( mtxA, manifold[j], manifold[j] );
+
+      vg_line( manifold[0], manifold[1], 0xffffffff );
+      vg_line( manifold[1], manifold[2], 0xffffffff );
+      vg_line( manifold[2], manifold[3], 0xffffffff );
+      vg_line( manifold[3], manifold[0], 0xffffffff );
+
+      for( int j=0; j<4; j++ ){
+         rb_ct *ct = buf+count;
+
+         v3_copy( manifold[j], ct->co );
+         v3_copy( n, ct->n );
+
+         f32 l0 = v3_dot( tri[0], n ),
+               l1 = v3_dot( manifold[j], n );
+
+         ct->p = (l0-l1)*0.5f;
+         if( ct->p < 0.0f )
+            continue;
+
+         ct->type = k_contact_type_default;
+         count ++;
+
+         if( count >= 12 )
+            return count;
+      }
+   }
+   return count;
+}
+
+/* mtxB is defined only for tradition; it is not used currently */
+static int rb_capsule__scene( m4x3f mtxA, rb_capsule *c,
+                              m4x3f mtxB, bh_tree *scene_bh, 
+                              rb_ct *buf, u16 ignore ){
+   int count = 0;
+
+   boxf bbx;
+   v3_sub( mtxA[3], (v3f){ c->h, c->h, c->h }, bbx[0] );
+   v3_add( mtxA[3], (v3f){ c->h, c->h, c->h }, bbx[1] );
+   
+   scene_context *sc = scene_bh->user;
+   
+   bh_iter it;
+   bh_iter_init_box( 0, &it, bbx );
+   i32 idx;
+   while( bh_next( scene_bh, &it, &idx ) ){
+      u32 *ptri = &sc->arrindices[ idx*3 ];
+      if( sc->arrvertices[ptri[0]].flags & ignore ) continue;
+
+      v3f tri[3];
+      for( int j=0; j<3; j++ )
+         v3_copy( sc->arrvertices[ptri[j]].co, tri[j] );
+      
+      buf[ count ].element_id = ptri[0];
+
+      int contact = rb_capsule__triangle( mtxA, c, tri, &buf[count] );
+      count += contact;
+
+      if( count >= 16 ){
+         vg_warn("Exceeding capsule_vs_scene capacity. Geometry too dense!\n");
+         return count;
+      }
+   }
+
+   return count;
+}
+
diff --git a/src/shader_props.h b/src/shader_props.h
new file mode 100644 (file)
index 0000000..29e79f5
--- /dev/null
@@ -0,0 +1,37 @@
+#pragma once
+#include "vg/vg_platform.h"
+
+struct shader_props_standard
+{
+   u32 tex_diffuse;
+};
+
+struct shader_props_terrain
+{
+   u32 tex_diffuse;
+   v2f blend_offset;
+   v4f sand_colour;
+};
+
+struct shader_props_vertex_blend
+{
+   u32 tex_diffuse;
+   v2f blend_offset;
+};
+
+struct shader_props_water
+{
+   v4f shore_colour;
+   v4f deep_colour;
+   f32 fog_scale;
+   f32 fresnel;
+   f32 water_sale;
+   v4f wave_speed;
+};
+
+struct shader_props_cubemapped
+{
+   u32 tex_diffuse;
+   u32 cubemap_entity;
+   v4f tint;
+};
diff --git a/src/skaterift.c b/src/skaterift.c
new file mode 100644 (file)
index 0000000..36e087f
--- /dev/null
@@ -0,0 +1,679 @@
+/*
+ * =============================================================================
+ *
+ * Copyright  .        . .       -----, ,----- ,---.   .---.
+ * 2021-2024  |\      /| |           /  |      |    | |    /|
+ *            | \    / | +--        /   +----- +---'  |   / |
+ *            |  \  /  | |         /    |      |   \  |  /  |
+ *            |   \/   | |        /     |      |    \ | /   |
+ *            '        ' '--' [] '----- '----- '     ' '---'  SOFTWARE
+ *
+ * =============================================================================
+ */
+
+#define SR_ALLOW_REWIND_HUB
+
+#ifdef _WIN32
+ #include <winsock2.h>
+#endif
+
+/* 
+ *     system headers
+ * --------------------- */
+
+#include "vg/vg_opt.h"
+#include "vg/vg_loader.h"
+#include "vg/vg_io.h"
+
+#include "skaterift.h"
+#include "steam.h"
+#include "render.h"
+#include "world.h"
+#include "font.h"
+#include "player.h"
+#include "network.h"
+#include "menu.h"
+#include "vehicle.h"
+#include "save.h"
+#include "player_remote.h"
+#include "particle.h"
+#include "trail.h"
+#include "freecam.h"
+#include "ent_tornado.h"
+#include "ent_miniworld.h"
+#include "ent_skateshop.h"
+#include "ent_npc.h"
+#include "ent_camera.h"
+#include "world_map.h"
+#include "gui.h"
+#include "workshop.h"
+#include "audio.h"
+#include "player_render.h"
+#include "control_overlay.h"
+#include "client.h"
+
+struct skaterift_globals skaterift = 
+{ 
+   .time_rate = 1.0f,
+   .hub_world = "maps/dev_hub",
+};
+
+void game_launch_opt(void)
+{
+   const char *arg;
+   if( (arg = vg_long_opt_arg( "world" )) )
+      skaterift.hub_world = arg;
+}
+
+static void async_skaterift_player_start( void *payload, u32 size ){
+   world_switch_instance(0);
+}
+
+static void skaterift_restore_state(void)
+{
+   savedata_file sav;
+   skaterift_read_main_savedata( &sav );
+
+   vg_msg kvsav;
+   vg_msg_init( &kvsav, sav.buf, sizeof(sav.buf) );
+
+   u32 ach;
+   vg_msg_getkvintg( &kvsav, "ach", k_vg_msg_u32, &ach, NULL );
+   skaterift.achievements |= ach;
+
+   u32 board_reg_id = time(NULL) % addon_count( k_addon_type_board, 0 ),
+       player_reg_id = (time(NULL)+44) % addon_count( k_addon_type_player, 0 );
+
+   vg_msg_cursor orig = kvsav.cur;
+   if( vg_msg_seekframe( &kvsav, "player" ) ){
+      addon_alias q;
+
+      /* board */
+      skaterift_read_addon_alias( &kvsav, "board", k_addon_type_board, &q );
+      u32 reg_id = addon_match( &q );
+      if( reg_id != 0xffffffff ) 
+         board_reg_id = reg_id;
+
+      /* playermodel */
+      skaterift_read_addon_alias( &kvsav, "playermodel", 
+                                  k_addon_type_player, &q );
+      reg_id = addon_match( &q );
+      if( reg_id != 0xffffffff ) 
+         player_reg_id = reg_id;
+   }
+
+   localplayer.board_view_slot = 
+      addon_cache_create_viewer( k_addon_type_board, board_reg_id );
+   localplayer.playermodel_view_slot = 
+      addon_cache_create_viewer( k_addon_type_player, player_reg_id );
+
+   kvsav.cur = orig;
+}
+
+static addon_reg *skaterift_mount_world_unloadable( const char *path, u32 ext ){
+   addon_reg *reg = addon_mount_local_addon( path, k_addon_type_world, ".mdl" );
+   if( !reg ) vg_fatal_error( "world not found\n" );
+   reg->flags |= (ADDON_REG_HIDDEN | ext);
+   return reg;
+}
+
+static void skaterift_load_world_content(void){
+   /* hub world */
+   addon_reg *hub = skaterift_mount_world_unloadable( skaterift.hub_world, 0 );
+   skaterift_mount_world_unloadable( "maps/mp_spawn", 
+         ADDON_REG_CITY|ADDON_REG_PREMIUM );
+   skaterift_mount_world_unloadable( "maps/mp_mtzero", 
+         ADDON_REG_MTZERO|ADDON_REG_PREMIUM );
+   skaterift_mount_world_unloadable( "maps/dev_tutorial", 0 );
+   skaterift_mount_world_unloadable( "maps/dev_flatworld", 0 );
+   skaterift_mount_world_unloadable( "maps/mp_line1", ADDON_REG_PREMIUM );
+
+   world_static.load_state = k_world_loader_load;
+
+   struct world_load_args args = {
+      .purpose = k_world_purpose_hub,
+      .reg = hub
+   };
+   skaterift_world_load_thread( &args );
+}
+
+static void skaterift_load_player_content(void)
+{
+   particle_alloc( &particles_grind, 300 );
+   particle_alloc( &particles_env, 200 );
+
+   player_load_animation_reference( "models/ch_none.mdl" );
+   player_model_load( &localplayer.fallback_model, "models/ch_none.mdl" );
+   player__bind();
+   player_board_load( &localplayer.fallback_board, "models/board_none.mdl" );
+}
+
+void game_load(void)
+{
+   vg_console_reg_cmd( "load_world", skaterift_load_world_command, NULL );
+   vg_console_reg_var( "immobile", &localplayer.immobile, k_var_dtype_i32, 0 );
+   vg_loader_step( menu_init, NULL );
+
+   vg_loader_step( control_overlay_init, NULL );
+   vg_loader_step( world_init, NULL );
+   vg_loader_step( vehicle_init, NULL );
+   vg_loader_step( gui_init, NULL );
+
+   vg_loader_step( player_init, NULL );
+   vg_loader_step( player_ragdoll_init, NULL );
+   vg_loader_step( npc_init, NULL );
+
+   /* content stuff */
+   vg_loader_step( addon_system_init, NULL );
+   vg_loader_step( workshop_init, NULL );
+   vg_loader_step( skateshop_init, NULL );
+   vg_loader_step( ent_tornado_init, NULL );
+   vg_loader_step( skaterift_replay_init, NULL );
+   vg_loader_step( skaterift_load_player_content, NULL );
+
+   vg_bake_shaders();
+   vg_loader_step( audio_init, NULL );
+
+   vg_loader_step( skaterift_load_world_content, NULL );
+   vg_async_call( async_skaterift_player_start, NULL, 0 );
+   vg_async_stall();
+
+   vg_console_load_autos();
+
+   addon_mount_content_folder( k_addon_type_player, 
+                               "playermodels", ".mdl" );
+   addon_mount_content_folder( k_addon_type_board, "boards", ".mdl" );
+   addon_mount_content_folder( k_addon_type_world, "maps", ".mdl" );
+   addon_mount_workshop_items();
+   vg_async_call( async_addon_reg_update, NULL, 0 );
+   vg_async_stall();
+
+   skaterift_restore_state();
+   update_ach_models();
+
+   vg_loader_step( NULL, skaterift_autosave_synchronous );
+}
+
+static void draw_origin_axis(void)
+{
+   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 1.0f, 0.0f, 0.0f }, 0xffff0000 );
+   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 0.0f, 1.0f, 0.0f }, 0xff00ff00 );
+   vg_line( (v3f){ 0.0f, 0.0f, 0.0f }, (v3f){ 0.0f, 0.0f, 1.0f }, 0xff0000ff );
+}
+void skaterift_change_client_world_preupdate(void);
+
+/* 
+ * UPDATE LOOP
+ * ---------------------------------------------------------------------------*/
+
+void vg_pre_update(void)
+{
+   skaterift_preupdate_inputs();
+
+   steam_update();
+   skaterift_change_client_world_preupdate();
+
+   if( !g_client.loaded ) return;
+
+   draw_origin_axis();
+   addon_system_pre_update();
+   skateshop_world_preview_preupdate();
+   network_update();
+
+   /* time rate */
+   f32 target = 1;
+   if( skaterift.activity & k_skaterift_replay )
+      target = 0;
+
+   v3f listen_co;
+   v3_copy( localplayer.rb.co, listen_co );
+
+   if( skaterift.activity & k_skaterift_menu )
+   {
+      if( menu.bg_cam )
+      {
+         v3_copy( menu.bg_cam->transform.co, listen_co );
+      }
+      else target = 0;
+   }
+
+   vg_slewf( &skaterift.time_rate, target, vg.time_frame_delta * (1.0f/0.3f) );
+   vg.time_rate = vg_smoothstepf( skaterift.time_rate );
+   
+   /* TODO: how can we compress this? */
+   ent_miniworld_preupdate();
+   world_entity_focus_preupdate();
+
+   if( skaterift.activity != k_skaterift_menu )
+   {
+      player__pre_update();
+   }
+
+   skaterift_replay_pre_update();
+   remote_sfx_pre_update();
+   skateshop_world_preupdate( world_current_instance() );
+
+   world_update( world_current_instance(), localplayer.rb.co );
+   audio_ambient_sprites_update( world_current_instance(), listen_co );
+   world_map_pre_update();
+}
+
+void vg_fixed_update(void)
+{
+   if( !g_client.loaded ) return;
+
+   world_routes_fixedupdate( world_current_instance() );
+   player__update();
+   vehicle_update_fixed();
+}
+
+void vg_post_update(void)
+{
+   if( !g_client.loaded ) return;
+
+   player__post_update();
+
+   float dist;
+   int sample_index;
+   world_audio_sample_distances( localplayer.rb.co, &sample_index, &dist );
+
+   audio_lock();
+   vg_dsp.echo_distances[sample_index] = dist;
+
+   v3f ears = { 1.0f,0.0f,0.0f };
+   m3x3_mulv( g_render.cam.transform, ears, ears );
+   v3_copy( ears, vg_audio.external_listener_ears );
+   v3_copy( g_render.cam.transform[3], vg_audio.external_listener_pos );
+
+   if( localplayer.gate_waiting ){
+      m4x3_mulv( localplayer.gate_waiting->transport,
+                 vg_audio.external_listener_pos, 
+                 vg_audio.external_listener_pos );
+   }
+
+   v3_copy( localplayer.rb.v, vg_audio.external_lister_velocity );
+   audio_unlock();
+
+   vehicle_update_post();
+   skaterift_autosave_update();
+}
+
+/*
+ * RENDERING
+ * ---------------------------------------------------------------------------*/
+
+static void render_player_transparent(void)
+{
+   if( (skaterift.activity == k_skaterift_menu) &&
+       (menu.page == k_menu_page_main) && 
+       (menu.main_index == k_menu_main_guide) )
+   {
+      return;
+   }
+
+   static vg_camera small_cam;      /* DOES NOT NEED TO BE STATIC BUT MINGW 
+                                    SAIS OTHERWISE */
+
+   m4x3_copy( g_render.cam.transform, small_cam.transform );
+
+   small_cam.fov = g_render.cam.fov;
+   small_cam.nearz = 0.05f;
+   small_cam.farz  = 60.0f;
+
+   vg_camera_update_view( &small_cam );
+   vg_camera_update_projection( &small_cam );
+   vg_camera_finalize( &small_cam );
+
+   /* Draw player to window buffer and blend background ontop */
+   player__render( &small_cam );
+}
+
+static world_instance *get_view_world(void)
+{
+   if( (skaterift.activity & k_skaterift_menu) &&
+       (menu.page == k_menu_page_main) && 
+       (menu.main_index == k_menu_main_guide) )
+   {
+      return &world_static.instances[0];
+   }
+
+   world_instance *view_world = world_current_instance();
+   if( localplayer.gate_waiting && 
+         (localplayer.gate_waiting->flags & k_ent_gate_nonlocal) ){
+      view_world = &world_static.instances[world_static.active_instance ^ 0x1];
+   }
+
+   return view_world;
+}
+
+static void render_scene(void)
+{
+   /* Draw world */
+   glEnable( GL_DEPTH_TEST );
+
+   for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ )
+   {
+      if( world_static.instances[i].status == k_world_status_loaded )
+      {
+         world_prerender( &world_static.instances[i] );
+      }
+   }
+
+   if( menu_viewing_map() )
+   {
+      world_instance *world = world_current_instance();
+      glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
+      
+      v3f bg;
+      v3_muls( world->ub_lighting.g_daysky_colour,
+                  world->ub_lighting.g_day_phase - 
+                  world->ub_lighting.g_sunset_phase*0.1f, bg );
+
+      v3_muladds( bg, world->ub_lighting.g_sunset_colour,
+                  (1.0f-0.5f)*world->ub_lighting.g_sunset_phase, bg );
+
+      v3_muladds( bg, world->ub_lighting.g_nightsky_colour,
+                  (1.0f-world->ub_lighting.g_day_phase), bg );
+
+      glClearColor( bg[0], bg[1], bg[2], 0.0f );
+      glClear( GL_COLOR_BUFFER_BIT );
+      glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, 
+                                    GL_COLOR_ATTACHMENT1 } );
+
+      m4x3f identity;
+      m4x3_identity( identity );
+      render_world_override( world, world, identity, &g_render.cam, 
+                             world_map.close_spawn, 
+                             (v4f){world->tar_min, world->tar_max, 1.0f, 0.0f});
+      render_world_routes( world, world, identity, &g_render.cam, 0, 1 );
+      return;
+   }
+
+   world_instance *view_world = get_view_world();
+   render_world( view_world, &g_render.cam, 0, 0, 1, 1 );
+
+   particle_system_update( &particles_grind, vg.time_delta );
+   //particle_system_debug( &particles_grind );
+   particle_system_prerender( &particles_grind );
+   particle_system_render( &particles_grind, &g_render.cam );
+   
+   ent_tornado_pre_update();
+   particle_system_update( &particles_env, vg.time_delta );
+   particle_system_prerender( &particles_env );
+   particle_system_render( &particles_env, &g_render.cam );
+
+   player_glide_render_effects( &g_render.cam );
+
+   /* 
+    * render transition 
+    */
+   if( global_miniworld.transition == 0 ) 
+      return;
+
+   world_instance *holdout_world = NULL;
+   f32 t = 0.0f;
+
+   if( global_miniworld.transition == 1 ){
+      holdout_world = &world_static.instances[ k_world_purpose_hub ];
+      t = global_miniworld.t;
+   }
+   else{
+      holdout_world = &world_static.instances[ k_world_purpose_client ];
+      t = 1.0f-global_miniworld.t;
+   }
+
+   if( holdout_world->status != k_world_status_loaded )
+      return;
+
+   t = vg_smoothstepf( t );
+
+   glEnable( GL_STENCIL_TEST );
+   glDisable( GL_DEPTH_TEST );
+   glStencilOp( GL_KEEP, GL_KEEP, GL_REPLACE );  
+   glStencilFunc( GL_ALWAYS, 1, 0xFF ); 
+   glStencilMask( 0xFF );
+
+   shader_blit_transition_use();
+   shader_blit_transition_uInverseRatio( (v2f){1.0f,1.0f} );
+   shader_blit_transition_uT( -(sqrtf(2)+0.5f) * t );
+
+   render_fsquad();
+   render_world( holdout_world, &global_miniworld.cam, 1, 0, 1, 1 );
+}
+
+static void skaterift_composite_maincamera(void)
+{
+   vg_camera_lerp( &localplayer.cam, &world_static.focus_cam,
+                vg_smoothstepf(world_static.focus_strength), &g_render.cam );
+
+   if( skaterift.activity == k_skaterift_replay )
+   {
+      if( player_replay.use_freecam )
+      {
+         freecam_preupdate();
+         v3_copy( player_replay.replay_freecam.pos, g_render.cam.pos );
+         v3_copy( player_replay.replay_freecam.angles, g_render.cam.angles );
+         g_render.cam.fov = player_replay.replay_freecam.fov;
+      }
+      else
+      {
+         skaterift_get_replay_cam( &g_render.cam );
+      }
+   }
+
+   g_render.cam.nearz = 0.1f;
+   g_render.cam.farz  = 2100.0f;
+
+   if( (skaterift.activity == k_skaterift_menu) && menu.bg_cam )
+   {
+      ent_camera_unpack( menu.bg_cam, &g_render.cam );
+   }
+
+   if( menu_viewing_map() )
+   {
+      vg_camera_copy( &world_map.cam, &g_render.cam );
+      g_render.cam.nearz = 4.0f;
+      g_render.cam.farz = 3100.0f;
+   }
+
+   if( global_miniworld.transition ){
+      f32 dt = vg.time_frame_delta / 2.0f,
+          s  = vg_signf( global_miniworld.transition );
+      global_miniworld.t += s * dt;
+
+      if( (global_miniworld.t > 1.0f) || (global_miniworld.t < 0.0f) ){
+         global_miniworld.t = vg_clampf( global_miniworld.t, 0.0f, 1.0f );
+         global_miniworld.transition = 0;
+      }
+   }
+
+   vg_camera_update_transform( &g_render.cam );
+   vg_camera_update_view( &g_render.cam );
+   vg_camera_update_projection( &g_render.cam );
+   vg_camera_finalize( &g_render.cam );
+}
+
+static void render_main_game(void)
+{
+   if( skaterift.activity == k_skaterift_replay )
+   {
+      player__animate_from_replay( &player_replay.local );
+   }
+   else{
+      player__animate();
+      skaterift_record_frame( &player_replay.local,
+                              localplayer.deferred_frame_record );
+      localplayer.deferred_frame_record = 0;
+   }
+   animate_remote_players();
+   player__pre_render();
+
+   skaterift_composite_maincamera();
+
+   /* --------------------------------------------------------------------- */
+   if( !menu_viewing_map() )
+   {
+      world_instance *world = world_current_instance();
+      render_world_cubemaps( world );
+
+      ent_gate *nlg = world->rendering_gate;
+      if( nlg && (nlg->flags & k_ent_gate_nonlocal) )
+         render_world_cubemaps( &world_static.instances[nlg->target] );
+   }
+
+   /* variable res target */
+   vg_framebuffer_bind( g_render.fb_main, k_render_scale );
+   glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT );
+
+   render_scene();
+   glEnable( GL_DEPTH_TEST );
+
+   /* full res target */
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+
+   render_player_transparent(); /* needs to read the depth buffer before we fuck
+                                   it up with the oblique rendering inside the
+                                   portals */
+
+   /* continue with variable rate */
+   if( !global_miniworld.transition && !menu_viewing_map() )
+   {
+      vg_framebuffer_bind( g_render.fb_main, k_render_scale );
+      render_world_gates( get_view_world(), &g_render.cam );
+   }
+
+   /* composite */
+
+   if( (skaterift.activity == k_skaterift_menu) && menu.bg_blur )
+      v2_muls( (v2f){ 0.04f, 0.001f }, 1.0f-skaterift.time_rate, 
+               g_render.blur_override );
+   else 
+      v2_zero( g_render.blur_override );
+   postprocess_to_screen( g_render.fb_main );
+
+   skaterift_replay_post_render();
+   control_overlay_render();
+}
+
+void vg_render(void)
+{
+   if( !g_client.loaded )
+   {
+      vg_loader_render();
+      return;
+   }
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+
+   glViewport( 0,0, vg.window_x, vg.window_y );
+   glDisable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+
+   glClearColor( 1.0f, 0.0f, 0.0f, 0.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+
+   render_main_game();
+   m4x4_copy( g_render.cam.mtx.pv, vg.pv );
+   
+   /* Other shite */
+   glDisable(GL_BLEND);
+   glDisable(GL_DEPTH_TEST);
+   vg_lines_drawall();
+   glViewport( 0,0, vg.window_x, vg.window_y );
+
+   gui_render_icons();
+}
+
+void vg_gui( ui_context *ctx )
+{
+   if( !g_client.loaded ) return;
+
+   gui_draw( ctx );
+
+   if( k_light_editor )
+      imgui_world_light_edit( ctx, world_current_instance() );
+   
+   vg_ui.tex_bg = g_render.fb_main->attachments[0].id;
+   vg_framebuffer_inverse_ratio( g_render.fb_main, vg_ui.bg_inverse_ratio );
+
+   menu_gui( ctx );
+   player__im_gui( ctx );
+   world_instance *world = world_current_instance();
+
+   world_routes_imgui( ctx, world );
+   skaterift_replay_imgui( ctx );
+   workshop_form_gui( ctx );
+   remote_player_network_imgui( ctx, vg.pv );
+
+   if( menu_viewing_map() )
+   {
+      remote_players_imgui_world( ctx, world_current_instance(), 
+                                  vg.pv, 2000.0f, 0 );
+      remote_players_imgui_lobby( ctx );
+   }
+   else 
+   {
+      remote_players_chat_imgui( ctx ); /* TODO: conditional */
+      remote_players_imgui_world( ctx, world_current_instance(), 
+                                  vg.pv, 100.0f, 1 );
+   }
+}
+
+#include "addon.c"
+#include "addon_types.c"
+#include "audio.c"
+#include "ent_challenge.c"
+#include "ent_glider.c"
+#include "entity.c"
+#include "ent_miniworld.c"
+#include "ent_objective.c"
+#include "ent_region.c"
+#include "ent_relay.c"
+#include "ent_route.c"
+#include "ent_skateshop.c"
+#include "ent_tornado.c"
+#include "ent_traffic.c"
+#include "freecam.c"
+#include "menu.c"
+#include "network.c"
+#include "particle.c"
+#include "player_basic_info.c"
+#include "player.c"
+#include "player_common.c"
+#include "player_dead.c"
+#include "player_drive.c"
+#include "player_effects.c"
+#include "player_glide.c"
+#include "player_ragdoll.c"
+#include "player_remote.c"
+#include "player_render.c"
+#include "player_replay.c"
+#include "player_skate.c"
+#include "player_walk.c"
+#include "render.c"
+#include "save.c"
+#include "scene.c"
+#include "steam.c"
+#include "trail.c"
+#include "vehicle.c"
+#include "workshop.c"
+#include "world_audio.c"
+#include "world.c"
+#include "world_entity.c"
+#include "world_gate.c"
+#include "world_gen.c"
+#include "world_load.c"
+#include "world_map.c"
+#include "world_physics.c"
+#include "world_render.c"
+#include "world_routes.c"
+#include "world_routes_ui.c"
+#include "world_sfd.c"
+#include "world_volumes.c"
+#include "world_water.c"
+#include "ent_npc.c"
+#include "model.c"
+#include "control_overlay.c"
+#include "ent_camera.c"
diff --git a/src/skaterift.h b/src/skaterift.h
new file mode 100644 (file)
index 0000000..e480d2e
--- /dev/null
@@ -0,0 +1,33 @@
+#pragma once
+#define SKATERIFT
+#define SKATERIFT_APPID 2103940
+
+#include "vg/vg_engine.h"
+#include "vg/vg_camera.h"
+
+enum skaterift_rt 
+{
+   k_skaterift_rt_workshop_preview,
+   k_skaterift_rt_server_status,
+   k_skaterift_rt_max
+};
+
+struct skaterift_globals
+{
+   f32 time_rate;
+   
+   enum skaterift_activity {
+      k_skaterift_default    = 0x00,
+      k_skaterift_replay     = 0x01,
+      k_skaterift_ent_focus  = 0x02,
+      k_skaterift_menu       = 0x04,
+   }
+   activity;
+   GLuint rt_textures[k_skaterift_rt_max];
+
+   u32 achievements;
+   int demo_mode;
+
+   const char *hub_world;
+}
+extern skaterift;
diff --git a/src/skaterift_lib.c b/src/skaterift_lib.c
new file mode 100644 (file)
index 0000000..95b8141
--- /dev/null
@@ -0,0 +1,31 @@
+#define QOI_IMPLEMENTATION
+#include "vg/submodules/qoi/qoi.h"
+#include "vg/vg_platform.h"
+#include "vg/vg_m.h"
+
+u8 *qoi_encode_rgbaf32( f32 *data, u32 width, u32 height, int *length )
+{
+   u8 *buf = (u8 *)data;
+   for( u32 i=0; i<width*height*4; i ++ )
+   {
+      buf[i] = vg_clampf( data[i] * 255.0f, 0.0f, 255.0f );
+   }
+
+   qoi_desc desc = 
+   {
+      .channels=4,  
+      .colorspace=0, 
+      .width=width, 
+      .height=height
+   };
+
+   return qoi_encode( buf, &desc, length );
+}
+
+void qoi_free( u8 *ptr )
+{
+   free( ptr );
+}
+
+#include "vg/vg_tool.h"
+#include "vg/vg_tool.c"
diff --git a/src/skeleton.h b/src/skeleton.h
new file mode 100644 (file)
index 0000000..9729b64
--- /dev/null
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2021-2022 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+#include "vg/vg_lines.h"
+#include "model.h"
+
+struct skeleton
+{
+   struct skeleton_bone
+   {
+      v3f co, end;
+      u32 parent;
+
+      u32 flags;
+      int defer;
+
+      mdl_keyframe kf;
+      mdl_bone *orig_bone;
+
+      u32 collider;
+      boxf hitbox;
+      const char *name;
+   }
+   *bones;
+   u32 bone_count;
+
+   struct skeleton_anim
+   {
+      const char *name;
+      u32 length;
+
+      float rate;
+      mdl_keyframe *anim_data;
+   }
+   *anims;
+   u32 anim_count;
+
+#if 0
+   m4x3f *final_mtx;
+#endif
+
+   struct skeleton_ik
+   {
+      u32 lower, upper, target, pole;
+      m3x3f ia, ib;
+   }
+   *ik;
+   u32 ik_count;
+
+   u32 
+       collider_count,
+       bindable_count;
+};
+
+static u32 skeleton_bone_id( struct skeleton *skele, const char *name )
+{
+   for( u32 i=1; i<skele->bone_count; i++ ){
+      if( !strcmp( skele->bones[i].name, name ))
+         return i;
+   }
+
+   vg_error( "skeleton_bone_id( *, \"%s\" );\n", name );
+   vg_fatal_error( "Bone does not exist\n" );
+
+   return 0;
+}
+
+static void keyframe_copy_pose( mdl_keyframe *kfa, mdl_keyframe *kfb, 
+                                   int num )
+{
+   for( int i=0; i<num; i++ )
+      kfb[i] = kfa[i];
+}
+
+
+/* apply a rotation from the perspective of root */
+static void keyframe_rotate_around( mdl_keyframe *kf, 
+                                    v3f origin, v3f offset, v4f q )
+{
+   v3f v0, co;
+   v3_add( kf->co, offset, co );
+   v3_sub( co, origin, v0 );
+   q_mulv( q, v0, v0 );
+   v3_add( v0, origin, co );
+   v3_sub( co, offset, kf->co );
+
+   q_mul( q, kf->q, kf->q );
+   q_normalize( kf->q );
+}
+
+static void keyframe_lerp( mdl_keyframe *kfa, mdl_keyframe *kfb, f32 t,
+                           mdl_keyframe *kfd ){
+   v3_lerp( kfa->co, kfb->co, t, kfd->co );
+   q_nlerp( kfa->q,  kfb->q,  t, kfd->q );
+   v3_lerp( kfa->s,  kfb->s,  t, kfd->s );
+}
+
+/*
+ * Lerp between two sets of keyframes and store in dest. Rotations use Nlerp.
+ */
+static void keyframe_lerp_pose( mdl_keyframe *kfa, mdl_keyframe *kfb, 
+                                float t, mdl_keyframe *kfd, int count ){
+   if( t <= 0.0001f ){
+      keyframe_copy_pose( kfa, kfd, count );
+      return;
+   }
+   else if( t >= 0.9999f ){
+      keyframe_copy_pose( kfb, kfd, count );
+      return;
+   }
+
+   for( int i=0; i<count; i++ )
+      keyframe_lerp( kfa+i, kfb+i, t, kfd+i );
+}
+
+static 
+void skeleton_lerp_pose( struct skeleton *skele,
+                         mdl_keyframe *kfa, mdl_keyframe *kfb, float t,
+                         mdl_keyframe *kfd )
+{
+   keyframe_lerp_pose( kfa, kfb, t, kfd, skele->bone_count-1 );
+}
+
+static void skeleton_copy_pose( struct skeleton *skele,
+                                   mdl_keyframe *kfa, mdl_keyframe *kfd )
+{
+   keyframe_copy_pose( kfa, kfd, skele->bone_count-1 );
+}
+
+/*
+ * Sample animation between 2 closest frames using time value. Output is a
+ * keyframe buffer that is allocated with an appropriate size
+ */
+static void skeleton_sample_anim( struct skeleton *skele,
+                                  struct skeleton_anim *anim,
+                                  float time,
+                                  mdl_keyframe *output )
+{
+   f32 animtime  = fmodf( time*anim->rate, anim->length ),
+       animframe = floorf( animtime ),
+       t = animtime - animframe;
+
+   u32 frame = (u32)animframe % anim->length,
+       next  = (frame+1) % anim->length;
+
+   mdl_keyframe *base  = anim->anim_data + (skele->bone_count-1)*frame,
+                *nbase = anim->anim_data + (skele->bone_count-1)*next;
+
+   skeleton_lerp_pose( skele, base, nbase, t, output );
+}
+
+static int skeleton_sample_anim_clamped( struct skeleton *skele,
+                                         struct skeleton_anim *anim,
+                                         float time,
+                                         mdl_keyframe *output )
+{
+   float end = (float)(anim->length-1) / anim->rate;
+   skeleton_sample_anim( skele, anim, vg_minf( end, time ), output );
+
+   if( time > end )
+      return 0;
+   else
+      return 1;
+}
+
+typedef enum anim_apply
+{
+   k_anim_apply_always,
+   k_anim_apply_defer_ik,
+   k_anim_apply_deffered_only,
+   k_anim_apply_absolute
+}
+anim_apply;
+
+static 
+int should_apply_bone( struct skeleton *skele, u32 id, anim_apply type )
+{
+   struct skeleton_bone *sb = &skele->bones[ id ],
+                        *sp = &skele->bones[ sb->parent ];
+
+   if( type == k_anim_apply_defer_ik ){
+      if( ((sp->flags & k_bone_flag_ik) && !(sb->flags & k_bone_flag_ik)) 
+          || sp->defer )
+      {
+         sb->defer = 1;
+         return 0;
+      }
+      else{
+         sb->defer = 0;
+         return 1;
+      }
+   }
+   else if( type == k_anim_apply_deffered_only ){
+      if( sb->defer )
+         return 1;
+      else
+         return 0;
+   }
+
+   return 1;
+}
+
+/*
+ * Apply block of keyframes to skeletons final pose
+ */
+static void skeleton_apply_pose( struct skeleton *skele, mdl_keyframe *pose,
+                                 anim_apply passtype, m4x3f *final_mtx ){
+   if( passtype == k_anim_apply_absolute ){
+      for( u32 i=1; i<skele->bone_count; i++ ){
+         mdl_keyframe *kf = &pose[i-1];
+
+         v3f *posemtx = final_mtx[i];
+
+         q_m3x3( kf->q, posemtx );
+         m3x3_scale( posemtx, kf->s );
+         v3_copy( kf->co, posemtx[3] );
+      }
+      return;
+   }
+
+   m4x3_identity( final_mtx[0] );
+   skele->bones[0].defer = 0;
+   skele->bones[0].flags &= ~k_bone_flag_ik;
+
+   for( u32 i=1; i<skele->bone_count; i++ ){
+      struct skeleton_bone *sb = &skele->bones[i],
+                           *sp = &skele->bones[sb->parent];
+      
+      if( !should_apply_bone( skele, i, passtype ) )
+         continue;
+
+      sb->defer = 0;
+
+      /* process pose */
+      m4x3f posemtx;
+
+      v3f temp_delta;
+      v3_sub( skele->bones[i].co, skele->bones[sb->parent].co, temp_delta );
+
+      /* pose matrix */
+      mdl_keyframe *kf = &pose[i-1];
+      q_m3x3( kf->q, posemtx );
+      m3x3_scale( posemtx, kf->s );
+      v3_copy( kf->co, posemtx[3] );
+      v3_add( temp_delta, posemtx[3], posemtx[3] );
+
+      /* final matrix */
+      m4x3_mul( final_mtx[ sb->parent ], posemtx, final_mtx[i] );
+   }
+}
+
+/* 
+ * Take the final matrices and decompose it into an absolute positioned anim
+ */
+static void skeleton_decompose_mtx_absolute( struct skeleton *skele, 
+                                             mdl_keyframe *anim,
+                                             m4x3f *final_mtx ){
+   for( u32 i=1; i<skele->bone_count; i++ ){
+      struct skeleton_bone *sb = &skele->bones[i];
+      mdl_keyframe *kf = &anim[i-1];
+      m4x3_decompose( final_mtx[i], kf->co, kf->q, kf->s );
+   }
+}
+
+/* 
+ * creates the reference inverse matrix for an IK bone, as it has an initial 
+ * intrisic rotation based on the direction that the IK is setup..
+ */
+static void skeleton_inverse_for_ik( struct skeleton *skele,
+                                     v3f ivaxis,
+                                     u32 id, m3x3f inverse )
+{
+   v3_copy( ivaxis, inverse[0] );
+   v3_copy( skele->bones[id].end, inverse[1] );
+   v3_normalize( inverse[1] );
+   v3_cross( inverse[0], inverse[1], inverse[2] );
+   m3x3_transpose( inverse, inverse );
+}
+
+/*
+ * Creates inverse rotation matrices which the IK system uses.
+ */
+static void skeleton_create_inverses( struct skeleton *skele )
+{
+   /* IK: inverse 'plane-bone space' axis '(^axis,^bone,...)[base] */
+   for( u32 i=0; i<skele->ik_count; i++ ){
+      struct skeleton_ik *ik = &skele->ik[i];
+
+      m4x3f inverse;
+      v3f iv0, iv1, ivaxis;
+      v3_sub( skele->bones[ik->target].co, skele->bones[ik->lower].co, iv0 );
+      v3_sub( skele->bones[ik->pole].co,   skele->bones[ik->lower].co, iv1 );
+      v3_cross( iv0, iv1, ivaxis );
+      v3_normalize( ivaxis );
+
+      skeleton_inverse_for_ik( skele, ivaxis, ik->lower, ik->ia );
+      skeleton_inverse_for_ik( skele, ivaxis, ik->upper, ik->ib );
+   }
+}
+
+/*
+ * Apply a model matrix to all bones, should be done last
+ */
+static 
+void skeleton_apply_transform( struct skeleton *skele, m4x3f transform,
+                               m4x3f *final_mtx )
+{
+   for( u32 i=0; i<skele->bone_count; i++ ){
+      struct skeleton_bone *sb = &skele->bones[i];
+      m4x3_mul( transform, final_mtx[i], final_mtx[i] );
+   }
+}
+
+/*
+ * Apply an inverse matrix to all bones which maps vertices from bind space into
+ * bone relative positions
+ */
+static void skeleton_apply_inverses( struct skeleton *skele, m4x3f *final_mtx ){
+   for( u32 i=0; i<skele->bone_count; i++ ){
+      struct skeleton_bone *sb = &skele->bones[i];
+      m4x3f inverse;
+      m3x3_identity( inverse );
+      v3_negate( sb->co, inverse[3] );
+
+      m4x3_mul( final_mtx[i], inverse, final_mtx[i] );
+   }
+}
+
+/*
+ * Apply all IK modifiers (2 bone ik reference from blender is supported)
+ */
+static void skeleton_apply_ik_pass( struct skeleton *skele, m4x3f *final_mtx ){
+   for( u32 i=0; i<skele->ik_count; i++ ){
+      struct skeleton_ik *ik = &skele->ik[i];
+      
+      v3f v0, /* base -> target */
+          v1, /* base -> pole */
+          vaxis;
+
+      v3f co_base,
+          co_target,
+          co_pole;
+
+      v3_copy( final_mtx[ik->lower][3], co_base );
+      v3_copy( final_mtx[ik->target][3], co_target );
+      v3_copy( final_mtx[ik->pole][3], co_pole );
+
+      v3_sub( co_target, co_base, v0 );
+      v3_sub( co_pole, co_base, v1 );
+      v3_cross( v0, v1, vaxis );
+      v3_normalize( vaxis );
+      v3_normalize( v0 );
+      v3_cross( vaxis, v0, v1 );
+
+      /* localize problem into [x:v0,y:v1] 2d plane */
+      v2f base = { v3_dot( v0, co_base   ), v3_dot( v1, co_base   ) },
+          end  = { v3_dot( v0, co_target ), v3_dot( v1, co_target ) },
+          knee;
+
+      /* Compute angles (basic trig)*/
+      v2f delta;
+      v2_sub( end, base, delta );
+
+      float 
+         l1 = v3_length( skele->bones[ik->lower].end ),
+         l2 = v3_length( skele->bones[ik->upper].end ),
+         d = vg_clampf( v2_length(delta), fabsf(l1 - l2), l1+l2-0.00001f ),
+         c = acosf( (l1*l1 + d*d - l2*l2) / (2.0f*l1*d) ),
+         rot = atan2f( delta[1], delta[0] ) + c - VG_PIf/2.0f;
+
+      knee[0] = sinf(-rot) * l1;
+      knee[1] = cosf(-rot) * l1;
+
+      m4x3_identity( final_mtx[ik->lower] );
+      m4x3_identity( final_mtx[ik->upper] );
+
+      /* create rotation matrix */
+      v3f co_knee;
+      v3_muladds( co_base, v0, knee[0], co_knee );
+      v3_muladds( co_knee, v1, knee[1], co_knee );
+      vg_line( co_base, co_knee, 0xff00ff00 );
+
+      m4x3f transform;
+      v3_copy( vaxis, transform[0] );
+      v3_muls( v0, knee[0], transform[1] );
+      v3_muladds( transform[1], v1, knee[1], transform[1] );
+      v3_normalize( transform[1] );
+      v3_cross( transform[0], transform[1], transform[2] );
+      v3_copy( co_base, transform[3] );
+
+      m3x3_mul( transform, ik->ia, transform );
+      m4x3_copy( transform, final_mtx[ik->lower] );
+
+      /* upper/knee bone */
+      v3_copy( vaxis, transform[0] );
+      v3_sub( co_target, co_knee, transform[1] );
+      v3_normalize( transform[1] );
+      v3_cross( transform[0], transform[1], transform[2] );
+      v3_copy( co_knee, transform[3] );
+
+      m3x3_mul( transform, ik->ib, transform );
+      m4x3_copy( transform, final_mtx[ik->upper] );
+   }
+}
+
+/*
+ * Applies the typical operations that you want for an IK rig: 
+ *    Pose, IK, Pose(deferred), Inverses, Transform
+ */
+static void skeleton_apply_standard( struct skeleton *skele, mdl_keyframe *pose,
+                                     m4x3f transform, m4x3f *final_mtx ){
+   skeleton_apply_pose( skele, pose, k_anim_apply_defer_ik, final_mtx );
+   skeleton_apply_ik_pass( skele, final_mtx );
+   skeleton_apply_pose( skele, pose, k_anim_apply_deffered_only, final_mtx );
+   skeleton_apply_inverses( skele, final_mtx );
+   skeleton_apply_transform( skele, transform, final_mtx );
+}
+
+/*
+ * Get an animation by name
+ */
+static struct skeleton_anim *skeleton_get_anim( struct skeleton *skele,
+                                                   const char *name ){
+   for( u32 i=0; i<skele->anim_count; i++ ){
+      struct skeleton_anim *anim = &skele->anims[i];
+
+      if( !strcmp( anim->name, name ) )
+         return anim;
+   }
+
+   vg_error( "skeleton_get_anim( *, \"%s\" )\n", name );
+   vg_fatal_error( "Invalid animation name\n" );
+
+   return NULL;
+}
+
+static void skeleton_alloc_from( struct skeleton *skele,
+                                    void *lin_alloc,
+                                    mdl_context *mdl,
+                                    mdl_armature *armature ){
+   skele->bone_count     = armature->bone_count+1;
+   skele->anim_count     = armature->anim_count;
+   skele->ik_count       = 0;
+   skele->collider_count = 0;
+
+   for( u32 i=0; i<armature->bone_count; i++ ){
+      mdl_bone *bone = mdl_arritm( &mdl->bones, armature->bone_start+i );
+
+      if( bone->flags & k_bone_flag_ik )
+         skele->ik_count ++;
+
+      if( bone->collider )
+         skele->collider_count ++;
+   }
+
+   u32 bone_size = sizeof(struct skeleton_bone) * skele->bone_count,
+       ik_size   = sizeof(struct skeleton_ik)   * skele->ik_count,
+       mtx_size  = sizeof(m4x3f)                * skele->bone_count,
+       anim_size = sizeof(struct skeleton_anim) * skele->anim_count;
+
+   skele->bones      = vg_linear_alloc( lin_alloc, bone_size );
+   skele->ik         = vg_linear_alloc( lin_alloc, ik_size );
+   //skele->final_mtx  = vg_linear_alloc( lin_alloc, mtx_size );
+   skele->anims      = vg_linear_alloc( lin_alloc, anim_size );
+
+   memset( skele->bones, 0, bone_size );
+   memset( skele->ik, 0, ik_size );
+   //memset( skele->final_mtx, 0, mtx_size );
+   memset( skele->anims, 0, anim_size );
+}
+
+static void skeleton_fatal_err(void){
+   vg_fatal_error( "Skeleton setup failed" );
+}
+
+/* Setup an animated skeleton from model. mdl's metadata should stick around */
+static void skeleton_setup( struct skeleton *skele,
+                            void *lin_alloc, mdl_context *mdl ){
+   u32 ik_count = 0, collider_count = 0;
+   skele->bone_count = 0;
+   skele->bones = NULL;
+   //skele->final_mtx = NULL;
+   skele->anims = NULL;
+
+   if( !mdl->armatures.count ){
+      vg_error( "No skeleton in model\n" );
+      skeleton_fatal_err();
+   }
+
+   mdl_armature *armature = mdl_arritm( &mdl->armatures, 0 );
+   skeleton_alloc_from( skele, lin_alloc, mdl, armature );
+
+   for( u32 i=0; i<armature->bone_count; i++ ){
+      mdl_bone *bone = mdl_arritm( &mdl->bones, armature->bone_start+i );
+      struct skeleton_bone *sb = &skele->bones[i+1];
+
+      v3_copy( bone->co, sb->co );
+      v3_copy( bone->end, sb->end );
+
+      sb->parent = bone->parent;
+      sb->name   = mdl_pstr( mdl, bone->pstr_name );
+      sb->flags  = bone->flags;
+      sb->collider = bone->collider;
+      sb->orig_bone = bone;
+
+      if( sb->flags & k_bone_flag_ik ){
+         skele->bones[ sb->parent ].flags |= k_bone_flag_ik;
+         
+         if( ik_count == skele->ik_count ){
+            vg_error( "Too many ik bones, corrupt model file\n" );
+            skeleton_fatal_err();
+         }
+
+         struct skeleton_ik *ik = &skele->ik[ ik_count ++ ];
+         ik->upper = i+1;
+         ik->lower = bone->parent;
+         ik->target = bone->ik_target;
+         ik->pole = bone->ik_pole;
+      }
+
+      box_copy( bone->hitbox, sb->hitbox );
+
+      if( bone->collider ){
+         if( collider_count == skele->collider_count ){
+            vg_error( "Too many collider bones\n" );
+            skeleton_fatal_err();
+         }
+
+         collider_count ++;
+      }
+   }
+
+   /* fill in implicit root bone */
+   v3_zero( skele->bones[0].co );
+   v3_copy( (v3f){0.0f,1.0f,0.0f}, skele->bones[0].end );
+   skele->bones[0].parent = 0xffffffff;
+   skele->bones[0].flags = 0;
+   skele->bones[0].name = "[root]";
+   
+   /* process animation quick refs */
+   for( u32 i=0; i<skele->anim_count; i++ ){
+      mdl_animation *anim = 
+         mdl_arritm( &mdl->animations, armature->anim_start+i );
+
+      skele->anims[i].rate       = anim->rate;
+      skele->anims[i].length     = anim->length;
+      skele->anims[i].name       = mdl_pstr(mdl, anim->pstr_name);
+      skele->anims[i].anim_data  = 
+         mdl_arritm( &mdl->keyframes, anim->offset );
+
+      vg_info( "animation[ %f, %u ] '%s'\n", anim->rate,
+                                             anim->length,
+                                             skele->anims[i].name );
+   }
+
+   skeleton_create_inverses( skele );
+   vg_success( "Loaded skeleton with %u bones\n", skele->bone_count );
+   vg_success( "                     %u colliders\n", skele->collider_count );
+}
+
+static void skeleton_debug( struct skeleton *skele, m4x3f *final_mtx ){
+   for( u32 i=1; i<skele->bone_count; i ++ ){
+      struct skeleton_bone *sb = &skele->bones[i];
+
+      v3f p0, p1;
+      v3_copy( sb->co, p0 );
+      v3_add( p0, sb->end, p1 );
+
+      m4x3_mulv( final_mtx[i], p0, p0 );
+      m4x3_mulv( final_mtx[i], p1, p1 );
+
+      if( sb->flags & k_bone_flag_deform ){
+         if( sb->flags & k_bone_flag_ik ){
+            vg_line( p0, p1, 0xff0000ff );
+         }
+         else{
+            vg_line( p0, p1, 0xffcccccc );
+         }
+      }
+      else
+         vg_line( p0, p1, 0xff00ffff );
+   }
+}
diff --git a/src/steam.c b/src/steam.c
new file mode 100644 (file)
index 0000000..f28b8e7
--- /dev/null
@@ -0,0 +1,303 @@
+#include "vg/vg_steam.h"
+#include "vg/vg_steam_utils.h"
+#include "vg/vg_steam_networking.h"
+#include "vg/vg_steam_auth.h"
+#include "vg/vg_steam_http.h"
+#include "vg/vg_steam_friends.h"
+#include "vg/vg_steam_user_stats.h"
+#include "submodules/anyascii/impl/c/anyascii.c"
+#include "skaterift.h"
+#include <string.h>
+
+/*
+ * We only want to use steamworks if building for the networked version,
+ * theres not much point otherwise. We mainly want steamworks for setting
+ * achievements etc.. so that includes our own server too.
+ *
+ * This file also wraps the functions and interfaces that we want to use to 
+ * make them a bit easier to read, since they are the flat API they have very 
+ * long names. in non-networked builds they will return default errors or do
+ * nothing.
+ */
+
+char steam_username_at_startup[128] = "Unassigned";
+
+static void recv_steam_warning( int severity, const char *msg )
+{
+   if( severity == 0 )
+      vg_low( "%s\n", msg );
+   else
+      vg_info( "%s\n", msg );
+}
+
+int steam_ready = 0,
+    steam_stats_ready = 0;
+
+void *hSteamNetworkingSockets, *hSteamUser, *hSteamUserStats;
+static HSteamPipe hSteamClientPipe;
+
+static const char *steam_achievement_names[] = 
+{
+   "ALBERT", "MARC", "JANET", "BERNADETTA",
+   "ROUTE_MPY", "ROUTE_MPG", "ROUTE_MPB", "ROUTE_MPR",
+   "ROUTE_TO", "ROUTE_TC", "CITY_COMPLETE", "MTZERO_SILVER", "MTZERO_GOLD",
+   "80FT"
+};
+
+void steam_store_achievements(void)
+{
+   if( steam_ready && steam_stats_ready ){
+      SteamAPI_ISteamUserStats_StoreStats( hSteamUserStats );
+   }
+}
+
+void update_ach_models(void);
+void steam_set_achievement( const char *name )
+{
+   if( skaterift.demo_mode )
+      return;
+
+   /* hack lol */
+   if( !strcmp(name,"MARC") ) skaterift.achievements |= 0x1;
+   if( !strcmp(name,"ALBERT") ) skaterift.achievements |= 0x2;
+   if( !strcmp(name,"JANET") ) skaterift.achievements |= 0x4;
+   if( !strcmp(name,"BERNADETTA") ) skaterift.achievements |= 0x8;
+   update_ach_models();
+
+   if( steam_ready && steam_stats_ready ){
+      if( SteamAPI_ISteamUserStats_SetAchievement( hSteamUserStats, name ) ){
+         vg_success( "Achievement set! '%s'\n", name );
+
+      }
+      else{
+         vg_warn( "Failed to set achievement: %s\n", name );
+      }
+   }
+   else{
+      vg_warn( "Failed to set achievement (steam not ready): %s\n", name );
+   }
+}
+
+void steam_clear_achievement( const char *name )
+{
+   if( steam_ready && steam_stats_ready ){
+      if( SteamAPI_ISteamUserStats_ClearAchievement( hSteamUserStats, name ) ){
+         vg_info( "Achievement cleared: '%s'\n", name );
+      }
+      else{
+         vg_warn( "Failed to clear achievement: %s\n", name );
+      }
+   }
+   else{
+      vg_warn( "Failed to clear achievement (steam not ready): %s\n", name );
+   }
+}
+
+
+void steam_print_all_achievements(void)
+{
+   vg_info( "Achievements: \n" );
+
+   if( steam_ready && steam_stats_ready ){
+      for( int i=0; i<VG_ARRAY_LEN(steam_achievement_names); i++ ){
+         steamapi_bool set = 0;
+         const char *name = steam_achievement_names[i];
+
+         if( SteamAPI_ISteamUserStats_GetAchievement( 
+                  hSteamUserStats, name, &set ) )
+         {
+            vg_info( "  %s %s\n", (set? "[YES]": "[   ]"), name );
+         }
+         else{
+            vg_warn( "  Error while fetching achievement status '%s'\n", name );
+         }
+      }
+   }
+   else{
+      vg_warn( "  Steam is not initialized, no results\n" );
+   }
+}
+
+int steam_achievement_ccmd( int argc, char const *argv[] )
+{
+   if( !(steam_ready && steam_stats_ready) ) return 1;
+
+   if( argc == 1 ){
+      if( !strcmp( argv[0], "list" ) ){
+         steam_print_all_achievements();
+         return 0;
+      }
+      else if( !strcmp( argv[0], "clearall" )){
+         for( int i=0; i<VG_ARRAY_LEN(steam_achievement_names); i++ )
+            steam_clear_achievement( steam_achievement_names[i] );
+         
+         steam_store_achievements();
+      }
+   }
+
+   if( argc == 2 ){
+      if( !strcmp( argv[0], "set" ) ){
+         steam_set_achievement( argv[1] );
+         steam_store_achievements();
+         return 0;
+      }
+      else if( strcmp( argv[0], "clear" ) ){
+         steam_clear_achievement( argv[1] );
+         steam_store_achievements();
+         return 0;
+      }
+   }
+
+   return 1;
+}
+
+static void steam_on_recieve_current_stats( CallbackMsg_t *msg )
+{
+   UserStatsReceived_t *rec = (UserStatsReceived_t *)msg->m_pubParam;
+
+   if( rec->m_eResult == k_EResultOK ){
+      vg_info( "Recieved stats for: %lu (user: %lu)\n", rec->m_nGameID,
+                                                        rec->m_steamIDUser );
+      steam_stats_ready = 1;
+
+      steamapi_bool set = 0;
+      if( SteamAPI_ISteamUserStats_GetAchievement( 
+               hSteamUserStats, "MARC", &set ) ){
+         if( set ) skaterift.achievements |= 0x1;
+      }
+      if( SteamAPI_ISteamUserStats_GetAchievement( 
+               hSteamUserStats, "ALBERT", &set ) ){
+         if( set ) skaterift.achievements |= 0x2;
+      }
+      if( SteamAPI_ISteamUserStats_GetAchievement( 
+               hSteamUserStats, "JANET", &set ) ){
+         if( set ) skaterift.achievements |= 0x4;
+      }
+      if( SteamAPI_ISteamUserStats_GetAchievement( 
+               hSteamUserStats, "BERNADETTA", &set ) ){
+         if( set ) skaterift.achievements |= 0x8;
+      }
+      update_ach_models();
+   }
+   else{
+      vg_error( "Error recieveing stats for user (%u)\n", rec->m_eResult );
+   }
+}
+
+static u32 utf8_byte0_byte_count( u8 char0 )
+{
+   for( u32 k=2; k<4; k++ ){
+      if( !(char0 & (0x80 >> k)) )
+         return k;
+   }
+
+   return 0;
+}
+
+u32 str_utf8_collapse( const char *str, char *buf, u32 length )
+{
+   u8 *ustr = (u8 *)str;
+   u32 utf32_code = 0x00000000;
+   u32 i=0, j=0, utf32_byte_ct=0;
+
+   for(;j < length-1;){
+      if( ustr[i] == 0x00 )
+         break;
+      
+      if( ustr[i] & 0x80 ){
+         if( utf32_byte_ct ){
+            utf32_byte_ct --;
+            utf32_code |= (ustr[i] & 0x3F) << (utf32_byte_ct*6);
+
+            if( !utf32_byte_ct ){
+               const char *match;
+               size_t chars = anyascii( utf32_code, &match );
+
+               for( u32 k=0; k<VG_MIN(chars, length-1-j); k++ ){
+                  buf[ j++ ] = (u8)match[k];
+               }
+            }
+         }
+         else{
+            utf32_byte_ct = utf8_byte0_byte_count( ustr[i] )-1;
+            utf32_code = ustr[i] & (0x3F >> utf32_byte_ct);
+            utf32_code <<= utf32_byte_ct*6;
+         }
+      }
+      else{
+         utf32_byte_ct = 0x00;
+         buf[j ++] = str[i];
+      }
+
+      i++;
+   }
+
+   buf[j] = 0x00;
+   return j;
+}
+
+int steam_init(void)
+{
+   const char *username = "offline player";
+
+   vg_info( "Initializing steamworks\n" );
+
+   if( !SteamAPI_Init() ){
+      printf("\n");
+      vg_error( "Steamworks failed to initialize\n" );
+      return 1;
+   }
+
+   steam_ready = 1;
+
+   SteamAPI_ManualDispatch_Init();
+
+   /* Connect interfaces */
+   hSteamClientPipe = SteamAPI_GetHSteamPipe();
+   hSteamNetworkingSockets = SteamAPI_SteamNetworkingSockets_SteamAPI();
+   hSteamUser = SteamAPI_SteamUser();
+
+   ISteamUtils *utils = SteamAPI_SteamUtils();
+   SteamAPI_ISteamUtils_SetWarningMessageHook( utils, recv_steam_warning );
+
+   printf("\n");
+   vg_success( "\nSteamworks API running\n" );
+
+   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
+   username = SteamAPI_ISteamFriends_GetPersonaName( hSteamFriends );
+
+   /*
+    * Request stats
+    * --------------------------------------------------------
+    */
+   hSteamUserStats = SteamAPI_SteamUserStats();
+   steam_register_callback( k_iUserStatsReceived,
+                            steam_on_recieve_current_stats );
+
+   if( !SteamAPI_ISteamUserStats_RequestCurrentStats( hSteamUserStats ) )
+      vg_warn( "No Steam Logon: Cannot request stats\n" );
+
+
+   vg_console_reg_cmd( "ach", steam_achievement_ccmd, NULL );
+
+   /* TODO: On username update callback */
+   str_utf8_collapse( username, steam_username_at_startup, 
+                        VG_ARRAY_LEN(steam_username_at_startup) );
+
+   return 1;
+}
+
+void steam_update(void)
+{
+   if( steam_ready ){
+      steamworks_event_loop( hSteamClientPipe );
+   }
+}
+
+void steam_end(void)
+{
+   if( steam_ready ){
+      vg_info( "Shutting down\n..." );
+      SteamAPI_Shutdown();
+   }
+}
diff --git a/src/steam.h b/src/steam.h
new file mode 100644 (file)
index 0000000..e2ed982
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ * All trademarks are property of their respective owners
+ */
+#pragma once
+
+extern int steam_ready, steam_stats_ready;
+extern void *hSteamNetworkingSockets, *hSteamUser, *hSteamUserStats;
+extern char steam_username_at_startup[128];
+
+int steam_init(void);
+void steam_update(void);
+void steam_end(void);
+u32 str_utf8_collapse( const char *str, char *buf, u32 length );
+int steam_achievement_ccmd( int argc, char const *argv[] );
+void steam_print_all_achievements(void);
+void steam_clear_achievement( const char *name );
+void steam_set_achievement( const char *name );
+void steam_store_achievements(void);
diff --git a/src/traffic.h b/src/traffic.h
new file mode 100644 (file)
index 0000000..004c624
--- /dev/null
@@ -0,0 +1,222 @@
+#ifndef TRAFFIC_H
+#define TRAFFIC_H
+
+#include "common.h"
+#include "model.h"
+#include "rigidbody.h"
+#include "world.h"
+
+typedef struct traffic_node traffic_node;
+typedef struct traffic_driver traffic_driver;
+
+struct traffic_node
+{
+   v3f co, h;
+
+   union
+   {
+      struct{ traffic_node *next, *next1; };
+      struct{ mdl_node *mn_next, *mn_next1; };
+   };
+};
+
+struct traffic_driver
+{
+   m4x3f transform;
+
+   traffic_node *current;
+   int option;
+   float t, speed;
+};
+
+static float eval_bezier_length( v3f p0, v3f p1, v3f h0, v3f h1, int res )
+{
+   float length = 0.0f, m = 1.0f/(float)res;
+   v3f l, p;
+   v3_copy( p0, l );
+
+   for( int i=0; i<res; i++ )
+   {
+      float t = (float)(i+1)*m;
+      eval_bezier_time(p0,p1,h0,h1,t,p);
+      length += v3_dist( p,l );
+      v3_copy( p, l );
+   }
+
+   return length;
+}
+
+static void traffic_finalize( traffic_node *system, int count )
+{
+   for( int i=0; i<count; i++ )
+   {
+      traffic_node *tn = &system[i];
+
+      if( tn->mn_next )
+         tn->next = &system[ tn->mn_next->sub_uid ];
+      if( tn->mn_next1 )
+         tn->next1 = &system[ tn->mn_next1->sub_uid ];
+   }
+}
+
+static void traffic_visualize_link( traffic_node *ta, traffic_node *tb )
+{
+   v3f p0, p1, h0, h1, p, l;
+
+   if( !tb ) return;
+
+   v3_copy( ta->co, p0 );
+   v3_muladds( ta->co, ta->h,  1.0f, h0 );
+   v3_copy( tb->co, p1 );
+   v3_muladds( tb->co, tb->h, -1.0f, h1 );
+   v3_copy( p0, l );
+
+   vg_line_pt3( h0, 0.2f, 0xff00ff00 );
+   vg_line_pt3( h1, 0.2f, 0xffff00ff );
+   vg_line( p0, h0, 0xff000000 );
+   vg_line( p1, h1, 0xff000000 );
+
+   for( int i=0; i<5; i++ )
+   {
+      float t = (float)(i+1)/5.0f;
+      eval_bezier_time( p0, p1, h0, h1, t, p );
+
+      vg_line( p, l, 0xffffffff );
+      v3_copy( p, l );
+   }
+}
+
+static void sample_wheel_floor( v3f pos )
+{
+   v3f ground;
+   v3_copy( pos, ground );
+   ground[1] += 4.0f;
+   
+   ray_hit hit;
+   hit.dist = 8.0f;
+
+   if( ray_world( ground, (v3f){0.0f,-1.0f,0.0f}, &hit ))
+   {
+      v3_copy( hit.pos, pos );
+   }
+}
+
+static void traffic_drive( traffic_driver *driver )
+{
+   traffic_node *next, *current = driver->current;
+
+   if( !current ) return;
+   next = driver->option==0? current->next: current->next1;
+   
+   if( driver->t > 1.0f )
+   {
+      driver->t = driver->t - floorf( driver->t );
+      driver->current = driver->option==0? current->next: current->next1;
+      driver->option = 0;
+      
+      current = driver->current;
+      if( !current )
+         return;
+
+      if( current->next && current->next1 )
+         if( vg_randf() > 0.5f )
+            driver->option = 1;
+   }
+
+   traffic_visualize_link( current, next );
+
+   /*
+    * Calculate the speed of the curve at the current point. On the reference
+    * curve the rate should come out to be exactly 1 ktimestep traveled.
+    * Dividing this distance by ktimestep gives us the modifier to use.
+    */
+   v3f p0,p1,h0,h1,pc,pn;
+   
+   v3_copy( current->co, p0 );
+   v3_muladds( current->co, current->h, 1.0f, h0 );
+   v3_copy( next->co, p1 );
+   v3_muladds( next->co, next->h, -1.0f, h1 );
+
+   eval_bezier_time( p0,p1,h0,h1, driver->t, pc );
+   eval_bezier_time( p0,p1,h0,h1, driver->t + vg.time_delta, pn );
+
+   float mod = vg.time_delta / v3_dist( pc, pn );
+   v3f dir,side,up;
+   v3_sub( pn, pc, dir );
+   v3_normalize(dir);
+   
+   /*
+    * Stick the car on the ground by casting rays where the wheels are
+    */
+   side[0] = -dir[2];
+   side[1] =  0.0f;
+   side[2] =  dir[0];
+   v3_normalize(side);
+
+   v3f fl, fr, bc;
+   v3_muladds( pc, dir, 2.0f, fr );
+   v3_muladds( pc, dir, 2.0f, fl );
+   v3_muladds( pc, dir, -2.0f, bc );
+   v3_muladds( fr, side, 1.0f, fr );
+   v3_muladds( fl, side, -1.0f, fl );
+
+   sample_wheel_floor( fl );
+   sample_wheel_floor( fr );
+   sample_wheel_floor( bc );
+
+   vg_line( fl, fr, 0xff00ffff );
+   vg_line( fr, bc, 0xff00ffff );
+   vg_line( bc, fl, 0xff00ffff );
+
+   v3f norm;
+   v3f v0, v1;
+   v3_sub( fr, fl, v0 );
+   v3_sub( bc, fl, v1 );
+   v3_cross( v1, v0, norm );
+   v3_normalize( norm );
+
+   /* 
+    * Jesus take the wheel
+    */
+   float steer_penalty = 1.0f-v3_dot( dir, driver->transform[0] );
+   steer_penalty /= vg.time_delta;
+   steer_penalty *= 30.0f;
+   
+   float target_speed = vg_maxf( 16.0f * (1.0f-steer_penalty), 0.1f ),
+         accel = target_speed - driver->speed;
+   driver->speed = stable_force( driver->speed, accel*vg.time_delta*2.0f );
+   driver->t += driver->speed*mod*vg.time_delta;
+
+   /* 
+    * Update transform
+    */
+   v3_cross( dir, norm, side );
+   v3_copy( dir, driver->transform[0] );
+   v3_copy( norm, driver->transform[1] );
+   v3_copy( side, driver->transform[2] );
+
+   v3_add( fl, fr, pc );
+   v3_add( bc, pc, pc );
+   v3_muls( pc, 1.0f/3.0f, pc );
+   v3_copy( pc, driver->transform[3] );
+}
+
+static void traffic_visualize( traffic_node *system, int count )
+{
+   for( int i=0; i<count; i++ )
+   {
+      traffic_node *tn = &system[i];
+
+      traffic_visualize_link( tn, tn->next );
+      traffic_visualize_link( tn, tn->next1 );
+   }
+}
+
+static void traffic_visualize_car( traffic_driver *driver )
+{
+   vg_line_boxf_transformed( driver->transform, 
+                                       (boxf){{-1.0f,0.0f,-0.5f},
+                                              { 1.0f,0.0f, 0.5f}}, 0xff00ff00 );
+}
+
+#endif /* TRAFFIC_H */
diff --git a/src/trail.c b/src/trail.c
new file mode 100644 (file)
index 0000000..376a009
--- /dev/null
@@ -0,0 +1,187 @@
+#pragma once
+#include "vg/vg_engine.h"
+#include "vg/vg_platform.h"
+#include "vg/vg_m.h"
+#include "vg/vg_lines.h"
+#include "vg/vg_async.h"
+#include "vg/vg_camera.h"
+#include "trail.h"
+#include "shaders/particle.h"
+#include "shaders/trail.h"
+
+static void trail_increment( trail_system *sys ){
+   sys->head ++;
+
+   if( sys->head == sys->max )
+      sys->head = 0;
+
+   /* undesirable effect: will remove active points if out of space! */
+   if( sys->count < sys->max )
+      sys->count ++;
+}
+
+void trail_system_update( trail_system *sys, f32 dt,
+                          v3f co, v3f normal, f32 alpha )
+{
+   /* update existing points and clip dead ones */
+   bool clip_allowed = 1;
+   for( i32 i=0; i<sys->count; i ++ ){
+      i32 i0 = sys->head - sys->count + i;
+      if( i0 < 0 ) i0 += sys->max;
+
+      trail_point *p0 = &sys->array[i0];
+      p0->alpha -= dt/sys->lifetime;
+
+      if( clip_allowed ){
+         if( p0->alpha <= 0.0f )
+            sys->count --;
+         else
+            clip_allowed = 0;
+      }
+   }
+
+   i32 icur  = sys->head -1,
+       iprev = sys->head -2,
+       ihead = sys->head;
+
+   if( icur  < 0 ) icur += sys->max;
+   if( iprev < 0 ) iprev += sys->max;
+
+   trail_point *pcur  = &sys->array[ icur ],
+               *pprev = &sys->array[ iprev ],
+               *phead = &sys->array[ ihead ],
+               *pdest = NULL;
+   v3f dir;
+
+   f32 k_min = 0.001f;
+
+   if( sys->count == 0 ){
+      trail_increment( sys );
+      v3_copy( (v3f){0,0,-1}, dir );
+      pdest = phead;
+   }
+   else if( sys->count == 1 ){
+      if( v3_dist2( pcur->co, co ) < k_min*k_min )
+         return;
+
+      trail_increment( sys );
+      pdest = phead;
+      v3_sub( co, pcur->co, dir );
+   }
+   else {
+      if( v3_dist2( pprev->co, co ) < k_min*k_min )
+         return;
+
+      if( v3_dist2( pprev->co, co ) > sys->min_dist*sys->min_dist ){
+         trail_increment( sys );
+         pdest = phead;
+      }
+      else
+         pdest = pcur;
+
+      v3_sub( co, pprev->co, dir );
+   }
+
+   v3_cross( dir, normal, pdest->right );
+   v3_normalize( pdest->right );
+   v3_copy( co, pdest->co );
+   v3_copy( normal, pdest->normal );
+   pdest->alpha = alpha;
+}
+
+void trail_system_debug( trail_system *sys )
+{
+   for( i32 i=0; i<sys->count; i ++ ){
+      i32 i0 = sys->head - sys->count + i;
+      if( i0 < 0 ) i0 += sys->max;
+
+      trail_point *p0 = &sys->array[i0];
+      vg_line_point( p0->co, 0.04f, 0xff000000 | (u32)(p0->alpha*255.0f) );
+      vg_line_arrow( p0->co, p0->right, 0.3f, VG__GREEN );
+
+      if( i == sys->count-1 ) break;
+
+      i32 i1 = i0+1;
+      if( i1 == sys->max ) i1 = 0;
+
+      trail_point *p1 = &sys->array[i1];
+      vg_line( p0->co, p1->co, VG__RED );
+   }
+}
+
+struct trail_init_args {
+   trail_system *sys;
+};
+
+void async_trail_init( void *payload, u32 size )
+{
+   struct trail_init_args *args = payload;
+   trail_system *sys = args->sys;
+
+   glGenVertexArrays( 1, &sys->vao );
+   glGenBuffers( 1, &sys->vbo );
+   glBindVertexArray( sys->vao );
+
+   size_t stride = sizeof(trail_vert);
+
+   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
+   glBufferData( GL_ARRAY_BUFFER, sys->max*stride*2, NULL, GL_DYNAMIC_DRAW );
+
+   /* 0: coordinates */
+   glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, stride, (void*)0 );
+   glEnableVertexAttribArray( 0 );
+}
+
+void trail_alloc( trail_system *sys, u32 max )
+{
+   size_t stride = sizeof(trail_vert);
+   sys->max = max;
+   sys->array = vg_linear_alloc( vg_mem.rtmemory, max*sizeof(trail_point) );
+   sys->vertices = vg_linear_alloc( vg_mem.rtmemory, max*stride*2 );
+
+   vg_async_item *call = vg_async_alloc( sizeof(struct trail_init_args) );
+
+   struct trail_init_args *init = call->payload;
+   init->sys = sys;
+   vg_async_dispatch( call, async_trail_init );
+}
+
+void trail_system_prerender( trail_system *sys )
+{
+   if( sys->count < 2 ) return;
+
+   for( i32 i=0; i<sys->count; i ++ ){
+      i32 i0 = sys->head - sys->count + i;
+      if( i0 < 0 ) i0 += sys->max;
+
+      trail_point *p0 = &sys->array[i0];
+      trail_vert *v0 = &sys->vertices[i*2+0],
+                 *v1 = &sys->vertices[i*2+1];
+
+      v3_muladds( p0->co, p0->right, -sys->width, v0->co );
+      v3_muladds( p0->co, p0->right,  sys->width, v1->co );
+      v0->co[3] = p0->alpha;
+      v1->co[3] = p0->alpha;
+   }
+
+   glBindVertexArray( sys->vao );
+
+   size_t stride = sizeof(trail_vert);
+   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
+   glBufferSubData( GL_ARRAY_BUFFER, 0, sys->count*stride*2, sys->vertices );
+}
+
+void trail_system_render( trail_system *sys, vg_camera *cam )
+{
+   if( sys->count < 2 ) return;
+   glDisable( GL_CULL_FACE );
+   glEnable( GL_DEPTH_TEST );
+
+   shader_trail_use();
+   shader_trail_uPv( cam->mtx.pv );
+   shader_trail_uPvPrev( cam->mtx_prev.pv );
+   shader_trail_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
+
+       glBindVertexArray( sys->vao );
+   glDrawArrays( GL_TRIANGLE_STRIP, 0, sys->count*2 );
+}
diff --git a/src/trail.h b/src/trail.h
new file mode 100644 (file)
index 0000000..82c7d60
--- /dev/null
@@ -0,0 +1,33 @@
+#pragma once
+
+typedef struct trail_system trail_system;
+typedef struct trail_point trail_point;
+typedef struct trail_vert trail_vert;
+
+struct trail_system {
+   struct trail_point {
+      v3f co, normal, right;
+      f32 alpha;
+   }
+   *array;
+
+#pragma pack(push,1)
+   struct trail_vert {
+      v4f co; /* xyz: position, w: alpha */
+   }
+   *vertices;
+#pragma pack(pop)
+
+   i32 head, count, max;
+   GLuint vao, vbo;
+
+   /* render settings */
+   f32 width, lifetime, min_dist;
+};
+
+void trail_alloc( trail_system *sys, u32 max );
+void trail_system_update( trail_system *sys, f32 dt, v3f co, 
+                          v3f normal, f32 alpha );
+void trail_system_debug( trail_system *sys );
+void trail_system_prerender( trail_system *sys );
+void trail_system_render( trail_system *sys, vg_camera *cam );
diff --git a/src/vehicle.c b/src/vehicle.c
new file mode 100644 (file)
index 0000000..c0a3376
--- /dev/null
@@ -0,0 +1,279 @@
+#include "skaterift.h"
+#include "vehicle.h"
+#include "scene_rigidbody.h"
+
+struct drivable_vehicle gzoomer =
+{
+   .rb.co = {-2000,-2000,-2000}
+};
+
+int spawn_car( int argc, const char *argv[] )
+{
+   v3f ra, rb, rx;
+   v3_copy( g_render.cam.pos, ra );
+   v3_muladds( ra, g_render.cam.transform[2], -10.0f, rb );
+
+   float t;
+   if( spherecast_world( world_current_instance(), 
+                         ra, rb, 1.0f, &t, rx, 0 ) != -1 )
+   {
+      v3_lerp( ra, rb, t, gzoomer.rb.co );
+      gzoomer.rb.co[1] += 4.0f;
+      q_axis_angle( gzoomer.rb.q, (v3f){1.0f,0.0f,0.0f}, 0.001f );
+      v3_zero( gzoomer.rb.v );
+      v3_zero( gzoomer.rb.w );
+      
+      rb_update_matrices( &gzoomer.rb );
+      gzoomer.alive = 1;
+
+      vg_success( "Spawned car\n" );
+   }
+   else{
+      vg_error( "Can't spawn here\n" );
+   }
+
+   return 0;
+}
+
+void vehicle_init(void)
+{
+   q_identity( gzoomer.rb.q );
+   v3_zero( gzoomer.rb.w );
+   v3_zero( gzoomer.rb.v );
+   v3_zero( gzoomer.rb.co );
+   rb_setbody_sphere( &gzoomer.rb, 1.0f, 8.0f, 1.0f );
+
+   VG_VAR_F32( k_car_spring,        flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_spring_damp,   flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_spring_length, flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_wheel_radius,  flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_friction_lat,  flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_friction_roll, flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_drive_force,   flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_air_resistance,flags=VG_VAR_PERSISTENT );
+   VG_VAR_F32( k_car_downforce,     flags=VG_VAR_PERSISTENT );
+
+   VG_VAR_I32( gzoomer.inside );
+
+   vg_console_reg_cmd( "spawn_car", spawn_car, NULL );
+
+   v3_copy((v3f){ -1.0f, -0.25f, -1.5f }, gzoomer.wheels_local[0] );
+   v3_copy((v3f){  1.0f, -0.25f, -1.5f }, gzoomer.wheels_local[1] );
+   v3_copy((v3f){ -1.0f, -0.25f,  1.5f }, gzoomer.wheels_local[2] );
+   v3_copy((v3f){  1.0f, -0.25f,  1.5f }, gzoomer.wheels_local[3] );
+}
+
+void vehicle_wheel_force( int index )
+{
+   v3f pa, pb, n;
+   m4x3_mulv( gzoomer.rb.to_world, gzoomer.wheels_local[index], pa );
+   v3_muladds( pa, gzoomer.rb.to_world[1], -k_car_spring_length, pb );
+
+
+#if 1
+   float t;
+   if( spherecast_world( world_current_instance(), pa, pb, 
+                         k_car_wheel_radius, &t, n, 0 ) == -1 )
+   { t = 1.0f;
+   }
+
+#else
+
+   v3f dir;
+   v3_muls( gzoomer.rb.up, -1.0f, dir );
+   
+   ray_hit hit;
+   hit.dist = k_car_spring_length;
+   ray_world( pa, dir, &hit );
+
+   float t = hit.dist / k_car_spring_length;
+
+#endif
+
+   v3f pc;
+   v3_lerp( pa, pb, t, pc );
+
+   m4x3f mtx;
+   m3x3_copy( gzoomer.rb.to_world, mtx );
+   v3_copy( pc, mtx[3] );
+   vg_line_sphere( mtx, k_car_wheel_radius, VG__BLACK );
+   vg_line( pa, pc, VG__WHITE );
+   v3_copy( pc, gzoomer.wheels[index] );
+
+   if( t < 1.0f ){
+      /* spring force */
+      float Fv = (1.0f-t) * k_car_spring*vg.time_fixed_delta;
+      
+      v3f delta;
+      v3_sub( pa, gzoomer.rb.co, delta );
+
+      v3f rv;
+      v3_cross( gzoomer.rb.w, delta, rv );
+      v3_add( gzoomer.rb.v, rv, rv );
+
+      Fv += v3_dot(rv, gzoomer.rb.to_world[1]) 
+               * -k_car_spring_damp*vg.time_fixed_delta;
+
+      /* scale by normal incident */
+      Fv *= v3_dot( n, gzoomer.rb.to_world[1] );
+
+      v3f F;
+      v3_muls( gzoomer.rb.to_world[1], Fv, F );
+      rb_linear_impulse( &gzoomer.rb, delta, F );
+
+      /* friction vectors
+       * -------------------------------------------------------------*/
+      v3f tx, ty;
+      
+      if( index <= 1 )
+         v3_cross( gzoomer.steerv, n, tx );
+      else
+         v3_cross( n, gzoomer.rb.to_world[2], tx );
+      v3_cross( tx, n, ty );
+
+      v3_copy( tx, gzoomer.tangent_vectors[ index ][0] );
+      v3_copy( ty, gzoomer.tangent_vectors[ index ][1] );
+
+      gzoomer.normal_forces[ index ] = Fv;
+      gzoomer.tangent_forces[ index ][0] = 0.0f;
+      gzoomer.tangent_forces[ index ][1] = 0.0f;
+
+      /* orient inverse inertia tensors */
+      v3f raW;
+      m3x3_mulv( gzoomer.rb.to_world, gzoomer.wheels_local[index], raW );
+
+      v3f raCtx, raCtxI, raCty, raCtyI;
+      v3_cross( tx, raW, raCtx );
+      v3_cross( ty, raW, raCty );
+      m3x3_mulv( gzoomer.rb.iIw, raCtx, raCtxI );
+      m3x3_mulv( gzoomer.rb.iIw, raCty, raCtyI );
+
+      gzoomer.tangent_mass[index][0] = gzoomer.rb.inv_mass;
+      gzoomer.tangent_mass[index][0] += v3_dot( raCtx, raCtxI );
+      gzoomer.tangent_mass[index][0] = 1.0f/gzoomer.tangent_mass[index][0];
+      
+      gzoomer.tangent_mass[index][1] = gzoomer.rb.inv_mass;
+      gzoomer.tangent_mass[index][1] += v3_dot( raCty, raCtyI );
+      gzoomer.tangent_mass[index][1] = 1.0f/gzoomer.tangent_mass[index][1];
+
+      /* apply drive force */
+      if( index >= 2 ){
+         v3_muls( ty, -gzoomer.drive * k_car_drive_force 
+                                     * vg.time_fixed_delta, F );
+         rb_linear_impulse( &gzoomer.rb, raW, F );
+      }
+   }
+   else{
+      gzoomer.normal_forces[ index ] = 0.0f;
+      gzoomer.tangent_forces[ index ][0] = 0.0f;
+      gzoomer.tangent_forces[ index ][1] = 0.0f;
+   }
+}
+
+void vehicle_solve_friction(void)
+{
+   rigidbody *rb = &gzoomer.rb;
+   for( int i=0; i<4; i++ ){
+      v3f raW;
+      m3x3_mulv( rb->to_world, gzoomer.wheels_local[i], raW );
+
+      v3f rv;
+      v3_cross( rb->w, raW, rv );
+      v3_add( rb->v, rv, rv );
+
+      float     fx = k_car_friction_lat * gzoomer.normal_forces[i],
+                fy = k_car_friction_roll * gzoomer.normal_forces[i],
+               vtx = v3_dot( rv, gzoomer.tangent_vectors[i][0] ),
+               vty = v3_dot( rv, gzoomer.tangent_vectors[i][1] ),
+           lambdax = gzoomer.tangent_mass[i][0] * -vtx,
+           lambday = gzoomer.tangent_mass[i][1] * -vty;
+      
+      float tempx = gzoomer.tangent_forces[i][0],
+            tempy = gzoomer.tangent_forces[i][1];
+      gzoomer.tangent_forces[i][0] = vg_clampf( tempx + lambdax, -fx, fx );
+      gzoomer.tangent_forces[i][1] = vg_clampf( tempy + lambday, -fy, fy );
+      lambdax = gzoomer.tangent_forces[i][0] - tempx;
+      lambday = gzoomer.tangent_forces[i][1] - tempy;
+
+      v3f impulsex, impulsey;
+      v3_muls( gzoomer.tangent_vectors[i][0], lambdax, impulsex );
+      v3_muls( gzoomer.tangent_vectors[i][1], lambday, impulsey );
+      rb_linear_impulse( rb, raW, impulsex );
+      rb_linear_impulse( rb, raW, impulsey );
+   }
+}
+
+void vehicle_update_fixed(void)
+{
+   if( !gzoomer.alive )
+      return;
+
+   rigidbody *rb = &gzoomer.rb;
+
+   v3_muls( rb->to_world[2], -cosf(gzoomer.steer), gzoomer.steerv );
+   v3_muladds( gzoomer.steerv, rb->to_world[0], 
+               sinf(gzoomer.steer), gzoomer.steerv );
+
+   /* apply air resistance */
+   v3f Fair, Fdown;
+
+   v3_muls( rb->v, -k_car_air_resistance, Fair );
+   v3_muls( rb->to_world[1], -fabsf(v3_dot( rb->v, rb->to_world[2] )) *
+                                 k_car_downforce, Fdown );
+
+   v3_muladds( rb->v, Fair,  vg.time_fixed_delta, rb->v );
+   v3_muladds( rb->v, Fdown, vg.time_fixed_delta, rb->v );
+   
+   for( int i=0; i<4; i++ )
+      vehicle_wheel_force( i );
+
+   rigidbody _null = {0};
+   _null.inv_mass = 0.0f;
+   m3x3_zero( _null.iI );
+
+   rb_ct manifold[64];
+   int len = rb_sphere__scene( rb->to_world, 1.0f, NULL, 
+                               world_current_instance()->geo_bh,
+                               manifold, 0 );
+   for( int j=0; j<len; j++ ){
+      manifold[j].rba = rb;
+      manifold[j].rbb = &_null;
+   }
+   rb_manifold_filter_coplanar( manifold, len, 0.05f );
+
+   if( len > 1 ){
+      rb_manifold_filter_backface( manifold, len );
+      rb_manifold_filter_joint_edges( manifold, len, 0.05f );
+      rb_manifold_filter_pairs( manifold, len, 0.05f );
+   }
+   len = rb_manifold_apply_filtered( manifold, len );
+
+   rb_presolve_contacts( manifold, vg.time_fixed_delta, len );
+   for( int i=0; i<8; i++ ){
+      rb_solve_contacts( manifold, len );
+      vehicle_solve_friction();
+   }
+
+   rb_iter( rb );
+   rb_update_matrices( rb );
+}
+
+void vehicle_update_post(void)
+{
+   if( !gzoomer.alive )
+      return;
+
+   vg_line_sphere( gzoomer.rb.to_world, 1.0f, VG__WHITE );
+
+   /* draw friction vectors */
+   v3f p0, px, py;
+
+   for( int i=0; i<4; i++ ){
+      v3_copy( gzoomer.wheels[i], p0 );
+      v3_muladds( p0, gzoomer.tangent_vectors[i][0], 0.5f, px );
+      v3_muladds( p0, gzoomer.tangent_vectors[i][1], 0.5f, py );
+
+      vg_line( p0, px, VG__RED );
+      vg_line( p0, py, VG__GREEN );
+   }
+}
diff --git a/src/vehicle.h b/src/vehicle.h
new file mode 100644 (file)
index 0000000..3e60bb7
--- /dev/null
@@ -0,0 +1,42 @@
+#pragma once
+#include "vg/vg_rigidbody.h"
+#include "player.h"
+#include "world.h"
+#include "world_physics.h"
+
+static float k_car_spring = 1.0f,
+                k_car_spring_damp = 0.001f,
+                k_car_spring_length = 0.5f,
+                k_car_wheel_radius = 0.2f,
+                k_car_friction_lat = 0.6f,
+                k_car_friction_roll = 0.01f,
+                k_car_drive_force = 1.0f,
+                k_car_air_resistance = 0.1f,
+                k_car_downforce      = 0.5f;
+
+typedef struct drivable_vehicle drivable_vehicle;
+struct drivable_vehicle
+{
+   int alive, inside;
+   rigidbody rb;
+
+   v3f wheels[4];
+
+   float tangent_mass[4][2],
+         normal_forces[4],
+         tangent_forces[4][2];
+
+   float steer, drive;
+   v3f steerv;
+
+   v3f   tangent_vectors[4][2];
+   v3f   wheels_local[4];
+}
+extern gzoomer;
+
+int spawn_car( int argc, const char *argv[] );
+void vehicle_init(void);
+void vehicle_wheel_force( int index );
+void vehicle_solve_friction(void);
+void vehicle_update_fixed(void);
+void vehicle_update_post(void);
diff --git a/src/workshop.c b/src/workshop.c
new file mode 100644 (file)
index 0000000..7814b09
--- /dev/null
@@ -0,0 +1,1638 @@
+#include "vg/vg_engine.h"
+#include "vg/vg_tex.h"
+#include "vg/vg_image.h"
+#include "vg/vg_msg.h"
+#include "vg/vg_binstr.h"
+#include "vg/vg_loader.h"
+#include "vg/vg_io.h"
+#include "ent_skateshop.h"
+
+#include "vg/vg_steam_auth.h"
+#include "vg/vg_steam_ugc.h"
+#include "vg/vg_steam_friends.h"
+#include "steam.h"
+#include "workshop.h"
+
+struct workshop_form workshop_form;
+
+static struct ui_enum_opt workshop_form_visibility_opts[] = {
+ { k_ERemoteStoragePublishedFileVisibilityPublic,       "Public"       },
+ { k_ERemoteStoragePublishedFileVisibilityUnlisted,     "Unlisted"     },
+ { k_ERemoteStoragePublishedFileVisibilityFriendsOnly,  "Friends Only" },
+ { k_ERemoteStoragePublishedFileVisibilityPrivate,      "Private"      },
+};
+
+static struct ui_enum_opt workshop_form_type_opts[] = {
+ { k_addon_type_none,   "None"   },
+ { k_addon_type_board,  "Board"  },
+ { k_addon_type_world,  "World"  },
+ { k_addon_type_player, "Player" },
+};
+
+/* 
+ * Close the form and discard UGC query result
+ */
+static void workshop_quit_form(void){
+   player_board_unload( &workshop_form.board_model );
+   workshop_form.file_intent = k_workshop_form_file_intent_none;
+
+   if( workshop_form.ugc_query.result == k_EResultOK ){
+      workshop_form.ugc_query.result = k_EResultNone;
+
+      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+      SteamAPI_ISteamUGC_ReleaseQueryUGCRequest( 
+            hSteamUGC, workshop_form.ugc_query.handle );
+   }
+
+   workshop_form.page = k_workshop_form_hidden;
+   workshop_form.op = k_workshop_op_none;
+}
+
+/*
+ * Delete all information about the submission
+ */
+static void workshop_reset_submission_data(void)
+{
+   workshop_form.submission.file_id = 0; /* assuming id of 0 is none/invalid */
+   workshop_form.submission.description[0] = '\0';
+   workshop_form.submission.title[0] = '\0';
+   workshop_form.submission.author[0] = '\0';
+   workshop_form.submission.submission_type_selection = 
+      k_addon_type_none;
+   workshop_form.submission.type = k_addon_type_none;
+
+   workshop_form.submission.visibility =
+      k_ERemoteStoragePublishedFileVisibilityPublic;
+
+   workshop_form.addon_folder[0] = '\0';
+   player_board_unload( &workshop_form.board_model );
+   workshop_form.file_intent = k_workshop_form_file_intent_none;
+}
+
+
+/*
+ * Mostly copies of what it sais on the Steam API documentation
+ */
+static const char *workshop_EResult_user_string( EResult result )
+{
+   switch( result ){
+    case k_EResultInsufficientPrivilege:
+     return "Your account is currently restricted from uploading content "
+             "due to a hub ban, account lock, or community ban. You need to "
+             "contact Steam Support to resolve the issue."; 
+    case k_EResultBanned:
+     return "You do not have permission to upload content to this hub "
+             "because you have an active VAC or Game ban.";
+    case k_EResultTimeout:
+     return "The operation took longer than expected, so it was discarded. "
+            "Please try again.";
+    case k_EResultNotLoggedOn:
+     return "You are currently not logged into Steam.";
+    case k_EResultServiceUnavailable:
+     return "The workshop server is having issues or is unavailable, "
+            "please try again.";
+    case k_EResultInvalidParam:
+     return "One of the submission fields contains something not being " 
+            "accepted by that field.";
+    case k_EResultAccessDenied:
+     return "There was a problem trying to save the title and description. "
+            "Access was denied.";
+    case k_EResultLimitExceeded:
+     return "You have exceeded your Steam Cloud quota. If you wish to "
+            "upload this file, you must remove some published items.";
+    case k_EResultFileNotFound:
+     return "The uploaded file could not be found.";
+    case k_EResultDuplicateRequest:
+     return "The file was already successfully uploaded.";
+    case k_EResultDuplicateName:
+     return "You already have a Steam Workshop item with that name.";
+    case k_EResultServiceReadOnly:
+     return "Due to a recent password or email change, you are not allowed "
+             "to upload new content. Usually this restriction will expire in"
+             " 5 days, but can last up to 30 days if the account has been "
+             "inactive recently.";
+     default: 
+      return "Operation failed for an error which has not been accounted for "
+             "by the programmer. Try again, sorry :)";
+   }
+}
+
+/*
+ * op: k_workshop_form_op_publishing_update 
+ * ----------------------------------------------------------------------------
+ */
+
+/*
+ * The endpoint of this operation
+ */
+static void on_workshop_update_result( void *data, void *user )
+{
+   vg_info( "Recieved workshop update result\n" );
+   SubmitItemUpdateResult_t *result = data;
+
+   /* this seems to be set here, but my account definitely has accepted it */
+   if( result->m_bUserNeedsToAcceptWorkshopLegalAgreement ){
+      vg_warn( "Workshop agreement currently not accepted\n" );
+   }
+   
+   if( result->m_eResult == k_EResultOK ){
+      workshop_form.page = k_workshop_form_closing_good;
+      workshop_form.failure_or_success_string = "Uploaded workshop file!";
+      vg_success( "file uploaded\n" );
+   }
+   else{
+      workshop_form.page = k_workshop_form_closing_bad;
+      workshop_form.failure_or_success_string = 
+         workshop_EResult_user_string( result->m_eResult );
+
+      vg_error( "Error with the submitted file (%d)\n", result->m_eResult );
+   }
+   workshop_form.op = k_workshop_op_none;
+}
+
+static const char *workshop_filetype_folder(void){
+   enum addon_type type = workshop_form.submission.type;
+   if     ( type == k_addon_type_board )  return "boards/";
+   else if( type == k_addon_type_player ) return "playermodels/";
+   else if( type == k_addon_type_world )  return "maps/";
+
+   return "unknown_addon_type/";
+}
+
+/*
+ * reciever on completion of packaging the files, it will then start the item
+ * update with Steam API
+ */
+static void workshop_form_upload_submission( PublishedFileId_t file_id,
+                                                char *metadata )
+{
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+   UGCUpdateHandle_t handle 
+      = SteamAPI_ISteamUGC_StartItemUpdate( hSteamUGC, SKATERIFT_APPID,
+                                            file_id );
+
+   /* TODO: Handle failure cases for these */
+
+   SteamAPI_ISteamUGC_SetItemMetadata( hSteamUGC, handle, metadata );
+
+   if( workshop_form.submission.submit_title ){
+      vg_info( "Setting title\n" );
+      SteamAPI_ISteamUGC_SetItemTitle( hSteamUGC, handle, 
+                                       workshop_form.submission.title );
+   }
+
+   if( workshop_form.submission.submit_description ){
+      vg_info( "Setting description\n" );
+      SteamAPI_ISteamUGC_SetItemDescription( hSteamUGC, handle, 
+                                       workshop_form.submission.description);
+   }
+
+   if( workshop_form.submission.submit_file_and_image ){
+      char path_buf[4096];
+      vg_str folder;
+      vg_strnull( &folder, path_buf, 4096 );
+      vg_strcat( &folder, vg.base_path );
+
+      vg_strcat( &folder, workshop_filetype_folder() );
+      vg_strcat( &folder, workshop_form.addon_folder );
+
+      vg_info( "Setting item content\n" );
+      SteamAPI_ISteamUGC_SetItemContent( hSteamUGC, handle, folder.buffer );
+      
+      vg_str preview = folder;
+      vg_strcat( &preview, "/preview.jpg" );
+
+      vg_info( "Setting preview image\n" );
+      SteamAPI_ISteamUGC_SetItemPreview( hSteamUGC, handle, preview.buffer );
+   }
+
+   vg_info( "Setting visibility\n" );
+   SteamAPI_ISteamUGC_SetItemVisibility( hSteamUGC, handle, 
+                                 workshop_form.submission.visibility );
+
+   vg_info( "Submitting updates\n" );
+   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
+   call->userdata = NULL;
+   call->p_handler = on_workshop_update_result;
+   call->id = SteamAPI_ISteamUGC_SubmitItemUpdate( hSteamUGC, handle, "" );
+}
+
+/*
+ * Steam API call result for when we've created a new item on their network, or 
+ * not, if it has failed
+ */
+static void on_workshop_createitem( void *data, void *user )
+{
+   CreateItemResult_t *result = data;
+
+   if( result->m_eResult == k_EResultOK ){
+      vg_info( "Created workshop file with id: %lu\n", 
+                result->m_nPublishedFileId );
+
+      if( result->m_bUserNeedsToAcceptWorkshopLegalAgreement ){
+         vg_warn( "Workshop agreement currently not accepted\n" );
+      }
+      
+      workshop_form_upload_submission( result->m_nPublishedFileId, user );
+   }
+   else{
+      const char *errstr = workshop_EResult_user_string( result->m_eResult );
+      
+      if( errstr ){
+         vg_error( "ISteamUGC_CreateItem() failed(%d): '%s' \n", 
+                     result->m_eResult, errstr );
+      }
+
+      workshop_form.page = k_workshop_form_closing_bad;
+      workshop_form.failure_or_success_string = errstr;
+   }
+}
+
+/*
+ * Starts the workshop upload process through Steam API
+ */
+static void workshop_form_async_submit_begin( void *payload, u32 size )
+{
+
+   /* use existing file */
+   if( workshop_form.submission.file_id ){
+      workshop_form_upload_submission( workshop_form.submission.file_id,
+                                       payload );
+   }
+   else{
+      vg_steam_async_call *call = vg_alloc_async_steam_api_call();
+      call->userdata = payload;
+      call->p_handler = on_workshop_createitem;
+      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+      call->id = SteamAPI_ISteamUGC_CreateItem( hSteamUGC, SKATERIFT_APPID, 
+                                                k_EWorkshopFileTypeCommunity );
+   }
+}
+
+/*
+ * Downloads the framebuffer into scratch memory
+ */
+static void workshop_form_async_download_image( void *payload, u32 size )
+{
+   int w, h;
+   vg_framebuffer_get_res( g_render.fb_workshop_preview, &w, &h );
+   vg_linear_clear( vg_mem.scratch );
+   workshop_form.img_buffer = vg_linear_alloc( vg_mem.scratch, w*h*3 );
+
+   vg_info( "read framebuffer: glReadPixels( %dx%d )\n", w,h );
+
+   glBindFramebuffer( GL_READ_FRAMEBUFFER, g_render.fb_workshop_preview->id );
+   glReadBuffer( GL_COLOR_ATTACHMENT0 );
+   glReadPixels( 0,0, w,h, GL_RGB, GL_UNSIGNED_BYTE, workshop_form.img_buffer );
+
+   workshop_form.img_w = w;
+   workshop_form.img_h = h;
+}
+
+/*
+ * Thread which kicks off the upload process
+ */
+static void _workshop_form_submit_thread( void *data )
+{
+   vg_async_call( workshop_form_async_download_image, NULL, 0 );
+   vg_async_stall();
+
+   char path_buf[4096];
+   vg_str folder;
+   vg_strnull( &folder, path_buf, 4096 );
+
+   vg_strcat( &folder, workshop_filetype_folder() );
+   vg_strcat( &folder, workshop_form.addon_folder );
+
+   if( !vg_strgood(&folder) ){
+      vg_error( "addon folder path too long\n" );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   /* 
+    * Create the metadata file
+    * -----------------------------------------------------------------------*/
+   u8 descriptor_buf[ 512 ];
+   vg_msg descriptor;
+   vg_msg_init( &descriptor, descriptor_buf, sizeof(descriptor_buf) );
+   vg_linear_clear( vg_mem.scratch );
+
+   /* short description */
+   vg_msg_frame( &descriptor, "workshop" );
+      vg_msg_wkvstr( &descriptor, "title", workshop_form.submission.title );
+      //vg_msg_wkvstr( &descriptor, "author", "unknown" );
+      vg_msg_wkvnum( &descriptor, "type", k_vg_msg_u32, 1,
+                     &workshop_form.submission.type );
+      vg_msg_wkvstr( &descriptor, "folder", workshop_form.addon_folder );
+   vg_msg_end_frame( &descriptor );
+   //vg_msg_wkvstr( &descriptor, "location", "USA" );
+
+   char *short_descriptor_str = 
+      vg_linear_alloc( vg_mem.scratch, vg_align8(descriptor.cur.co*2+1));
+   vg_bin_str( descriptor_buf, short_descriptor_str, descriptor.cur.co );
+   short_descriptor_str[descriptor.cur.co*2] = '\0';
+   vg_info( "binstr: %s\n", short_descriptor_str );
+
+   vg_dir dir;
+   if( !vg_dir_open( &dir, folder.buffer ) )
+   {
+      vg_error( "could not open addon folder '%s'\n", folder.buffer );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   while( vg_dir_next_entry(&dir) )
+   {
+      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file )
+      {
+         const char *d_name = vg_dir_entry_name(&dir);
+         if( d_name[0] == '.' ) continue;
+
+         vg_str file = folder;
+         vg_strcat( &file, "/" );
+         vg_strcat( &file, d_name );
+         if( !vg_strgood( &file ) ) continue;
+
+         char *ext = vg_strch( &file, '.' );
+         if( !ext ) continue;
+         if( strcmp(ext,".mdl") ) continue;
+
+         vg_msg_wkvstr( &descriptor, "content", d_name );
+         break;
+      }
+   }
+   vg_dir_close(&dir);
+
+   vg_str descriptor_file = folder;
+   vg_strcat( &descriptor_file, "/addon.inf" );
+   if( !vg_strgood(&descriptor_file) ){
+      vg_error( "Addon info path too long\n" );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+   
+   FILE *fp = fopen( descriptor_file.buffer, "wb" );
+   if( !fp ){
+      vg_error( "Could not open addon info file '%s'\n", 
+                descriptor_file.buffer );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+   fwrite( descriptor_buf, descriptor.cur.co, 1, fp );
+   fclose( fp );
+
+   /* Save the preview 
+    * -----------------------------------------------------------------------*/
+   vg_str preview = folder;
+   vg_strcat( &preview, "/preview.jpg" );
+
+   if( !vg_strgood(&preview) ){
+      vg_error( "preview image path too long\n" );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   int w = workshop_form.img_w,
+       h = workshop_form.img_h;
+
+   vg_info( "writing: %s (%dx%d @90%%)\n", preview.buffer, w,h );
+   stbi_flip_vertically_on_write(1);
+   stbi_write_jpg( preview.buffer, w,h, 3, workshop_form.img_buffer, 90 );
+
+   vg_async_call( workshop_form_async_submit_begin, short_descriptor_str, 0 );
+}
+
+/*
+ * Entry point for the publishing submission operation
+ */
+static void workshop_op_submit( ui_context *ctx )
+{
+   /* TODO: Show these errors to the user */
+   if( workshop_form.submission.submit_title )
+   {
+      if( !workshop_form.submission.title[0] )
+      {
+         ui_start_modal( ctx, "Cannot submit because a title is required\n",
+                         UI_MODAL_WARN );
+         workshop_form.op = k_workshop_op_none;
+         return;
+      }
+   }
+
+   if( workshop_form.submission.submit_description )
+   {
+      if( !workshop_form.submission.description[0] )
+      {
+         ui_start_modal( ctx, 
+                         "Cannot submit because a description is required\n",
+                         UI_MODAL_WARN );
+         workshop_form.op = k_workshop_op_none;
+         return;
+      }
+   }
+
+   if( workshop_form.submission.submit_file_and_image )
+   {
+      if( workshop_form.file_intent == k_workshop_form_file_intent_none )
+      {
+         ui_start_modal( ctx, "Cannot submit because the file is "
+                         "empty or unspecified\n", UI_MODAL_WARN );
+         workshop_form.op = k_workshop_op_none;
+         return;
+      }
+   }
+
+   player_board_unload( &workshop_form.board_model );
+   workshop_form.file_intent = k_workshop_form_file_intent_none;
+   workshop_form.op = k_workshop_op_publishing_update;
+
+   vg_loader_start( _workshop_form_submit_thread, NULL );
+}
+
+/*
+ *  op: k_workshop_form_op_loading_model
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * Reciever for completion of the model file load
+ */
+static void workshop_form_loadmodel_async_complete( void *payload, u32 size )
+{
+   v2_zero( workshop_form.view_angles );
+   v3_zero( workshop_form.view_offset );
+   workshop_form.view_dist = 1.0f;
+   workshop_form.view_changed = 1;
+   workshop_form.file_intent = k_workshop_form_file_intent_new;
+   
+   vg_success( "workshop async load complete\n" );
+   workshop_form.op = k_workshop_op_none;
+}
+
+/*
+ * Reciever for failure to load
+ */
+static void workshop_form_loadmodel_async_error( void *payload, u32 size ){
+}
+
+/*
+ * Thread which loads the model from the disk 
+ */
+static void _workshop_form_load_thread( void *data )
+{
+   char path_buf[4096];
+   vg_str folder;
+   vg_strnull( &folder, path_buf, 4096 );
+
+   vg_strcat( &folder, workshop_filetype_folder() );
+   vg_strcat( &folder, workshop_form.addon_folder );
+
+   if( !vg_strgood(&folder) ){
+      vg_error( "workshop async load failed: path too long\n" );
+      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   vg_dir dir;
+   if( !vg_dir_open( &dir, folder.buffer ) ){
+      vg_error( "workshop async load failed: could not open folder\n" );
+      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   vg_info( "Searching %s for model files\n", folder.buffer );
+
+   int found_mdl = 0;
+   while( vg_dir_next_entry(&dir) ){
+      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file ){
+         const char *d_name = vg_dir_entry_name(&dir);
+         if( d_name[0] == '.' ) continue;
+
+         vg_str file = folder;
+         vg_strcat( &file, "/" );
+         vg_strcat( &file, d_name );
+         if( !vg_strgood( &file ) ) continue;
+
+         char *ext = vg_strch( &file, '.' );
+         if( !ext ) continue;
+         if( strcmp(ext,".mdl") ) continue;
+         found_mdl = 1;
+         break;
+      }
+   }
+   vg_dir_close(&dir);
+
+   if( !found_mdl ){
+      vg_error( "workshop async load failed: no model files found\n" );
+      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   if( workshop_form.submission.type == k_addon_type_board )
+      player_board_load( &workshop_form.board_model, path_buf );
+   else if( workshop_form.submission.type == k_addon_type_player )
+      player_model_load( &workshop_form.player_model, path_buf );
+
+   vg_async_call( workshop_form_loadmodel_async_complete, NULL, 0 );
+}
+
+/*
+ * Entry point for load model operation
+ */
+static void workshop_op_load_model( ui_context *ctx )
+{
+   world_instance *world = world_current_instance();
+   workshop_form.view_world = world;
+
+   if( workshop_form.submission.type == k_addon_type_board )
+   {
+      if( mdl_arrcount( &world->ent_swspreview ) )
+      {
+         workshop_form.ptr_ent = mdl_arritm( &world->ent_swspreview, 0 );
+      }
+      else
+      {
+         ui_start_modal( ctx, "There is no ent_swspreview in the level. \n"
+                         "Cannot publish here\n", UI_MODAL_BAD );
+         workshop_form.op = k_workshop_op_none;
+         return;
+      }
+   }
+   else if( workshop_form.submission.type == k_addon_type_player ){}
+   else 
+   {
+      ui_start_modal( ctx, "Don't know how to prepare for this item type. \n"
+                      "Please contact the developers.\n", UI_MODAL_BAD );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   workshop_form.op = k_workshop_op_loading_model;
+   vg_loader_start( _workshop_form_load_thread, NULL );
+}
+
+/*
+ * op: k_workshop_form_op_downloading_submission 
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * The image has been decoded and is ready to slap into the framebuffer
+ */
+static void workshop_form_async_imageload( void *data, u32 len )
+{
+   if( data )
+   {
+      vg_framebuffer_attachment *a = 
+         &g_render.fb_workshop_preview->attachments[0];
+
+      glBindTexture( GL_TEXTURE_2D, a->id );
+      glTexSubImage2D( GL_TEXTURE_2D, 0,0,0,
+                        WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT, 
+                        a->format, a->type, data );
+      stbi_image_free( data );
+      vg_success( "Loaded workshop preview image\n" );
+   }
+   else
+   {
+      snprintf( workshop_form.error_msg, sizeof(workshop_form.error_msg),
+               "Preview image could not be loaded. Reason: %s\n",
+                stbi_failure_reason() );
+      ui_start_modal( &vg_ui.ctx, workshop_form.error_msg, UI_MODAL_BAD );
+   }
+   workshop_form.op = k_workshop_op_none;
+}
+
+/*
+ * Load the image located at ./workshop_preview.jpg into our framebuffer
+ */
+static void _workshop_load_preview_thread( void *data ){
+   char path_buf[ 4096 ];
+   vg_str path;
+   vg_strnull( &path, path_buf, 4096 );
+   vg_strcat( &path, workshop_filetype_folder() );
+   vg_strcat( &path, workshop_form.addon_folder );
+   vg_strcat( &path, "/preview.jpg" );
+
+   if( vg_strgood( &path ) )
+   {
+      stbi_set_flip_vertically_on_load(1);
+      int x, y, nc;
+      u8 *rgb = stbi_load( path.buffer, &x, &y, &nc, 3 );
+
+      if( rgb )
+      {
+         if( (x == WORKSHOP_PREVIEW_WIDTH) && (y == WORKSHOP_PREVIEW_HEIGHT) )
+         {
+            vg_async_call( workshop_form_async_imageload, rgb, x*y*3 );
+         }
+         else
+         {
+            vg_error( "Resolution does not match framebuffer, so we can't"
+                      " show it\n" );
+            stbi_image_free( rgb );
+            vg_async_call( workshop_form_async_imageload, NULL, 0 );
+         }
+      }
+      else
+      {
+         vg_async_call( workshop_form_async_imageload, NULL, 0 );
+      }
+   }
+   else
+   {
+      vg_async_call( workshop_form_async_imageload, NULL, 0 );
+   }
+}
+
+/*
+ * Entry point to view operation
+ */
+static void workshop_op_download_and_view_submission( int result_index )
+{
+   workshop_form.op = k_workshop_op_downloading_submission;
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+   ISteamRemoteStorage *hSteamRemoteStorage = SteamAPI_SteamRemoteStorage();
+   SteamUGCDetails_t details;
+   if( SteamAPI_ISteamUGC_GetQueryUGCResult( hSteamUGC, 
+                                             workshop_form.ugc_query.handle,
+                                             result_index,
+                                             &details ) )
+   {
+      workshop_reset_submission_data();
+      workshop_form.submission.submit_description = 0;
+      workshop_form.submission.submit_file_and_image = 0;
+      workshop_form.submission.submit_title = 0;
+
+      u8 metadata_buf[512];
+      char metadata_str[1024+1];
+      int have_meta = SteamAPI_ISteamUGC_GetQueryUGCMetadata( hSteamUGC, 
+                                              workshop_form.ugc_query.handle,
+                                              result_index, metadata_str, 
+                                              1024+1 );
+
+      vg_strncpy( details.m_rgchDescription, 
+                  workshop_form.submission.description, 
+                  VG_ARRAY_LEN( workshop_form.submission.description ),
+                  k_strncpy_always_add_null );
+
+      vg_strncpy( details.m_rgchTitle,
+                  workshop_form.submission.title,
+                  VG_ARRAY_LEN( workshop_form.submission.title ),
+                  k_strncpy_always_add_null );
+
+      snprintf( workshop_form.addon_folder, 
+                 VG_ARRAY_LEN( workshop_form.addon_folder ),
+                 "Steam Cloud ("PRINTF_U64")", details.m_nPublishedFileId );
+
+      workshop_form.submission.file_id = details.m_nPublishedFileId;
+      workshop_form.file_intent = k_workshop_form_file_intent_keep_old;
+      workshop_form.page = k_workshop_form_edit;
+      workshop_form.submission.visibility = details.m_eVisibility;
+      workshop_form.submission.type = k_addon_type_none;
+      workshop_form.submission.submission_type_selection = k_addon_type_none;
+
+      if( have_meta )
+      {
+         u32 len = strlen(metadata_str);
+         vg_info( "Metadata: %s\n", metadata_str );
+         vg_str_bin( metadata_str, metadata_buf, len );
+         vg_msg msg;
+         vg_msg_init( &msg, metadata_buf, len/2 );
+         
+         if( vg_msg_seekframe( &msg, "workshop" ))
+         {
+            u32 type;
+            vg_msg_getkvintg( &msg, "type", k_vg_msg_u32, &type, NULL );
+            workshop_form.submission.type = type;
+            workshop_form.submission.submission_type_selection = type;
+
+            const char *kv_folder = vg_msg_getkvstr( &msg, "folder" );
+            if( kv_folder )
+            {
+               vg_strncpy( kv_folder, workshop_form.addon_folder,
+                           sizeof(workshop_form.addon_folder),
+                           k_strncpy_always_add_null );
+            }
+         }
+      }
+      else
+      {
+         vg_error( "No metadata was returned with this item.\n" );
+      }
+
+      vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
+      glClearColor( 0.2f, 0.0f, 0.0f, 1.0f );
+      glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+      glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+      glViewport( 0,0, vg.window_x, vg.window_y );
+
+      vg_loader_start( _workshop_load_preview_thread, NULL );
+   }
+   else
+   {
+      vg_error( "GetQueryUGCResult: Index out of range\n" );
+      workshop_form.op = k_workshop_op_none;
+   }
+}
+
+/*
+ * Regular stuff
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * View a page of results on the sidebar
+ */
+static void workshop_view_page( int req )
+{
+   if( workshop_form.ugc_query.result != k_EResultOK )
+   {
+      vg_error( "Tried to change page without complete data\n" );
+      workshop_form.op = k_workshop_op_none;
+      return;
+   }
+
+   int page = VG_MAX(VG_MIN(req, workshop_form.view_published_page_count-1),0),
+       start = page * WORKSHOP_VIEW_PER_PAGE,
+       end   = VG_MIN( (page+1) * WORKSHOP_VIEW_PER_PAGE, 
+                       workshop_form.ugc_query.returned_item_count ),
+       count = end-start;
+
+   vg_info( "View page %d\n", page );
+
+   workshop_form.view_published_page_id = page;
+   workshop_form.published_files_list_length = count;
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+
+   for( int i=0; i<count; i ++ )
+   {
+      struct published_file *pfile = &workshop_form.published_files_list[i];
+
+      SteamUGCDetails_t details;
+      if( SteamAPI_ISteamUGC_GetQueryUGCResult( hSteamUGC, 
+                                                workshop_form.ugc_query.handle,
+                                                start+i,
+                                                &details ) )
+      {
+         if( details.m_eResult != k_EResultOK )
+         {
+            snprintf( pfile->title, 80, "Error (%d)", details.m_eResult );
+         }
+         else
+         {
+            vg_strncpy( details.m_rgchTitle, pfile->title, 80,
+                        k_strncpy_always_add_null );
+         }
+
+         pfile->result = details.m_eResult;
+         pfile->result_index = start+i;
+      }
+      else
+      {
+         pfile->result = k_EResultValueOutOfRange;
+         pfile->result_index = -1;
+         snprintf( pfile->title, 80, "Error (invalid index)" );
+      }
+   }
+}
+
+/*
+ * Steam API result for when we recieve submitted UGC information about the user
+ */
+static void on_workshop_UGCQueryComplete( void *data, void *userdata )
+{
+   SteamUGCQueryCompleted_t *query = data;
+   workshop_form.ugc_query.result = query->m_eResult;
+
+   if( query->m_eResult == k_EResultOK )
+   {
+      if( query->m_unTotalMatchingResults > 50 )
+      {
+         vg_warn( "You have %d items submitted, "
+                  "we can only view the last 50\n" );
+      }
+
+      workshop_form.ugc_query.all_item_count = query->m_unTotalMatchingResults;
+      workshop_form.ugc_query.returned_item_count =
+                                             query->m_unNumResultsReturned;
+
+      workshop_form.ugc_query.handle = query->m_handle;
+      workshop_form.view_published_page_count = 
+         (query->m_unNumResultsReturned+WORKSHOP_VIEW_PER_PAGE-1)/
+            WORKSHOP_VIEW_PER_PAGE;
+      workshop_form.view_published_page_id = 0;
+      workshop_form.published_files_list_length = 0;
+
+      workshop_view_page( 0 );
+   }
+   else
+   {
+      vg_error( "Steam UGCQuery failed (%d)\n", query->m_eResult );
+      workshop_form.view_published_page_count = 0;
+      workshop_form.view_published_page_id = 0;
+      workshop_form.published_files_list_length = 0;
+
+      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+      SteamAPI_ISteamUGC_ReleaseQueryUGCRequest( hSteamUGC, query->m_handle );
+   }
+}
+
+/*
+ * Console command to open the workshop publisher
+ */
+int workshop_submit_command( int argc, const char *argv[] )
+{
+   if( !steam_ready )
+   {
+      ui_start_modal( &vg_ui.ctx,
+                      "Steam API is not initialized\n", UI_MODAL_BAD );
+      return 0;
+   }
+
+   workshop_form.page = k_workshop_form_open;
+   workshop_form.view_published_page_count = 0;
+   workshop_form.view_published_page_id = 0;
+   workshop_form.published_files_list_length = 0;
+   workshop_form.ugc_query.result = k_EResultNone;
+
+   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
+
+   ISteamUser *hSteamUser = SteamAPI_SteamUser();
+   CSteamID steamid;
+   steamid.m_unAll64Bits = SteamAPI_ISteamUser_GetSteamID( hSteamUser );
+
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+   call->p_handler = on_workshop_UGCQueryComplete;
+   call->userdata = NULL;
+   UGCQueryHandle_t handle = SteamAPI_ISteamUGC_CreateQueryUserUGCRequest
+              ( 
+                hSteamUGC,
+                steamid.m_comp.m_unAccountID,
+                k_EUserUGCList_Published, 
+                k_EUGCMatchingUGCType_Items,
+                k_EUserUGCListSortOrder_CreationOrderDesc,
+                SKATERIFT_APPID, SKATERIFT_APPID,
+                1 );
+   SteamAPI_ISteamUGC_SetReturnMetadata( hSteamUGC, handle, 1 );
+   call->id = SteamAPI_ISteamUGC_SendQueryUGCRequest( hSteamUGC, handle );
+   return 0;
+}
+
+void workshop_init(void)
+{
+   vg_console_reg_cmd( "workshop_submit", workshop_submit_command, NULL );
+}
+
+static void workshop_render_world_preview(void)
+{
+   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
+
+   glClearColor( 0.0f, 0.0f, 0.3f, 1.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+   glEnable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+
+   render_world( world_current_instance(), &g_render.cam, 0, 0, 1, 1 );
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+}
+
+/*
+ * Redraw the playermodel into the workshop framebuffer 
+ */
+static void workshop_render_player_preview(void)
+{
+   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
+   glClearColor( 0.16f, 0.15f, 0.15f, 1.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+   glEnable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+
+   struct skeleton *sk = &localplayer.skeleton;
+
+   player_pose res;
+   res.type = k_player_pose_type_ik;
+
+   struct skeleton_anim *anim = skeleton_get_anim( sk, "idle_cycle+y" );
+   skeleton_sample_anim( sk, anim, vg.time*0.1f, res.keyframes );
+   q_axis_angle( res.root_q, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
+   v3_zero( res.root_co );
+   res.root_co[1] = 200.0f;
+
+   m4x3f transform;
+   q_m3x3( res.root_q, transform );
+   v3_copy( res.root_co, transform[3] );
+
+   /* TODO: Function. */
+   skeleton_apply_pose( sk, res.keyframes, k_anim_apply_defer_ik, 
+                        localplayer.final_mtx );
+   skeleton_apply_ik_pass( sk, localplayer.final_mtx );
+   skeleton_apply_pose( sk, res.keyframes, k_anim_apply_deffered_only,
+                        localplayer.final_mtx );
+   skeleton_apply_inverses( sk, localplayer.final_mtx );
+   skeleton_apply_transform( sk, transform, localplayer.final_mtx );
+
+   vg_camera cam;
+   v3_copy( (v3f){ 0.0f, 201.7f, 1.2f }, cam.pos );
+   
+   cam.nearz = 0.01f;
+   cam.farz = 100.0f;
+   cam.fov = 57.0f;
+   v3_zero( cam.angles );
+   
+   vg_camera_update_transform( &cam );
+   vg_camera_update_view( &cam );
+   vg_camera_update_projection( &cam );
+   vg_camera_finalize( &cam );
+
+   render_playermodel( &cam, world_current_instance(), 0, 
+                       &workshop_form.player_model, sk, localplayer.final_mtx );
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+}
+
+/*
+ * Redraw the model file into the workshop framebuffer
+ */
+static void workshop_render_board_preview(void)
+{
+   if( !workshop_form.ptr_ent )
+   {
+      return;
+   }
+
+   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
+
+   glClearColor( 0.0f, 0.0f, 0.3f, 1.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+   glEnable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+
+   ent_swspreview *swsprev = workshop_form.ptr_ent;
+   world_instance *world = workshop_form.view_world;
+
+   ent_camera *ref = mdl_arritm( &world->ent_camera, 
+                                 mdl_entity_id_id(swsprev->id_camera) );
+   ent_marker *display = mdl_arritm( &world->ent_marker,
+                                 mdl_entity_id_id(swsprev->id_display) ),
+              *display1= mdl_arritm( &world->ent_marker,
+                                 mdl_entity_id_id(swsprev->id_display1) );
+
+   v3f baseco;
+   v3_add( display->transform.co, display1->transform.co, baseco );
+   v3_muls( baseco, 0.5f, baseco );
+
+   vg_camera cam;
+   v3f basevector;
+   v3_sub( display->transform.co, ref->transform.co, basevector );
+   float dist = v3_length( basevector );
+
+   v3f baseangles;
+   v3_angles( basevector, baseangles );
+
+   v2_add( workshop_form.view_angles, baseangles, cam.angles );
+   cam.angles[2] = 0.0f;
+
+   float sX = sinf( cam.angles[0] ),
+         cX = cosf( cam.angles[0] ),
+         sY = sinf( cam.angles[1] ),
+         cY = cosf( cam.angles[1] );
+
+   v3f offset = { -sX * cY, sY, cX * cY };
+
+   v3_muladds( display->transform.co, offset, 
+               dist*workshop_form.view_dist, cam.pos );
+
+   cam.pos[0] += -sX*workshop_form.view_offset[2];
+   cam.pos[2] +=  cX*workshop_form.view_offset[2];
+   cam.pos[0] +=  cX*workshop_form.view_offset[0];
+   cam.pos[2] +=  sX*workshop_form.view_offset[0];
+   cam.pos[1] += workshop_form.view_offset[1];
+
+   cam.nearz = 0.01f;
+   cam.farz  = 100.0f;
+   cam.fov   = ref->fov;
+   
+   vg_camera_update_transform( &cam );
+   vg_camera_update_view( &cam );
+   vg_camera_update_projection( &cam );
+   vg_camera_finalize( &cam );
+
+   m4x3f mmdl, mmdl1;
+   mdl_transform_m4x3( &display->transform, mmdl );
+   mdl_transform_m4x3( &display1->transform, mmdl1 );
+
+   /* force update this for nice shadows. its usually set in the world
+    * pre-render step, but that includes timer stuff 
+    */
+   struct player_board *board = &workshop_form.board_model;
+   struct ub_world_lighting *ubo = &world->ub_lighting;
+   v3f vp0, vp1;
+   v3_copy((v3f){0.0f,0.1f, board->truck_positions[0][2]}, vp0 );
+   v3_copy((v3f){0.0f,0.1f, board->truck_positions[1][2]}, vp1 );
+   m4x3_mulv( mmdl1, vp0, ubo->g_board_0 );
+   m4x3_mulv( mmdl1, vp1, ubo->g_board_1 );
+   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
+   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
+                    sizeof(struct ub_world_lighting), &world->ub_lighting );
+
+   render_world( world, &cam, 0, 0, 0, 0 );
+   struct player_board_pose pose = {0};
+   render_board( &cam, world, board, mmdl,  &pose, k_board_shader_entity );
+   render_board( &cam, world, board, mmdl1, &pose, k_board_shader_entity );
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+   glViewport( 0,0, vg.window_x, vg.window_y );
+}
+
+/*
+ * ImGUI section for workshop form
+ * -----------------------------------------------------------------------------
+ */
+
+static void workshop_changed_model_path( ui_context *ctx, char *buf, u32 len )
+{
+   workshop_form.submission.submit_file_and_image = 1;
+}
+
+static void workshop_changed_title( ui_context *ctx, char *buf, u32 len )
+{
+   workshop_form.submission.submit_title = 1;
+}
+
+static void workshop_changed_description( ui_context *ctx, char *buf, u32 len )
+{
+   workshop_form.submission.submit_description = 1;
+}
+
+static void workshop_form_gui_page_undecided( ui_context *ctx, ui_rect content )
+{
+   ui_rect box;
+   rect_copy( content, box );
+   box[3] = 128;
+   box[2] = (box[2]*2)/3;
+   ui_rect_center( content, box );
+
+   ui_rect row;
+   ui_split( box, k_ui_axis_h, 28, 0, row, box );
+   ui_text( ctx, row, 
+            "Select the type of item\n", 1, k_ui_align_middle_center,0);
+   ui_split( box, k_ui_axis_h, 28, 0, row, box );
+   ui_enum( ctx, row, "Type:", workshop_form_type_opts,
+               4, &workshop_form.submission.submission_type_selection );
+   ui_split( box, k_ui_axis_h, 8, 0, row, box );
+   ui_split( box, k_ui_axis_h, 28, 0, row, box );
+
+   ui_rect button_l, button_r;
+   rect_copy( row, button_l );
+   button_l[2] = 128*2;
+   ui_rect_center( row, button_l );
+   ui_split_ratio( button_l, k_ui_axis_v, 0.5f, 2, button_l, button_r );
+
+   enum addon_type type = workshop_form.submission.submission_type_selection;
+   if( type != k_addon_type_none)
+   {
+      if( ui_button_text( ctx, button_l, "OK", 1 ) == 1 )
+      {
+         workshop_form.submission.type = type;
+
+         if( type == k_addon_type_world ){
+            workshop_form.view_changed = 1;
+            workshop_form.file_intent = k_workshop_form_file_intent_new;
+         }
+      }
+   }
+   else
+   {
+      ui_fill( ctx, button_l, ui_colour(ctx,k_ui_bg) );
+      ui_text( ctx, button_l, "OK", 1, k_ui_align_middle_center, 
+               ui_colour(ctx, k_ui_bg+4) );
+   }
+   
+   if( ui_button_text( ctx, button_r, "Cancel", 1 ) == 1 )
+   {
+      workshop_form.page = k_workshop_form_open;
+      workshop_form.file_intent = k_workshop_form_file_intent_none;
+   }
+}
+
+static void workshop_form_gui_draw_preview( ui_context *ctx, ui_rect img_box )
+{
+   enum addon_type type = workshop_form.submission.type;
+   if( workshop_form.file_intent == k_workshop_form_file_intent_keep_old )
+   {
+      ui_image( ctx, 
+                img_box, &g_render.fb_workshop_preview->attachments[0].id );
+   }
+   else if( workshop_form.file_intent == k_workshop_form_file_intent_new )
+   {
+      ui_image( ctx, 
+                img_box, &g_render.fb_workshop_preview->attachments[0].id );
+
+      if( type == k_addon_type_world )
+      {
+         return;
+      }
+
+      int hover  = ui_inside_rect( img_box, ctx->mouse ),
+          target = ui_inside_rect( img_box, ctx->mouse_click );
+      
+      if( ui_click_down(ctx,UI_MOUSE_MIDDLE) && target )
+      {
+         v3_copy( workshop_form.view_offset,
+                  workshop_form.view_offset_begin );
+      }
+      else if( ui_click_down(ctx,UI_MOUSE_LEFT) && target )
+      {
+         v2_copy( workshop_form.view_angles, 
+                  workshop_form.view_angles_begin );
+      }
+
+      if( ui_clicking(ctx,UI_MOUSE_MIDDLE) && target )
+      {
+         v2f delta = { ctx->mouse[0]-ctx->mouse_click[0],
+                       ctx->mouse[1]-ctx->mouse_click[1] };
+
+         float *begin  = workshop_form.view_offset_begin,
+               *offset = workshop_form.view_offset;
+         offset[0] = vg_clampf( begin[0]-delta[0]*0.002f, -1.0f, 1.0f );
+         offset[2] = vg_clampf( begin[2]-delta[1]*0.002f, -1.0f, 1.0f );
+         workshop_form.view_changed = 1;
+      }
+      else if( ui_clicking(ctx,UI_MOUSE_LEFT) && target )
+      {
+         v2f delta = { ctx->mouse[0]-ctx->mouse_click[0],
+                       ctx->mouse[1]-ctx->mouse_click[1] };
+
+         v2f angles;
+         v2_muladds( workshop_form.view_angles_begin, delta, 0.002f, angles);
+
+         float limit = VG_PIf*0.2f;
+
+         angles[0] = vg_clampf( angles[0], -limit, limit );
+         angles[1] = vg_clampf( angles[1], -limit, limit );
+
+         v2_copy( angles, workshop_form.view_angles );
+         workshop_form.view_changed = 1;
+      }
+
+      if( !ui_clicking(ctx,UI_MOUSE_LEFT) && hover )
+      {
+         float zoom = workshop_form.view_dist;
+         zoom += vg.mouse_wheel[1] * -0.07f;
+         zoom = vg_clampf( zoom, 0.4f, 2.0f );
+         
+         if( zoom != workshop_form.view_dist )
+         {
+            workshop_form.view_changed = 1;
+            workshop_form.view_dist = zoom;
+         }
+      }
+   }
+   else
+   {
+      ui_text( ctx, img_box, "No image", 1, k_ui_align_middle_center,
+               ui_colour( ctx, k_ui_orange ) );
+   }
+}
+
+static void workshop_form_gui_edit_page( ui_context *ctx, ui_rect content )
+{
+   enum addon_type type = workshop_form.submission.type;
+
+   if( type == k_addon_type_none )
+   {
+      workshop_form_gui_page_undecided( ctx, content );
+      return;
+   }
+
+   ui_rect image_plane;
+   ui_split( content, k_ui_axis_h, 300, 0, image_plane, content );
+   ui_fill( ctx, image_plane, ui_colour( ctx, k_ui_bg+0 ) );
+
+   ui_rect img_box;
+   ui_fit_item( image_plane, (ui_px[2]){ 3, 2 }, img_box );
+   workshop_form_gui_draw_preview( ctx, img_box );
+
+   /* file path */
+   ui_rect file_button, file_label;
+
+   char buf[128];
+   snprintf( buf, 128, 
+             "Addon folder: skaterift/%s", workshop_filetype_folder() );
+
+   if( type == k_addon_type_world )
+   {
+      struct ui_textbox_callbacks callbacks = 
+      {
+         .change = workshop_changed_model_path
+      };
+      ui_textbox( ctx, content, buf, workshop_form.addon_folder,
+                  VG_ARRAY_LEN(workshop_form.addon_folder), 1, 0, &callbacks );
+   }
+   else
+   {
+      ui_rect file_entry;
+      ui_standard_widget( ctx, content, file_entry, 1 );
+      ui_split( file_entry, k_ui_axis_v, -128, 0, file_entry, file_button );
+
+      if( workshop_form.file_intent != k_workshop_form_file_intent_none )
+      {
+         ui_text( ctx, file_entry, workshop_form.addon_folder, 1, 
+                  k_ui_align_middle_left, ui_colour( ctx, k_ui_fg+4 ) );
+
+         if( ui_button_text( ctx, file_button, "Remove", 1 ) == 1 )
+         {
+            if( type == k_addon_type_board )
+               player_board_unload( &workshop_form.board_model );
+            else if( type == k_addon_type_player )
+               player_model_unload( &workshop_form.player_model );
+
+            workshop_form.file_intent = k_workshop_form_file_intent_none;
+            workshop_form.addon_folder[0] = '\0';
+         }
+      }
+      else
+      {
+         struct ui_textbox_callbacks callbacks = 
+         {
+            .change = workshop_changed_model_path
+         };
+
+         ui_textbox( ctx, file_entry, buf, workshop_form.addon_folder,
+                     VG_ARRAY_LEN(workshop_form.addon_folder), 1,
+                     0, &callbacks );
+
+         if( ui_button_text( ctx, file_button, "Load", 1 ) == 1 )
+         {
+            workshop_op_load_model( ctx );
+         }
+      }
+   }
+
+   const char *str_title = "Title:", *str_desc = "Description:";
+
+   /* title box */
+   {
+      struct ui_textbox_callbacks callbacks = {
+         .change = workshop_changed_title
+      };
+      ui_textbox( ctx, content, str_title, workshop_form.submission.title, 
+                  VG_ARRAY_LEN(workshop_form.submission.title), 1,
+                  0, &callbacks );
+   }
+
+   /* visibility option */
+   {
+      ui_enum( ctx, content, "Visibility:", workshop_form_visibility_opts,
+               4, &workshop_form.submission.visibility );
+   }
+
+   /* description box */
+   {
+      struct ui_textbox_callbacks callbacks = 
+      {
+         .change = workshop_changed_description
+      };
+      ui_textbox( ctx, content, str_desc, workshop_form.submission.description,
+                  VG_ARRAY_LEN(workshop_form.submission.description), 4,
+                  UI_TEXTBOX_MULTILINE|UI_TEXTBOX_WRAP, &callbacks );
+   }
+
+   /* submissionable */
+   ui_rect final_row;
+   ui_split( content, k_ui_axis_h, content[3]-32-8, 0, content, final_row );
+
+   ui_rect submission_center;
+   rect_copy( final_row, submission_center );
+   submission_center[2] = 256;
+   ui_rect_center( final_row, submission_center );
+
+   ui_rect btn_left, btn_right;
+   ui_split_ratio( submission_center, k_ui_axis_v, 0.5f, 8, 
+                   btn_left, btn_right );
+
+   if( ui_button_text( ctx, btn_left, "Publish", 1 ) == 1 )
+   {
+      workshop_op_submit( ctx );
+   }
+   if( ui_button_text( ctx, btn_right, "Cancel", 1 ) == 1 )
+   {
+      workshop_form.page = k_workshop_form_open;
+      player_board_unload( &workshop_form.board_model );
+      workshop_form.file_intent = k_workshop_form_file_intent_none;
+   }
+
+   /* disclaimer */
+   const char *disclaimer_text = 
+      "By submitting this item, you agree to the workshop terms of service";
+
+   ui_rect disclaimer_row, inner, link;
+   ui_split( content, k_ui_axis_h, content[3]-32, 0, content, disclaimer_row );
+
+   ui_px btn_width = 32;
+
+   rect_copy( disclaimer_row, inner );
+   inner[2] = ui_text_line_width( ctx, disclaimer_text ) + btn_width+8;
+
+   ui_rect label;
+   ui_rect_center( disclaimer_row, inner );
+   ui_split( inner, k_ui_axis_v, inner[2]-btn_width, 0, label, btn_right);
+   ui_rect_pad( btn_right, (ui_px[2]){2,2} );
+
+   if( ui_button_text( ctx, btn_right, "\xb2", 2 ) == 1 )
+   {
+      ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
+      SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage( hSteamFriends,
+            "https://steamcommunity.com/sharedfiles/workshoplegalagreement",
+            k_EActivateGameOverlayToWebPageMode_Default );
+   }
+
+   ui_text( ctx, label, disclaimer_text, 1, k_ui_align_middle_left,
+            ui_colour( ctx, k_ui_fg+4 ) );
+}
+
+static void workshop_form_gui_sidebar( ui_context *ctx, ui_rect sidebar )
+{
+   ui_fill( ctx, sidebar, ui_colour( ctx, k_ui_bg+2 ) );
+
+   ui_rect title;
+   ui_split( sidebar, k_ui_axis_h, 28, 0, title, sidebar );
+
+   if( workshop_form.page == k_workshop_form_edit )
+   {
+      ui_text( ctx, title, "Editing", 1, k_ui_align_middle_center, 0 );
+      ui_split( sidebar, k_ui_axis_h, 28, 0, title, sidebar );
+
+      if( workshop_form.submission.type != k_addon_type_none )
+      {
+         char buf[512];
+         vg_str str;
+         vg_strnull( &str, buf, 512 );
+
+         if( workshop_form.submission.file_id )
+            vg_strcat( &str, "Editing an existing " );
+         else
+            vg_strcat( &str, "Creating a new " );
+
+         if( workshop_form.submission.type == k_addon_type_board )
+            vg_strcat( &str, "skateboard." );
+         else if( workshop_form.submission.type == k_addon_type_world )
+            vg_strcat( &str, "world." );
+         else if( workshop_form.submission.type == k_addon_type_player )
+            vg_strcat( &str, "playermodel." );
+         else
+            vg_strcat( &str, "???." );
+
+         ui_text( ctx, title, buf, 1, k_ui_align_middle_center, 
+                  ui_colour(ctx, k_ui_fg+4) );
+      }
+      return;
+   }
+
+   /* 
+    * sidebar existing entries panel
+    */
+   ui_text( ctx, title, "Your submissions", 1, k_ui_align_middle_center, 0 );
+
+   ui_rect controls, btn_create_new;
+   ui_split( sidebar, k_ui_axis_h,  32, 0, controls, sidebar );
+   ui_split( sidebar, k_ui_axis_h, -32, 0, sidebar, btn_create_new );
+   ui_fill( ctx, controls, ui_colour( ctx, k_ui_bg+1 ) );
+
+   char buf[32];
+   vg_str str;
+   vg_strnull( &str, buf, sizeof(buf) );
+   vg_strcat( &str, "page " );
+   vg_strcati32( &str,  workshop_form.view_published_page_id+1 );
+   vg_strcatch( &str, '/' );
+   vg_strcati32( &str, workshop_form.view_published_page_count );
+
+   ui_rect_pad( controls, (ui_px[2]){0,4} );
+   ui_rect info;
+   ui_split_ratio( controls, k_ui_axis_v, 0.25f, 0, info, controls );
+   ui_text( ctx, info, buf, 1, k_ui_align_middle_center, 0 );
+
+   ui_rect btn_left, btn_right;
+   ui_split_ratio( controls, k_ui_axis_v, 0.5f, 2, btn_left, btn_right );
+            
+   if( ui_button_text( ctx, btn_left, "newer", 1 ) == 1 )
+   {
+      workshop_view_page( workshop_form.view_published_page_id-1 );
+   }
+
+   if( ui_button_text( ctx, btn_right, "older", 1 ) == 1 )
+   {
+      workshop_view_page( workshop_form.view_published_page_count+1 );
+   }
+
+   if( ui_button_text( ctx, btn_create_new, "Create New Item", 1 ) == 1 )
+   {
+      workshop_reset_submission_data();
+      workshop_form.submission.submit_title = 1;
+      workshop_form.submission.submit_description = 1;
+      workshop_form.submission.submit_file_and_image = 1;
+      workshop_form.page = k_workshop_form_edit;
+   }
+
+   for( int i=0; i<workshop_form.published_files_list_length; i++ )
+   {
+      ui_rect item;
+      ui_split( sidebar, k_ui_axis_h, 28, 0, item, sidebar );
+      ui_rect_pad( item, (ui_px[2]){4,4} );
+      
+      struct published_file *pfile = &workshop_form.published_files_list[i];
+      if( ui_button_text( ctx, item, pfile->title, 1 ) == 1 )
+      {
+         if( pfile->result == k_EResultOK )
+         {
+            vg_info( "Select index: %d\n", pfile->result_index );
+            workshop_op_download_and_view_submission( pfile->result_index );
+         }
+         else
+         {
+            vg_warn( "Cannot select that item, result not OK\n" );
+         }
+      }
+   }
+}
+
+void workshop_form_gui( ui_context *ctx )
+{
+   enum workshop_form_page stable_page = workshop_form.page;
+   if( stable_page == k_workshop_form_hidden ) return;
+
+   ui_rect null;
+   ui_rect screen = { 0, 0, vg.window_x, vg.window_y };
+   ui_rect window = { 0, 0, 1000, 700 };
+   ui_rect_center( screen, window );
+   ctx->wants_mouse = 1;
+
+   ui_fill( ctx, window, ui_colour( ctx, k_ui_bg+1 ) );
+   ui_outline( ctx, window, 1, ui_colour( ctx, k_ui_bg+7 ), 0 );
+
+   ui_rect title, panel;
+   ui_split( window, k_ui_axis_h, 28, 0, title, panel );
+   ui_fill( ctx, title, ui_colour( ctx, k_ui_bg+7 ) );
+   ui_text( ctx, title, "Workshop tool", 1, k_ui_align_middle_center, 
+            ui_colourcont( ctx, k_ui_bg+7 ) );
+
+   ui_rect quit_button;
+   ui_split( title, k_ui_axis_v, title[2]-title[3], 2, title, quit_button );
+
+   if( vg_loader_availible() )
+   {
+      if( ui_button_text( ctx, quit_button, "X", 1 ) == 1 )
+      {
+         workshop_quit_form();
+         return;
+      }
+   }
+
+   /* 
+    * temporary operation blinders, we don't yet have a nice way to show the
+    * user that we're doing something uninterruptable, so the code just 
+    * escapes here and we show them a basic string
+    */
+
+   if( workshop_form.op != k_workshop_op_none )
+   {
+      const char *op_string = "The programmer has not bothered to describe "
+                              "the current operation that is running.";
+
+      switch( workshop_form.op )
+      {
+         case k_workshop_op_loading_model:
+            op_string = "Operation in progress: Loading model file.";
+         break;
+         case k_workshop_op_publishing_update:
+            op_string = "Operation in progress: publishing submission update "
+                        "to steam.";
+         break;
+         case k_workshop_op_downloading_submission:
+            op_string = "Operation in progress: downloading existing submission"
+                        " from Steam services.";
+         break;
+         default: break;
+      }
+
+      ui_text( ctx, panel, op_string, 1, k_ui_align_middle_center, 0 );
+      return;
+   }
+
+   /* re draw board preview if need to */
+   if( (stable_page == k_workshop_form_edit) && 
+       workshop_form.view_changed && 
+       workshop_form.file_intent == k_workshop_form_file_intent_new )
+   {
+      enum addon_type type = workshop_form.submission.type;
+      if( type == k_addon_type_board ){
+         workshop_render_board_preview();
+      }
+      else if( type == k_addon_type_world ){
+         vg_success( "Renders world preview\n" );
+         workshop_render_world_preview();
+      }
+      else if( type == k_addon_type_player ){
+         workshop_render_player_preview();
+      }
+
+      workshop_form.view_changed = 0;
+   }
+
+   struct workshop_form *form = &workshop_form;
+
+   ui_rect sidebar, content;
+   ui_split_ratio( panel, k_ui_axis_v, 0.3f, 1, sidebar, content );
+
+   /* content page */
+   ui_rect_pad( content, (ui_px[2]){8,8} );
+
+   if( stable_page == k_workshop_form_edit )
+   {
+      workshop_form_gui_edit_page( ctx, content );
+   }
+   else if( stable_page == k_workshop_form_open )
+   {
+      ui_text( ctx, content, "Nothing selected.", 1, k_ui_align_middle_center,
+               ui_colour( ctx, k_ui_fg+4 ) );
+   }
+   else if( stable_page >= k_workshop_form_cclosing )
+   {
+      ui_rect submission_row;
+      ui_split( content, k_ui_axis_h, content[3]-32-8, 0, content, 
+                   submission_row );
+
+      u32 colour;
+
+      if( stable_page == k_workshop_form_closing_bad )
+         colour = ui_colour( ctx, k_ui_red+k_ui_brighter );
+      else
+         colour = ui_colour( ctx, k_ui_green+k_ui_brighter );
+
+      ui_text( ctx, content, workshop_form.failure_or_success_string, 1, 
+               k_ui_align_middle_center, colour );
+
+      ui_rect submission_center;
+      rect_copy( submission_row, submission_center );
+      submission_center[2] = 128;
+      ui_rect_center( submission_row, submission_center );
+      ui_rect_pad( submission_center, (ui_px[2]){8,8} );
+
+      if( ui_button_text( ctx, submission_center, "OK", 1 ) == 1 )
+      {
+         workshop_form.page = k_workshop_form_open;
+      }
+   }
+
+   workshop_form_gui_sidebar( ctx, sidebar );
+}
+
+/*
+ * Some async api stuff
+ * -----------------------------------------------------------------------------
+ */
+
+void async_workshop_get_filepath( void *data, u32 len )
+{
+   struct async_workshop_filepath_info *info = data;
+
+   u64 _size;
+   u32 _ts;
+
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+   if( !SteamAPI_ISteamUGC_GetItemInstallInfo( hSteamUGC, info->id, &_size,
+                                               info->buf, info->len, &_ts ))
+   {
+      vg_error( "GetItemInstallInfo failed\n" );
+      info->buf[0] = '\0';
+   }
+}
+
+void async_workshop_get_installed_files( void *data, u32 len )
+{
+   struct async_workshop_installed_files_info *info = data;
+
+   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
+   u32 count = SteamAPI_ISteamUGC_GetSubscribedItems( hSteamUGC, info->buffer,
+                                                      *info->len );
+
+   vg_info( "Found %u subscribed items\n", count );
+
+   u32 j=0;
+   for( u32 i=0; i<count; i++ ){
+      u32 state = SteamAPI_ISteamUGC_GetItemState( hSteamUGC, info->buffer[i] );
+      if( state & k_EItemStateInstalled ){
+         info->buffer[j ++] = info->buffer[i];
+      }
+   }
+
+   *info->len = j;
+}
diff --git a/src/workshop.h b/src/workshop.h
new file mode 100644 (file)
index 0000000..08776df
--- /dev/null
@@ -0,0 +1,134 @@
+#pragma once
+#include "addon_types.h"
+#include "vg/vg_steam_remote_storage.h"
+#include "skaterift.h"
+#include "vg/vg_steam_auth.h"
+#include "vg/vg_steam_ugc.h"
+#include "vg/vg_steam_friends.h"
+#include "steam.h"
+#include "ent_skateshop.h"
+
+struct async_workshop_filepath_info{
+   PublishedFileId_t id;
+   char *buf;
+   u32 len;
+};
+
+struct async_workshop_installed_files_info{
+   PublishedFileId_t *buffer;
+   u32 *len; /* inout */
+};
+
+struct async_workshop_metadata_info{
+   struct workshop_file_info *info;
+   const char *path;
+};
+
+
+#define WORKSHOP_VIEW_PER_PAGE 15
+
+struct workshop_form{
+   enum workshop_op {
+      k_workshop_op_none,
+      k_workshop_op_downloading_submission,
+      k_workshop_op_publishing_update,
+      k_workshop_op_loading_model
+   }
+   op;
+
+   struct {
+      char title[80];
+      char description[512];
+      char author[32];
+      i32 submission_type_selection;
+      enum addon_type type;
+
+      PublishedFileId_t file_id; /* 0 if not published yet */
+
+      i32 visibility;
+      int submit_title,       /* set if the respective controls are touched */
+          submit_description,
+          submit_file_and_image;
+   } 
+   submission;
+
+   enum workshop_form_page{
+      k_workshop_form_hidden, 
+      k_workshop_form_open,      /* open but not looking at anything */
+      k_workshop_form_edit,      /* editing a submission */
+      k_workshop_form_cclosing,
+      k_workshop_form_closing_good, /* post upload screen */
+      k_workshop_form_closing_bad,  
+   }
+   page;
+
+   /* model viewer 
+    * -----------------------------
+    */
+
+   char addon_folder[128];
+   struct player_board board_model;
+   struct player_model player_model;
+
+   /* what does the user want to do with the image preview? */
+   enum workshop_form_file_intent{
+      k_workshop_form_file_intent_none,         /* loading probably */
+      k_workshop_form_file_intent_new,          /* board_model is valid */
+      k_workshop_form_file_intent_keep_old      /* just browsing */
+   }
+   file_intent;
+
+   world_instance *view_world;
+   ent_swspreview *ptr_ent;
+   v2f view_angles,
+       view_angles_begin;
+   v3f view_offset,
+       view_offset_begin;
+
+   float view_dist;
+   int view_changed;
+
+   /*
+    * published UGC request
+    * ------------------------------
+    */
+
+   struct {
+      UGCQueryHandle_t handle;
+      EResult result;
+
+      int all_item_count,
+          returned_item_count;
+   }
+   ugc_query;
+
+   /* 
+    * UI information
+    * ------------------------------------------
+    */
+
+   const char *failure_or_success_string;
+   char error_msg[256];
+
+   int img_w, img_h;
+   u8 *img_buffer;
+
+   int view_published_page_count,
+       view_published_page_id;
+
+   struct published_file{
+      EResult result;
+      int result_index;
+      char title[80];
+   }
+   published_files_list[WORKSHOP_VIEW_PER_PAGE];
+   int published_files_list_length;
+}
+extern workshop_form;
+
+void workshop_init(void);
+int workshop_submit_command( int argc, const char *argv[] );
+void async_workshop_get_filepath( void *data, u32 len );
+void async_workshop_get_installed_files( void *data, u32 len );
+void workshop_load_metadata( const char *path,struct workshop_file_info *info );
+void workshop_form_gui( ui_context *ctx );
diff --git a/src/world.c b/src/world.c
new file mode 100644 (file)
index 0000000..e6d6c31
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#include "skaterift.h"
+#include "world.h"
+#include "network.h"
+#include "vg/vg_loader.h"
+#include "vg/vg_mem.h"
+#include "save.h"
+#include "player.h"
+#include "ent_traffic.h"
+
+struct world_static world_static;
+
+world_instance *world_current_instance(void)
+{
+   return &world_static.instances[ world_static.active_instance ];
+}
+
+static int skaterift_switch_instance_cmd( int argc, const char *argv[] );
+
+void world_init(void)
+{
+   vg_loader_step( world_render_init, NULL );
+   vg_loader_step( world_sfd_init, NULL );
+   vg_loader_step( world_water_init, NULL );
+   vg_loader_step( world_gates_init, NULL );
+   vg_loader_step( world_routes_init, NULL );
+
+   /* Allocate dynamic world memory arena */
+   u32 max_size = 76*1024*1024;
+   world_static.heap = vg_create_linear_allocator( vg_mem.rtmemory, max_size,
+                                                   VG_MEMORY_SYSTEM );
+
+   vg_console_reg_cmd( "switch_active_instance", 
+                        skaterift_switch_instance_cmd, NULL );
+}
+
+void world_switch_instance( u32 index )
+{
+   localplayer.subsystem = k_player_subsystem_walk;
+
+   if( index >= VG_ARRAY_LEN(world_static.instances) ){
+      vg_error( "Instance ID out of range (%u)\n", index );
+      return;
+   }
+
+   world_instance *new = &world_static.instances[ index ];
+
+   if( new->status != k_world_status_loaded ){
+      vg_error( "Instance is not loaded (%u)\n", index );
+      return;
+   }
+
+   if( skaterift.demo_mode ){
+      if( world_static.instance_addons[index]->flags & ADDON_REG_PREMIUM ){
+         vg_error( "Can't switch to a premium world in the demo version\n" );
+         return;
+      }
+   }
+
+   world_instance *current = 
+      &world_static.instances[ world_static.active_instance ];
+
+   if( index != world_static.active_instance ){
+      v3_copy( localplayer.rb.co, current->player_co );
+      skaterift_autosave(1);
+   }
+
+   v3_copy( new->player_co, localplayer.rb.co );
+
+   world_static.active_instance = index;
+   player__reset();
+}
+
+static int skaterift_switch_instance_cmd( int argc, const char *argv[] )
+{
+   if( argc )
+      world_switch_instance( atoi(argv[0]) );
+   else 
+      vg_info( "switch_active_instance <id>\n" );
+   return 0;
+}
+
+void skaterift_world_get_save_path( enum world_purpose which, char buf[128] )
+{
+   addon_reg *reg = world_static.instance_addons[ which ];
+
+   if( !reg )
+      vg_fatal_error( "Looking up addon for world without one\n" );
+
+   char id[76];
+   addon_alias_uid( &reg->alias, id );
+   snprintf( buf, 128, "savedata/%s.bkv", id );
+}
+
+void world_update( world_instance *world, v3f pos )
+{
+   world_render.sky_time += world_render.sky_rate * vg.time_delta;
+   world_render.sky_rate = vg_lerp( world_render.sky_rate, 
+                                    world_render.sky_target_rate, 
+                                    vg.time_delta * 5.0 );
+
+   world_routes_update_timer_texts( world );
+   world_routes_update( world );
+   ent_traffic_update( world, pos );
+   world_sfd_update( world, pos );
+   world_volumes_update( world, pos );
+}
diff --git a/src/world.h b/src/world.h
new file mode 100644 (file)
index 0000000..3a067db
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+#include "render.h"
+#include "network_msg.h"
+#include "addon.h"
+#include "scene.h"
+
+/* types
+ */
+
+enum world_geo_type{
+   k_world_geo_type_solid = 0,
+   k_world_geo_type_nonsolid = 1,
+   k_world_geo_type_water = 2
+};
+
+enum world_purpose{
+   k_world_purpose_invalid = -1,
+   k_world_purpose_hub = 0,
+   k_world_purpose_client = 1,
+   k_world_max
+};
+
+struct leaderboard_cache {
+   enum request_status status;
+   f64 cache_time;
+   u8 *data;
+   u32 data_len;
+};
+
+typedef struct world_instance world_instance;
+
+void skaterift_world_get_save_path( enum world_purpose which, char buf[128] );
+
+/* submodule headers */
+#include "world_entity.h"
+#include "world_gate.h"
+#include "world_gen.h"
+#include "world_info.h"
+#include "world_physics.h"
+#include "world_render.h"
+#include "world_sfd.h"
+#include "world_volumes.h"
+#include "world_water.h"
+#include "world_audio.h"
+#include "world_routes.h"
+#include "world_routes_ui.h"
+
+/* console variables */
+
+static f32   k_day_length            = 30.0f; /* minutes */
+static i32   k_debug_light_indices   = 0,
+             k_debug_light_complexity= 0,
+             k_light_preview         = 0,
+             k_light_editor          = 0;
+
+#define WORLD_SURFACE_HAS_TRAFFIC 0x1
+#define WORLD_SURFACE_HAS_PROPS   0x2
+
+struct world_instance {
+   /* Fixed items
+    * -------------------------------------------------------
+    */
+
+   v4f player_co;
+
+   void *heap;
+   enum world_status{
+      k_world_status_unloaded = 0,
+      k_world_status_loading = 1,
+      k_world_status_loaded = 2,
+      k_world_status_unloading = 3  /* dont spawn sounds and stuff */
+   }
+   status;
+
+   struct{
+      boxf depthbounds;
+      int depth_computed;
+
+      float height;
+      int enabled;
+      v4f plane;
+   }
+   water;
+
+   f64 time;
+   f32 tar_min, tar_max;
+
+   /* STD140 */
+   struct ub_world_lighting{
+      v4f g_cube_min,
+          g_cube_inv_range;
+
+      v4f g_water_plane,
+          g_depth_bounds;
+
+      v4f g_daysky_colour;
+      v4f g_nightsky_colour;
+      v4f g_sunset_colour;
+      v4f g_ambient_colour;
+      v4f g_sunset_ambient;
+      v4f g_sun_colour;
+      v4f g_sun_dir;
+      v4f g_board_0;
+      v4f g_board_1;
+
+      float g_water_fog;
+      float g_time;
+      float g_realtime;
+      float g_shadow_length;
+      float g_shadow_spread;
+
+      float g_time_of_day;
+      float g_day_phase;
+      float g_sunset_phase;
+
+      int g_light_preview;
+      int g_shadow_samples;
+
+      int g_debug_indices;
+      int g_debug_complexity;
+   }
+   ub_lighting;
+   GLuint ubo_lighting;
+   int    ubo_bind_point;
+
+   GLuint tbo_light_entities,
+          tex_light_entities,
+          tex_light_cubes;
+
+   float probabilities[3];
+
+   v3i light_cubes;
+   vg_framebuffer *heightmap;
+
+   /*
+    * Dynamically allocated when world_load is called.
+    *
+    *                  the following arrays index somewhere into this linear 
+    *                  allocator
+    * --------------------------------------------------------------------------
+    */
+
+   /*
+    * Main world .mdl 
+    */
+   mdl_context meta;
+
+   GLuint *textures;
+   u32 texture_count;
+
+   struct world_surface{
+      mdl_material info;
+      mdl_submesh sm_geo,
+                  sm_no_collide;
+      u32 flags;
+      u32 alpha_tex;
+   }
+   * surfaces;
+   u32 surface_count;
+
+   ent_worldinfo info;
+   mdl_array_ptr ent_spawn,
+                 ent_gate,
+                 ent_light,
+                 ent_route_node,
+                 ent_path_index,
+                 ent_checkpoint,
+                 ent_route,
+                 ent_water,
+
+                 ent_audio_clip,
+                 ent_audio,
+                 ent_volume,
+                 ent_traffic,
+                 ent_skateshop,
+                 ent_marker,
+                 ent_camera,
+                 ent_swspreview,
+                 ent_ccmd,
+                 ent_objective,
+                 ent_challenge,
+                 ent_relay,
+                 ent_cubemap,
+                 ent_miniworld,
+                 ent_prop,
+                 ent_region,
+                 ent_glider,
+                 ent_npc;
+
+   enum skybox {
+      k_skybox_default,
+      k_skybox_space
+   } skybox;
+
+   ent_gate *rendering_gate;
+
+   /* logic 
+    * ----------------------------------------------------
+    */
+
+   /* world geometry */
+   scene_context scene_geo,
+                 scene_no_collide,
+                 scene_lines;
+
+   /* spacial mappings */
+   bh_tree *geo_bh,
+           *entity_bh;
+   u32 *entity_list;
+
+   /* graphics */
+   glmesh mesh_route_lines;
+   glmesh mesh_geo, 
+          mesh_no_collide;
+   u32 cubemap_cooldown, cubemap_side;
+
+   /* leaderboards */
+   struct leaderboard_cache *leaderboard_cache;
+
+   /* ui */
+   struct route_ui *routes_ui;
+};
+
+struct world_static {
+   /*
+    * Allocated as system memory
+    * --------------------------------------------------------------------------
+    */
+   void *heap;
+
+   u32 current_run_version;
+   double time, rewind_from, rewind_to, last_use;
+
+   u32 active_trigger_volumes[8];
+   u32 active_trigger_volume_count;
+
+   addon_reg *instance_addons[ k_world_max ];
+   world_instance instances[ k_world_max ];
+
+   enum world_purpose active_instance;
+   u32            focused_entity; /* like skateshop, challenge.. */
+   f32            focus_strength;
+   vg_camera      focus_cam;
+
+   /* challenges */
+   ent_objective *challenge_target;
+   f32 challenge_timer;
+
+   enum world_loader_state{
+      k_world_loader_none,
+      k_world_loader_preload,
+      k_world_loader_load
+   }
+   load_state;
+
+   bool clear_async_op_when_done;
+}
+extern world_static;
+
+struct world_load_args 
+{
+   enum world_purpose purpose;
+   addon_reg *reg;
+};
+
+void world_init(void);
+world_instance *world_current_instance(void);
+void world_switch_instance( u32 index );
+void skaterift_world_load_thread( void *_args );
+void world_update( world_instance *world, v3f pos );
diff --git a/src/world_audio.c b/src/world_audio.c
new file mode 100644 (file)
index 0000000..b33d62f
--- /dev/null
@@ -0,0 +1,139 @@
+#include "audio.h"
+#include "world_audio.h"
+
+/* finds any active playing in world and fades them out, we can only do this 
+ * while unloading */
+void world_fadeout_audio( world_instance *world )
+{
+   if( world->status != k_world_status_unloading ){
+      vg_fatal_error( "World status must be set to 'unloading', to fadeout"
+                      " audio.\n" );
+   }
+
+   u8 world_id = (world - world_static.instances) + 1;
+
+   audio_lock();
+   for( u32 i=0; i<AUDIO_CHANNELS; i++ ){
+      audio_channel *ch = &vg_audio.channels[i];
+
+      if( ch->allocated && (ch->world_id == world_id) ){
+         ch = audio_channel_fadeout( ch, 1.0f );
+      }
+   }
+   audio_unlock();
+}
+
+/*
+ * Trace out a random point, near the player to try and determine water areas
+ */
+enum audio_sprite_type world_audio_sample_sprite_random(v3f origin, v3f output)
+{
+   v3f chance = { (vg_randf64(&vg.rand)-0.5f) * 30.0f, 
+                  8,
+                  (vg_randf64(&vg.rand)-0.5f) * 30.0f };
+   
+   v3f pos;
+   v3_add( chance, origin, pos );
+
+   ray_hit contact;
+   contact.dist = vg_minf( 16.0f, pos[1] );
+
+   world_instance *world = world_current_instance();
+   
+   if( ray_world( world, pos, (v3f){0.0f,-1.0f,0.0f}, &contact, 
+            k_material_flag_ghosts ) ){
+      struct world_surface *mat = ray_hit_surface( world, &contact );
+
+      if( mat->info.surface_prop == k_surface_prop_grass){
+         v3_copy( contact.pos, output );
+         return k_audio_sprite_type_grass;
+      }
+      else{
+         return k_audio_sprite_type_none;
+      }
+   }
+
+   output[0] = pos[0];
+   output[1] = 0.0f;
+   output[2] = pos[2];
+
+   float dist = fabsf(output[1] - origin[1]);
+   
+   if( world->water.enabled && dist<=40.0f && !(world->info.flags&0x2) )
+      return k_audio_sprite_type_water;
+   else
+      return k_audio_sprite_type_none;
+}
+
+void world_audio_sample_distances( v3f co, int *index, float *value )
+{
+   float inr3 = 0.57735027,
+         inr2 = 0.70710678118;
+
+   v3f sample_directions[] = {
+      { -1.0f,  0.0f,  0.0f },
+      {  1.0f,  0.0f,  0.0f },
+      {  0.0f,  0.0f,  1.0f },
+      {  0.0f,  0.0f, -1.0f },
+      {  0.0f,  1.0f,  0.0f },
+      {  0.0f, -1.0f,  0.0f },
+      { -inr3,  inr3,  inr3 },
+      {  inr3,  inr3,  inr3 },
+      { -inr3,  inr3, -inr3 },
+      {  inr3,  inr3, -inr3 },
+      { -inr2,  0.0f,  inr2 },
+      {  inr2,  0.0f,  inr2 },
+      { -inr2,  0.0f, -inr2 },
+      {  inr2,  0.0f, -inr2 },
+   };
+
+   static int si = 0;
+   static float distances[16];
+
+   ray_hit ray;
+   ray.dist = 5.0f;
+
+   v3f rc, rd, ro;
+   v3_copy( sample_directions[ si ], rd );
+   v3_add( co, (v3f){0.0f,1.5f,0.0f}, ro );
+   v3_copy( ro, rc );
+
+   float dist = 200.0f;
+
+   for( int i=0; i<10; i++ ){
+      if( ray_world( world_current_instance(), rc, rd, &ray, 
+                     k_material_flag_ghosts ) ){
+         dist = (float)i*5.0f + ray.dist;
+         break;
+      }
+      else{
+         v3_muladds( rc, rd, ray.dist, rc );
+      }
+   }
+
+   distances[si] = dist;
+
+   if( vg_audio.debug_ui && vg_lines.enabled ){
+      for( int i=0; i<14; i++ ){
+         if( distances[i] != 200.0f ){
+            u32 colours[] = { VG__RED, VG__BLUE, VG__GREEN,
+                              VG__CYAN, VG__YELOW, VG__PINK,
+                              VG__WHITE };
+
+            u32 colour = colours[i%7];
+
+            v3f p1;
+            v3_muladds( ro, sample_directions[i], distances[i], p1 );
+            vg_line( ro, p1, colour );
+            vg_line_point( p1, 0.1f, colour );
+         }
+      }
+   }
+
+   *index = si;
+   *value = dist;
+
+   si ++;
+   if( si >= 14 )
+      si = 0;
+}
diff --git a/src/world_audio.h b/src/world_audio.h
new file mode 100644 (file)
index 0000000..07d66d1
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+#include "world.h"
+
+void world_fadeout_audio( world_instance *world );
+void world_audio_sample_distances( v3f co, int *index, float *value );
+enum audio_sprite_type 
+world_audio_sample_sprite_random( v3f origin, v3f output );
diff --git a/src/world_entity.c b/src/world_entity.c
new file mode 100644 (file)
index 0000000..b85e6fa
--- /dev/null
@@ -0,0 +1,876 @@
+#include "vg/vg_steam.h"
+#include "vg/vg_steam_user_stats.h"
+#include "model.h"
+#include "entity.h"
+#include "world.h"
+#include "world_load.h"
+#include "save.h"
+#include "vg/vg_msg.h"
+#include "menu.h"
+#include "ent_challenge.h"
+#include "ent_skateshop.h"
+#include "ent_route.h"
+#include "ent_traffic.h"
+#include "ent_glider.h"
+#include "ent_region.h"
+#include "ent_npc.h"
+#include "ent_camera.h"
+#include "input.h"
+#include "player_walk.h"
+
+bh_system bh_system_entity_list = 
+{
+   .expand_bound = entity_bh_expand_bound,
+   .item_centroid = entity_bh_centroid,
+   .item_closest = entity_bh_closest,
+   .item_swap = entity_bh_swap,
+   .item_debug = entity_bh_debug,
+   .cast_ray = NULL
+};
+
+void world_entity_set_focus( u32 entity_id )
+{
+   if( world_static.focused_entity )
+   {
+      vg_warn( "Entity %u#%u tried to take focus from %u#%u\n",
+                  mdl_entity_id_type( entity_id ),
+                  mdl_entity_id_id( entity_id ),
+                  mdl_entity_id_type( world_static.focused_entity ),
+                  mdl_entity_id_id( world_static.focused_entity ) );
+      return;
+   }
+
+   world_static.focused_entity = entity_id;
+}
+
+void world_entity_focus_modal(void)
+{
+   localplayer.immobile = 1;
+   menu.disable_open = 1;
+   srinput.state = k_input_state_resume;
+
+   v3_zero( localplayer.rb.v );
+   v3_zero( localplayer.rb.w );
+   player_walk.move_speed = 0.0f;
+   skaterift.activity = k_skaterift_ent_focus;
+}
+
+void world_entity_exit_modal(void)
+{
+   if( skaterift.activity != k_skaterift_ent_focus )
+   {
+      vg_warn( "Entity %u#%u tried to exit modal when we weren't in one\n",
+                  mdl_entity_id_type( world_static.focused_entity ),
+                  mdl_entity_id_id( world_static.focused_entity ) );
+      return;
+   }
+
+   localplayer.immobile = 0;
+   menu.disable_open = 0;
+   srinput.state = k_input_state_resume;
+   skaterift.activity = k_skaterift_default;
+}
+
+void world_entity_clear_focus(void)
+{
+   if( skaterift.activity == k_skaterift_ent_focus )
+   {
+      vg_warn( "Entity %u#%u tried to clear focus before exiting modal\n",
+                  mdl_entity_id_type( world_static.focused_entity ),
+                  mdl_entity_id_id( world_static.focused_entity ) );
+      return;
+   }
+
+   world_static.focused_entity = 0;
+}
+
+void world_entity_focus_camera( world_instance *world, u32 uid )
+{
+   if( mdl_entity_id_type( uid ) == k_ent_camera )
+   {
+      u32 index = mdl_entity_id_id( uid );
+      ent_camera *cam = mdl_arritm( &world->ent_camera, index );
+      ent_camera_unpack( cam, &world_static.focus_cam );
+   }
+   else 
+   {
+      vg_camera_copy( &localplayer.cam, &world_static.focus_cam );
+
+      /* TODO ? */
+      world_static.focus_cam.nearz = localplayer.cam.nearz;
+      world_static.focus_cam.farz = localplayer.cam.farz;
+   }
+}
+
+/* logic preupdate */
+void world_entity_focus_preupdate(void)
+{
+   f32 rate = vg_minf( 1.0f, vg.time_frame_delta * 2.0f );
+   int active = 0;
+   if( skaterift.activity == k_skaterift_ent_focus )
+      active = 1;
+
+   vg_slewf( &world_static.focus_strength, active, 
+             vg.time_frame_delta * (1.0f/0.5f) );
+
+   if( world_static.focused_entity == 0 )
+      return;
+
+   u32 type = mdl_entity_id_type( world_static.focused_entity ),
+       index = mdl_entity_id_id( world_static.focused_entity );
+
+   world_instance *world = world_current_instance();
+
+   static void (*table[])( ent_focus_context *ctx ) =
+   {
+      [ k_ent_skateshop ] = ent_skateshop_preupdate,
+      [ k_ent_challenge ] = ent_challenge_preupdate,
+      [ k_ent_route ] = ent_route_preupdate,
+      [ k_ent_npc ] = ent_npc_preupdate,
+   };
+
+   if( (type > VG_ARRAY_LEN(table)) || (table[type] == NULL) )
+   {
+      vg_fatal_error( "No pre-update method set for entity (%u#%u)\n",
+                      type, index );
+   }
+
+   table[type]( &(ent_focus_context){
+                  .world = world,
+                  .index = index,
+                  .active = active } );
+}
+
+/* additional renderings like text etc.. */
+void world_entity_focus_render(void)
+{
+   world_instance *world = world_current_instance();
+   if( skaterift.activity != k_skaterift_ent_focus ){
+      skateshop_render_nonfocused( world, &g_render.cam );
+      return;
+   }
+
+   u32 type = mdl_entity_id_type( world_static.focused_entity ),
+       index = mdl_entity_id_id( world_static.focused_entity );
+
+   if( type == k_ent_skateshop ){
+      ent_skateshop *skateshop = mdl_arritm( &world->ent_skateshop, index );
+      skateshop_render( skateshop );
+   }
+   else if( type == k_ent_challenge ){}
+   else if( type == k_ent_route ){}
+   else if( type == k_ent_miniworld ){}
+   else if( type == k_ent_npc ){}
+   else {
+      vg_fatal_error( "Programming error\n" );
+   }
+}
+
+void world_gen_entities_init( world_instance *world )
+{
+   /* lights */
+   for( u32 j=0; j<mdl_arrcount(&world->ent_light); j ++ ){
+      ent_light *light = mdl_arritm( &world->ent_light, j );
+
+      m4x3f to_world;
+      q_m3x3( light->transform.q, to_world );
+      v3_copy( light->transform.co, to_world[3] );
+      m4x3_invert_affine( to_world, light->inverse_world );
+
+      light->angle_sin_cos[0] = sinf( light->angle * 0.5f );
+      light->angle_sin_cos[1] = cosf( light->angle * 0.5f );
+   }
+
+   vg_async_call( world_link_gates_async, world, 0 );
+   vg_async_stall();
+
+   /* water */
+   for( u32 j=0; j<mdl_arrcount(&world->ent_water); j++ ){
+      ent_water *water = mdl_arritm( &world->ent_water, j );
+      if( world->water.enabled ){
+         vg_warn( "Multiple water surfaces in level!\n" );
+         break;
+      }
+
+      world->water.enabled = 1;
+      water_set_surface( world, water->transform.co[1] );
+   }
+   
+   /* volumes */
+   for( u32 j=0; j<mdl_arrcount(&world->ent_volume); j++ ){
+      ent_volume *volume = mdl_arritm( &world->ent_volume, j );
+      mdl_transform_m4x3( &volume->transform, volume->to_world );
+      m4x3_invert_full( volume->to_world, volume->to_local );
+   }
+
+   /* audio packs */
+   for( u32 j=0; j<mdl_arrcount(&world->ent_audio); j++ ){
+      ent_audio *audio = mdl_arritm( &world->ent_audio, j );
+
+      for( u32 k=0; k<audio->clip_count; k++ ){
+         ent_audio_clip *clip = mdl_arritm( &world->ent_audio_clip,  
+                                             audio->clip_start+k );
+
+         if( clip->_.file.pack_size ){
+            u32 size = clip->_.file.pack_size,
+                offset = clip->_.file.pack_offset;
+
+            /* embedded files are fine to clear the scratch buffer, only
+             * external audio uses it */
+
+            vg_linear_clear( vg_mem.scratch );
+            void *data = vg_linear_alloc( vg_mem.scratch, 
+                                          clip->_.file.pack_size );
+
+            mdl_fread_pack_file( &world->meta, &clip->_.file, data );
+
+            clip->_.clip.path = NULL;
+            clip->_.clip.flags = audio->flags;
+            clip->_.clip.data = data;
+            clip->_.clip.size = size;
+         }
+         else{
+            clip->_.clip.path = mdl_pstr(&world->meta,clip->_.file.pstr_path);
+            clip->_.clip.flags = audio->flags;
+            clip->_.clip.data = NULL;
+            clip->_.clip.size = 0;
+         }
+
+         audio_clip_load( &clip->_.clip, world->heap );
+      }
+   }
+
+   /* create generic entity hierachy for those who need it */
+   u32 indexed_count = 0;
+   struct {
+      u32 type;
+      mdl_array_ptr *array;
+   }
+   indexables[] = {
+      { k_ent_gate, &world->ent_gate },
+      { k_ent_objective, &world->ent_objective },
+      { k_ent_volume, &world->ent_volume },
+      { k_ent_challenge, &world->ent_challenge },
+      { k_ent_glider, &world->ent_glider },
+      { k_ent_npc, &world->ent_npc }
+   };
+
+   for( u32 i=0; i<VG_ARRAY_LEN(indexables); i++ )
+      indexed_count += mdl_arrcount( indexables[i].array );
+   vg_info( "indexing %u entities\n", indexed_count );
+
+   world->entity_list = vg_linear_alloc( world->heap, 
+                                         vg_align8(indexed_count*sizeof(u32)));
+
+   u32 index=0;
+   for( u32 i=0; i<VG_ARRAY_LEN(indexables); i++ ){
+      u32 type  = indexables[i].type,
+          count = mdl_arrcount( indexables[i].array );
+      
+      for( u32 j=0; j<count; j ++ )
+         world->entity_list[index ++] = mdl_entity_id( type, j );
+   }
+
+   world->entity_bh = bh_create( world->heap, &bh_system_entity_list, world,
+                                 indexed_count, 2 );
+
+   world->tar_min = world->entity_bh->nodes[0].bbx[0][1];
+   world->tar_max = world->entity_bh->nodes[0].bbx[1][1] + 20.0f;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i++ ){
+      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
+
+      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tar_min" ) )
+         world->tar_min = marker->transform.co[1];
+
+      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tar_max" ) )
+         world->tar_max = marker->transform.co[1];
+   }
+}
+
+ent_spawn *world_find_closest_spawn( world_instance *world, v3f position )
+{
+   ent_spawn *rp = NULL, *r;
+   float min_dist = INFINITY;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ ){
+      r = mdl_arritm( &world->ent_spawn, i );
+      float d = v3_dist2( r->transform.co, position );
+      
+      if( d < min_dist ){
+         min_dist = d;
+         rp = r;
+      }
+   }
+
+   if( !rp ){
+      if( mdl_arrcount(&world->ent_spawn) ){
+         vg_warn( "Invalid distances to spawns.. defaulting to first one.\n" );
+         return mdl_arritm( &world->ent_spawn, 0 );
+      }
+      else{
+         vg_error( "There are no spawns in the level!\n" );
+      }
+   }
+
+   return rp;
+}
+
+ent_spawn *world_find_spawn_by_name( world_instance *world, const char *name )
+{
+   ent_spawn *rp = NULL, *r;
+   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ ){
+      r = mdl_arritm( &world->ent_spawn, i );
+      if( !strcmp( mdl_pstr(&world->meta, r->pstr_name), name ) ){
+         rp = r;
+         break;
+      }
+   }
+
+   if( !rp )
+      vg_warn( "No spawn named '%s'\n", name );
+
+   return rp;
+}
+
+void world_default_spawn_pos( world_instance *world, v3f pos )
+{
+   ent_spawn *rp = world_find_spawn_by_name( world, "start" );
+   if( !rp ) rp = world_find_closest_spawn( world, (v3f){0,0,0} );
+   if( rp )
+      v3_copy( rp->transform.co, pos );
+   else
+   {
+      vg_error( "There are no valid spawns in the world\n" );
+      v3_zero( pos );
+   }
+}
+
+entity_call_result ent_volume_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+
+   if( !volume->target ) 
+      return k_entity_call_result_OK;
+
+   if( call->function == k_ent_function_trigger )
+   {
+      call->id = volume->target;
+
+      if( volume->flags & k_ent_volume_flag_particles )
+      {
+         float *co = alloca( sizeof(float)*3 );
+         co[0] = vg_randf64(&vg.rand)*2.0f-1.0f;
+         co[1] = vg_randf64(&vg.rand)*2.0f-1.0f;
+         co[2] = vg_randf64(&vg.rand)*2.0f-1.0f;
+         m4x3_mulv( volume->to_world, co, co );
+
+         call->function = k_ent_function_particle_spawn;
+         call->data = co;
+         entity_call( world, call );
+      }
+      else
+      {
+         call->function = volume->trigger.event;
+         entity_call( world, call );
+      }
+
+      return k_entity_call_result_OK;
+   }
+   else if( call->function == k_ent_function_trigger_leave )
+   {
+      call->id = volume->target;
+
+      if( volume->flags & k_ent_volume_flag_particles )
+      {
+         vg_warn( "Invalid condition; calling leave on particle volume.\n" );
+      }
+      else
+      {
+         call->function = volume->trigger.event_leave;
+         entity_call( world, call );
+      }
+
+      return k_entity_call_result_OK;
+   }
+
+   return k_entity_call_result_unhandled;
+}
+
+entity_call_result ent_audio_call( world_instance *world, ent_call *call )
+{
+   if( world->status == k_world_status_unloading )
+   {
+      vg_warn( "cannot modify audio while unloading world\n" );
+      return k_entity_call_result_invalid;
+   }
+
+   u8 world_id = (world - world_static.instances) + 1;
+   u32 index = mdl_entity_id_id( call->id );
+   ent_audio *audio = mdl_arritm( &world->ent_audio, index );
+
+   v3f sound_co;
+
+   if( call->function == k_ent_function_particle_spawn )
+   {
+      v3_copy( call->data, sound_co );
+   }
+   else if( call->function == k_ent_function_trigger )
+   {
+      v3_copy( audio->transform.co, sound_co );
+   }
+   else
+      return k_entity_call_result_unhandled;
+
+   float chance = vg_randf64(&vg.rand)*100.0f,
+         bar = 0.0f;
+
+   for( u32 i=0; i<audio->clip_count; i++ ){
+      ent_audio_clip *clip = mdl_arritm( &world->ent_audio_clip, 
+                                          audio->clip_start+i );
+
+      float mod = world->probabilities[ audio->probability_curve ],
+            p   = clip->probability * mod;
+
+      bar += p;
+      if( chance < bar )
+      {
+         audio_lock();
+
+         if( audio->behaviour == k_channel_behaviour_unlimited )
+         {
+            audio_oneshot_3d( &clip->_.clip, sound_co,
+                              audio->transform.s[0],
+                              audio->volume );
+         }
+         else if( audio->behaviour == k_channel_behaviour_discard_if_full )
+         {
+            audio_channel *ch = 
+               audio_get_group_idle_channel( audio->group, 
+                                             audio->max_channels );
+
+            if( ch )
+            {
+               audio_channel_init( ch, &clip->_.clip, audio->flags );
+               audio_channel_group( ch, audio->group );
+               audio_channel_world( ch, world_id );
+               audio_channel_set_spacial( ch, sound_co, audio->transform.s[0] );
+               audio_channel_edit_volume( ch, audio->volume, 1 );
+               ch = audio_relinquish_channel( ch );
+            }
+         }
+         else if( audio->behaviour == k_channel_behaviour_crossfade_if_full)
+         {
+            audio_channel *ch =
+               audio_get_group_idle_channel( audio->group,
+                                             audio->max_channels );
+
+            /* group is full */
+            if( !ch ){
+               audio_channel *existing = 
+                  audio_get_group_first_active_channel( audio->group );
+
+               if( existing ){
+                  if( existing->source == &clip->_.clip ){
+                     audio_unlock();
+                     return k_entity_call_result_OK;
+                  }
+                 
+                  existing->group = 0;
+                  existing = audio_channel_fadeout(existing, audio->crossfade);
+               }
+
+               ch = audio_get_first_idle_channel();
+            }
+
+            if( ch )
+            {
+               audio_channel_init( ch, &clip->_.clip, audio->flags );
+               audio_channel_group( ch, audio->group );
+               audio_channel_world( ch, world_id );
+               audio_channel_fadein( ch, audio->crossfade );
+               ch = audio_relinquish_channel( ch );
+            }
+         }
+
+         audio_unlock();
+         return k_entity_call_result_OK;
+      }
+   }
+   return k_entity_call_result_OK;
+}
+
+
+entity_call_result ent_ccmd_call( world_instance *world, ent_call *call )
+{
+   if( call->function == k_ent_function_trigger )
+   {
+      u32 index = mdl_entity_id_id( call->id );
+      ent_ccmd *ccmd = mdl_arritm( &world->ent_ccmd, index );
+      vg_execute_console_input( mdl_pstr(&world->meta, ccmd->pstr_command), 0 );
+      return k_entity_call_result_OK;
+   }
+   else
+      return k_entity_call_result_unhandled;
+}
+
+/*
+ * BVH implementation
+ * ----------------------------------------------------------------------------
+ */
+
+void entity_bh_expand_bound( void *user, boxf bound, u32 item_index )
+{
+   world_instance *world = user;
+
+   u32 id    = world->entity_list[ item_index ],
+       type  = mdl_entity_id_type( id ),
+       index = mdl_entity_id_id( id );
+
+   if( type == k_ent_gate ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
+      boxf box = {{ -gate->dimensions[0], -gate->dimensions[1], -0.1f },
+                  {  gate->dimensions[0],  gate->dimensions[1],  0.1f }};
+
+      m4x3_expand_aabb_aabb( gate->to_world, bound, box );
+   }
+   else if( type == k_ent_objective ){
+      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+      
+      /* TODO: This might be more work than necessary. could maybe just get
+       *       away with representing them as points */
+
+      boxf box;
+      box_init_inf( box );
+
+      for( u32 i=0; i<objective->submesh_count; i++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs,
+                                       objective->submesh_start+i );
+         box_concat( box, sm->bbx );
+      }
+
+      m4x3f transform;
+      mdl_transform_m4x3( &objective->transform, transform );
+      m4x3_expand_aabb_aabb( transform, bound, box );
+   }
+   else if( type == k_ent_volume ){
+      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+      m4x3_expand_aabb_aabb( volume->to_world, bound,
+                              (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}} );
+   }
+   else if( type == k_ent_challenge ){
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+
+      boxf box = {{-1.2f*0.5f,-0.72f*0.5f,-0.01f*0.5f},
+                  { 1.2f*0.5f, 0.72f*0.5f, 0.01f*0.5f}};
+      m4x3f transform;
+      mdl_transform_m4x3( &challenge->transform, transform );
+      m4x3_expand_aabb_aabb( transform, bound, box );
+   }
+   else if( type == k_ent_glider ){
+      ent_glider *glider = mdl_arritm( &world->ent_glider, index );
+      m4x3f transform;
+      mdl_transform_m4x3( &glider->transform, transform );
+      m4x3_expand_aabb_aabb( transform, bound,
+                            (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}} );
+   }
+   else if( type == k_ent_npc )
+   {
+      ent_npc *npc = mdl_arritm( &world->ent_npc, index );
+      box_addpt( bound, npc->transform.co );
+   }
+   else{
+      vg_fatal_error( "Programming error\n" );
+   }
+}
+
+float entity_bh_centroid( void *user, u32 item_index, int axis )
+{
+   world_instance *world = user;
+
+   u32 id    = world->entity_list[ item_index ],
+       type  = mdl_entity_id_type( id ),
+       index = mdl_entity_id_id( id );
+
+   if( type == k_ent_gate ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
+      return gate->to_world[3][axis];
+   }
+   else if( type == k_ent_objective ){
+      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+      return objective->transform.co[axis];
+   }
+   else if( type == k_ent_volume ){
+      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+      return volume->transform.co[axis];
+   }
+   else if( type == k_ent_challenge )
+   {
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+      return challenge->transform.co[axis];
+   }
+   else if( type == k_ent_glider )
+   {
+      ent_glider *glider = mdl_arritm( &world->ent_glider, index );
+      return glider->transform.co[axis];
+   }
+   else if( type == k_ent_npc )
+   {
+      ent_npc *npc = mdl_arritm( &world->ent_npc, index );
+      return npc->transform.co[axis];
+   }
+   else 
+   {
+      vg_fatal_error( "Programming error\n" );
+      return INFINITY;
+   }
+}
+
+void entity_bh_swap( void *user, u32 ia, u32 ib )
+{
+   world_instance *world = user;
+
+   u32 a = world->entity_list[ ia ],
+       b = world->entity_list[ ib ];
+
+   world->entity_list[ ia ] = b;
+   world->entity_list[ ib ] = a;
+}
+
+void entity_bh_debug( void *user, u32 item_index ){
+   world_instance *world = user;
+
+   u32 id    = world->entity_list[ item_index ],
+       type  = mdl_entity_id_type( id ),
+       index = mdl_entity_id_id( id );
+
+   if( type == k_ent_gate ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
+      boxf box = {{ -gate->dimensions[0], -gate->dimensions[1], -0.1f },
+                  {  gate->dimensions[0],  gate->dimensions[1],  0.1f }};
+      vg_line_boxf_transformed( gate->to_world, box, 0xf000ff00 );
+   }
+   else if( type == k_ent_objective ){
+      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+      boxf box;
+      box_init_inf( box );
+
+      for( u32 i=0; i<objective->submesh_count; i++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs,
+                                       objective->submesh_start+i );
+         box_concat( box, sm->bbx );
+      }
+
+      m4x3f transform;
+      mdl_transform_m4x3( &objective->transform, transform );
+      vg_line_boxf_transformed( transform, box, 0xf000ff00 );
+   }
+   else if( type == k_ent_volume ){
+      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+      vg_line_boxf_transformed( volume->to_world,
+                                (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}},
+                                0xf000ff00 );
+   }
+   else if( type == k_ent_challenge ){
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+
+      boxf box = {{-1.2f*0.5f,-0.72f*0.5f,-0.01f*0.5f},
+                  { 1.2f*0.5f, 0.72f*0.5f, 0.01f*0.5f}};
+      m4x3f transform;
+      mdl_transform_m4x3( &challenge->transform, transform );
+      vg_line_boxf_transformed( transform, box, 0xf0ff0000 );
+   }
+   else{
+      vg_fatal_error( "Programming error\n" );
+   }
+}
+
+void update_ach_models(void)
+{
+   world_instance *hub = &world_static.instances[k_world_purpose_hub];
+   if( hub->status != k_world_status_loaded ) return;
+
+   for( u32 i=0; i<mdl_arrcount( &hub->ent_prop ); i ++ ){
+      ent_prop *prop = mdl_arritm( &hub->ent_prop, i );
+      if( prop->flags & 0x2 ){
+         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "MARC" ) )
+            if( skaterift.achievements & 0x1 )
+               prop->flags &= ~0x1;
+         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "ALBERT" ) )
+            if( skaterift.achievements & 0x2 )
+               prop->flags &= ~0x1;
+         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "JANET" ) )
+            if( skaterift.achievements & 0x4 )
+               prop->flags &= ~0x1;
+         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "BERNADETTA" ) )
+            if( skaterift.achievements & 0x8 )
+               prop->flags &= ~0x1;
+      }
+   }
+}
+
+void entity_bh_closest( void *user, u32 item_index, v3f point, v3f closest )
+{
+   world_instance *world = user;
+
+   u32 id    = world->entity_list[ item_index ],
+       type  = mdl_entity_id_type( id ),
+       index = mdl_entity_id_id( id );
+
+   if( type == k_ent_gate ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
+      v3_copy( gate->to_world[3], closest );
+   }
+   else if( type == k_ent_objective ){
+      ent_objective *challenge = mdl_arritm( &world->ent_objective, index );
+      v3_copy( challenge->transform.co, closest );
+   }
+   else if( type == k_ent_volume ){
+      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+      v3_copy( volume->to_world[3], closest );
+   }
+   else if( type == k_ent_challenge ){
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+      v3_copy( challenge->transform.co, closest );
+   }
+   else{
+      vg_fatal_error( "Programming error\n" );
+   }
+}
+
+void world_entity_start( world_instance *world, vg_msg *sav )
+{
+   vg_info( "Start instance %p\n", world );
+
+   world->probabilities[ k_probability_curve_constant ] = 1.0f;
+   for( u32 i=0; i<mdl_arrcount(&world->ent_audio); i++ )
+   {
+      ent_audio *audio = mdl_arritm(&world->ent_audio,i);
+      if( audio->flags & AUDIO_FLAG_AUTO_START )
+      {
+         ent_call call;
+         call.data = NULL;
+         call.function = k_ent_function_trigger;
+         call.id = mdl_entity_id( k_ent_audio, i );
+         entity_call( world, &call );
+      }
+   }
+
+   /* read savedata 
+    * ----------------------------------------------------------------------- */
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ ){
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
+      const char *alias = mdl_pstr( &world->meta, challenge->pstr_alias );
+
+      u32 result;
+      vg_msg_getkvintg( sav, alias, k_vg_msg_u32, &result, NULL );
+
+      if( result ){
+         ent_call call;
+         call.data = NULL;
+         call.function = 0;
+         call.id = mdl_entity_id( k_ent_challenge, i );
+         entity_call( world, &call );
+      }
+   }
+
+   vg_msg routes_block = *sav;
+   if( vg_msg_seekframe( &routes_block, "routes" ) ){
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+
+         vg_msg route_info = routes_block;
+         if( vg_msg_seekframe( &route_info, 
+                               mdl_pstr(&world->meta,route->pstr_name) ) ){
+
+            u32 flags;
+            vg_msg_getkvintg( &route_info, "flags", k_vg_msg_u32,    
+                              &flags, NULL );
+            route->flags |= flags;
+
+            vg_msg_getkvintg( &route_info, "best_laptime", k_vg_msg_f64,
+                              &route->best_laptime, NULL );
+
+            f32 sections[ route->checkpoints_count ];
+            vg_msg_cmd cmd;
+            if( vg_msg_getkvcmd( &route_info, "sections", &cmd ) ){
+               vg_msg_cast( cmd.value, cmd.code, sections,
+                            k_vg_msg_f32 |
+                            vg_msg_count_bits(route->checkpoints_count) );
+            }
+            else{
+               for( u32 j=0; j<route->checkpoints_count; j ++ )
+                  sections[j] = 0.0f;
+            }
+
+            for( u32 j=0; j<route->checkpoints_count; j ++ ){
+               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint,
+                     route->checkpoints_start + j );
+
+               cp->best_time = sections[j];
+            }
+
+            /* LEGACY: check if steam achievements can give us a medal */
+            if( steam_ready && steam_stats_ready ){
+               for( u32 j=0; j<VG_ARRAY_LEN(track_infos); j ++ ){
+                  struct track_info *inf = &track_infos[j];
+                  if( !strcmp(inf->name,
+                              mdl_pstr(&world->meta,route->pstr_name))){
+                     
+                     steamapi_bool set = 0;
+                     if( SteamAPI_ISteamUserStats_GetAchievement( 
+                              hSteamUserStats, inf->achievement_id, &set ) )
+                     {
+                        if( set ){
+                           route->flags |= k_ent_route_flag_achieve_silver;
+                        }
+                     }
+                  }
+               }
+            }
+         }
+      }
+   }
+
+   ent_region_re_eval( world );
+}
+
+void world_entity_serialize( world_instance *world, vg_msg *sav )
+{
+   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ ){
+      ent_challenge *challenge = mdl_arritm(&world->ent_challenge,i);
+
+      const char *alias = mdl_pstr(&world->meta,challenge->pstr_alias);
+      vg_msg_wkvnum( sav, alias, k_vg_msg_u32, 1, &challenge->status );
+   }
+   
+   if( mdl_arrcount(&world->ent_route) ){
+      vg_msg_frame( sav, "routes" );
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+         
+         vg_msg_frame( sav, mdl_pstr( &world->meta, route->pstr_name ) );
+         {
+            vg_msg_wkvnum( sav, "flags", k_vg_msg_u32, 1, &route->flags );
+            vg_msg_wkvnum( sav, "best_laptime", 
+                           k_vg_msg_f64, 1, &route->best_laptime );
+
+            f32 sections[ route->checkpoints_count ];
+
+            for( u32 j=0; j<route->checkpoints_count; j ++ ){
+               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint,
+                     route->checkpoints_start + j );
+
+               sections[j] = cp->best_time;
+            }
+
+            vg_msg_wkvnum( sav, "sections", k_vg_msg_f32, 
+                           route->checkpoints_count, sections );
+         }
+         vg_msg_end_frame( sav );
+      }
+      vg_msg_end_frame( sav );
+   }
+}
diff --git a/src/world_entity.h b/src/world_entity.h
new file mode 100644 (file)
index 0000000..c954052
--- /dev/null
@@ -0,0 +1,46 @@
+#pragma once
+#include "world.h"
+#include "entity.h"
+#include "vg/vg_bvh.h"
+#include "vg/vg_msg.h"
+
+typedef struct ent_focus_context ent_focus_context;
+struct ent_focus_context
+{
+   world_instance *world;
+   u32 index; /* Array index of the focused entity */
+   bool active;
+};
+
+void world_gen_entities_init( world_instance *world );
+ent_spawn *world_find_spawn_by_name( world_instance *world, 
+                                        const char *name );
+ent_spawn *world_find_closest_spawn( world_instance *world, 
+                                        v3f position );
+void world_default_spawn_pos( world_instance *world, v3f pos );
+void world_entity_start( world_instance *world, vg_msg *sav );
+void world_entity_serialize( world_instance *world, vg_msg *sav );
+
+entity_call_result ent_volume_call( world_instance *world, ent_call *call );
+entity_call_result ent_audio_call( world_instance *world, ent_call *call );
+entity_call_result ent_ccmd_call( world_instance *world, ent_call *call );
+
+void entity_bh_expand_bound( void *user, boxf bound, u32 item_index );
+float entity_bh_centroid( void *user, u32 item_index, int axis );
+void entity_bh_swap( void *user, u32 ia, u32 ib );
+void entity_bh_debug( void *user, u32 item_index );
+void entity_bh_closest( void *user, u32 item_index, v3f point,
+                           v3f closest );
+
+void world_entity_set_focus( u32 entity_id );
+void world_entity_focus_modal(void);
+
+void world_entity_exit_modal(void);
+void world_entity_clear_focus(void);
+
+void world_entity_focus_preupdate(void);
+void world_entity_focus_render(void);
+void world_entity_focus_camera( world_instance *world, u32 uid );
+void update_ach_models(void);
+
+extern bh_system bh_system_entity_list;
diff --git a/src/world_gate.c b/src/world_gate.c
new file mode 100644 (file)
index 0000000..3e6fdbb
--- /dev/null
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#ifndef WORLD_GATE_C
+#define WORLD_GATE_C
+
+#include "world.h"
+#include "world_gate.h"
+
+#include "skaterift.h"
+#include "common.h"
+#include "model.h"
+#include "entity.h"
+#include "render.h"
+
+#include "world_water.h"
+#include "player_remote.h"
+#include "shaders/model_gate_unlinked.h"
+#include <string.h>
+
+struct world_gates world_gates;
+
+/*
+ * Update the transform matrices for gate
+ */
+void gate_transform_update( ent_gate *gate )
+{
+   if( gate->flags & k_ent_gate_flip )
+   {
+      v4f qflip;
+      q_axis_angle( qflip, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
+      q_mul( gate->q[1], qflip, gate->q[1] );
+      q_normalize( gate->q[1] );
+   }
+
+   m4x3f to_local, recv_to_world;
+
+   q_m3x3( gate->q[0], gate->to_world );
+   v3_copy( gate->co[0], gate->to_world[3] );
+   m4x3_invert_affine( gate->to_world, to_local );
+
+   q_m3x3( gate->q[1], recv_to_world );
+   v3_copy( gate->co[1], recv_to_world[3] );
+
+   m4x3_mul( recv_to_world, to_local, gate->transport );
+}
+
+void world_gates_init(void)
+{
+   vg_info( "world_gates_init\n" );
+   vg_linear_clear( vg_mem.scratch );
+
+   mdl_context mgate;
+   mdl_open( &mgate, "models/rs_gate.mdl", vg_mem.scratch );
+   mdl_load_metadata_block( &mgate, vg_mem.scratch );
+
+   mdl_mesh *surface = mdl_find_mesh( &mgate, "rs_gate" );
+   mdl_submesh *sm = mdl_arritm(&mgate.submeshs,surface->submesh_start);
+   world_gates.sm_surface = *sm;
+
+   const char *names[] = { "rs_gate_marker", "rs_gate_marker.001", 
+                           "rs_gate_marker.002", "rs_gate_marker.003" };
+
+   for( int i=0; i<4; i++ ){
+      mdl_mesh *marker = mdl_find_mesh( &mgate, names[i] );
+      sm = mdl_arritm( &mgate.submeshs, marker->submesh_start );
+      world_gates.sm_marker[i] = *sm;
+   }
+
+   mdl_async_load_glmesh( &mgate, &world_gates.mesh, NULL );
+   mdl_close( &mgate );
+}
+
+void ent_gate_get_mdl_mtx( ent_gate *gate, m4x3f mmdl )
+{
+   m4x3_copy( gate->to_world, mmdl );
+   
+   if( !(gate->flags & k_ent_gate_custom_mesh) ){
+      m3x3_scale( mmdl, (v3f){ gate->dimensions[0], 
+                               gate->dimensions[1], 1.0f } );
+   }
+}
+
+static void render_gate_mesh( world_instance *world, ent_gate *gate )
+{
+   if( gate->flags & k_ent_gate_custom_mesh ){
+      mesh_bind( &world->mesh_no_collide );
+      for( u32 i=0; i<gate->submesh_count; i++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       gate->submesh_start+i );
+         mdl_draw_submesh( sm );
+      }
+   }
+   else {
+      mesh_bind( &world_gates.mesh );
+      mdl_draw_submesh( &world_gates.sm_surface );
+   }
+}
+
+/*
+ * Render the view through a gate
+ */
+int render_gate( world_instance *world, world_instance *world_inside,
+                 ent_gate *gate, vg_camera *cam )
+{
+   v3f viewdir, gatedir;
+   m3x3_mulv( cam->transform, (v3f){0.0f,0.0f,-1.0f}, viewdir );
+   q_mulv( gate->q[0], (v3f){0.0f,0.0f,-1.0f}, gatedir );
+
+   v3f v0;
+   v3_sub( cam->pos, gate->co[0], v0 );
+
+   float dist = v3_dot(v0, gatedir);
+
+   /* Hard cutoff */
+   if( dist > 3.0f )
+      return 0;
+
+   if( v3_dist( cam->pos, gate->co[0] ) > 100.0f )
+      return 0;
+
+   {
+      f32 w = gate->dimensions[0],
+          h = gate->dimensions[1];
+
+      v3f a,b,c,d;
+      m4x3_mulv( gate->to_world, (v3f){-w,-h,0.0f}, a );
+      m4x3_mulv( gate->to_world, (v3f){ w,-h,0.0f}, b );
+      m4x3_mulv( gate->to_world, (v3f){ w, h,0.0f}, c );
+      m4x3_mulv( gate->to_world, (v3f){-w, h,0.0f}, d );
+
+      vg_line( a,b, 0xffffa000 );
+      vg_line( b,c, 0xffffa000 );
+      vg_line( c,d, 0xffffa000 );
+      vg_line( d,a, 0xffffa000 );
+      vg_line( gate->co[0], gate->co[1], 0xff0000ff );
+   }
+
+   /* update gate camera */
+   world_gates.cam.fov = cam->fov;
+   world_gates.cam.nearz = 0.1f;
+   world_gates.cam.farz  = 2000.0f;
+
+   m4x3_mul( gate->transport, cam->transform, world_gates.cam.transform );
+   vg_camera_update_view( &world_gates.cam );
+   vg_camera_update_projection( &world_gates.cam );
+
+   /* Add special clipping plane to projection */
+   v4f surface;
+   q_mulv( gate->q[1], (v3f){0.0f,0.0f,-1.0f}, surface );
+   surface[3] = v3_dot( surface, gate->co[1] );
+   
+   m4x3_mulp( world_gates.cam.transform_inverse, surface, surface );
+   surface[3] = -fabsf(surface[3]);
+
+   if( dist < -0.5f )
+      m4x4_clip_projection( world_gates.cam.mtx.p, surface );
+
+   /* Ready to draw with new camrea */
+   vg_camera_finalize( &world_gates.cam );
+
+   vg_line_point( world_gates.cam.transform[3], 0.3f, 0xff00ff00 );
+
+   shader_model_gate_use();
+   shader_model_gate_uPv( cam->mtx.pv );
+   shader_model_gate_uCam( cam->pos );
+   shader_model_gate_uColour( (v4f){0.0f,1.0f,0.0f,0.0f} );
+   shader_model_gate_uTime( vg.time*0.25f );
+   shader_model_gate_uInvRes( (v2f){
+         1.0f / (float)vg.window_x,
+         1.0f / (float)vg.window_y });
+
+   glEnable( GL_STENCIL_TEST );
+   glStencilOp( GL_KEEP, GL_KEEP, GL_REPLACE );  
+   glStencilFunc( GL_ALWAYS, 1, 0xFF ); 
+   glStencilMask( 0xFF );
+   glEnable( GL_CULL_FACE );
+
+   m4x3f mmdl;
+   ent_gate_get_mdl_mtx( gate, mmdl );
+   shader_model_gate_uMdl( mmdl );
+   render_gate_mesh( world, gate );
+
+   render_world( world_inside, &world_gates.cam, 
+                 1, !localplayer.gate_waiting, 1, 1 );
+
+   return 1;
+}
+
+void render_gate_unlinked( world_instance *world, 
+                           ent_gate *gate, vg_camera *cam )
+{
+   m4x3f mmdl; m4x4f m4mdl;
+   ent_gate_get_mdl_mtx( gate, mmdl );
+   m4x3_expand( mmdl, m4mdl );
+   m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
+
+   shader_model_gate_unlinked_use();
+   shader_model_gate_unlinked_uPv( cam->mtx.pv );
+   shader_model_gate_unlinked_uPvmPrev( m4mdl );
+   shader_model_gate_unlinked_uCam( cam->pos );
+   shader_model_gate_unlinked_uColour( (v4f){0.0f,1.0f,0.0f,0.0f} );
+   shader_model_gate_unlinked_uTime( vg.time*0.25f );
+   shader_model_gate_unlinked_uMdl( mmdl );
+
+   vg_line_point( gate->co[0], 0.1f, 0xffffff00 );
+
+   render_gate_mesh( world, gate );
+}
+
+/*
+ * Intersect the plane of a gate with a line segment, plane coordinate result 
+ * stored in 'where'
+ */
+static int gate_intersect_plane( ent_gate *gate, 
+                                    v3f pos, v3f last, v2f where )
+{
+   v4f surface;
+   q_mulv( gate->q[0], (v3f){0.0f,0.0f,-1.0f}, surface );
+   surface[3] = v3_dot( surface, gate->co[0] );
+
+   v3f v0, c, delta, p0;
+   v3_sub( pos, last, v0 );
+   float l = v3_length( v0 );
+
+   if( l == 0.0f )
+      return 0;
+
+   v3_divs( v0, l, v0 );
+
+   v3_muls( surface, surface[3], c );
+   v3_sub( c, last, delta );
+
+   float d = v3_dot( surface, v0 );
+
+   if( d > 0.00001f ){
+      float t = v3_dot(delta, surface) / d;
+      if( t >= 0.0f && t <= l ){
+         v3f local, rel;
+         v3_muladds( last, v0, t, local );
+         v3_sub( gate->co[0], local, rel );
+
+         where[0] = v3_dot( rel, gate->to_world[0] );
+         where[1] = v3_dot( rel, gate->to_world[1] );
+
+         where[0] /= v3_dot( gate->to_world[0], gate->to_world[0] );
+         where[1] /= v3_dot( gate->to_world[1], gate->to_world[1] );
+
+         return 1;
+      }
+   }
+
+   return 0;
+}
+
+/*
+ * Intersect specific gate
+ */
+int gate_intersect( ent_gate *gate, v3f pos, v3f last )
+{
+   v2f xy;
+
+   if( gate_intersect_plane( gate, pos, last, xy ) ){
+      if( (fabsf(xy[0]) <= gate->dimensions[0]) && 
+          (fabsf(xy[1]) <= gate->dimensions[1]) ){
+         return 1;
+      }
+   }
+
+   return 0;
+}
+
+/* 
+ * Intersect all gates in the world
+ */
+u32 world_intersect_gates( world_instance *world, v3f pos, v3f last )
+{
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+
+      if( !(gate->flags & k_ent_gate_linked) ) continue;
+      if( gate->flags & k_ent_gate_locked ) continue;
+
+      if( gate->flags & k_ent_gate_nonlocal ){
+         if( world_static.instances[gate->target].status 
+               != k_world_status_loaded )
+            continue;
+      }
+
+      if( gate_intersect( gate, pos, last ) )
+         return mdl_entity_id( k_ent_gate, i );
+   }
+
+   return 0;
+}
+
+entity_call_result ent_gate_call( world_instance *world, ent_call *call )
+{
+   u32 index = mdl_entity_id_id( call->id );
+   ent_gate *gate = mdl_arritm( &world->ent_gate, index );
+
+   if( call->function == 0 ) /* unlock() */
+   { 
+      gate->flags &= ~k_ent_gate_locked;
+      return k_entity_call_result_OK;
+   }
+   else 
+   {
+      return k_entity_call_result_unhandled;
+   }
+}
+
+
+/* 
+ * detatches any nonlocal gates 
+ */
+void world_unlink_nonlocal( world_instance *world )
+{
+   for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ )
+   {
+      ent_gate *gate = mdl_arritm( &world->ent_gate, j );
+
+      if( gate->flags & k_ent_gate_nonlocal )
+      {
+         gate->flags &= ~k_ent_gate_linked;
+      }
+   }
+}
+
+/*
+ * This has to be synchronous because main thread looks at gate data for 
+ * rendering, and we modify gates that the main thread has ownership of.
+ */
+void world_link_gates_async( void *payload, u32 size )
+{
+   VG_ASSERT( vg_thread_purpose() == k_thread_purpose_main );
+
+   world_instance *world = payload;
+   u32 world_id = world - world_static.instances;
+
+   for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ )
+   {
+      ent_gate *gate = mdl_arritm( &world->ent_gate, j );
+      gate_transform_update( gate );
+
+      if( skaterift.demo_mode )
+         if( world_static.instance_addons[world_id]->flags & ADDON_REG_PREMIUM )
+            continue;
+
+      if( !(gate->flags & k_ent_gate_nonlocal) ) continue;
+      if( gate->flags & k_ent_gate_linked ) continue;
+
+      const char *key = mdl_pstr( &world->meta, gate->key );
+      vg_info( "key: %s\n", key );
+
+      for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
+         world_instance *other = &world_static.instances[i];
+         if( other == world ) continue;
+         if( other->status != k_world_status_loaded ) continue;
+         vg_info( "Checking world %u for key matches\n", i );
+
+         for( u32 k=0; k<mdl_arrcount( &other->ent_gate ); k++ ){
+            ent_gate *gate2 = mdl_arritm( &other->ent_gate, k );
+
+            if( !(gate2->flags & k_ent_gate_nonlocal) ) continue;
+            if( gate2->flags & k_ent_gate_linked ) continue;
+
+            const char *key2 = mdl_pstr( &other->meta, gate2->key );
+            vg_info( " key2: %s\n", key2 );
+
+            if( strcmp( key, key2 ) ) continue;
+
+            vg_success( "Non-local matching pair '%s' found. (%u:%u)\n",
+                         key, world_id, i );
+
+            gate->flags |= k_ent_gate_linked;
+            gate2->flags |= k_ent_gate_linked;
+            gate->target = i;
+            gate2->target = world_id;
+
+            v3_copy( gate->co[0], gate2->co[1] );
+            v3_copy( gate2->co[0], gate->co[1] );
+            v4_copy( gate->q[0], gate2->q[1] );
+            v4_copy( gate2->q[0], gate->q[1] );
+
+            if( world->meta.info.version < 102 ){
+               /* LEGACY BEHAVIOUR: v101
+                *   this would flip both the client worlds portal's entrance and
+                *   exit. effectively the clients portal would be the opposite 
+                *   to the hub worlds one. new behaviour is to just flip the 
+                *   destinations so the rules are consistent in each world.
+                */
+               v4f qflip;
+               q_axis_angle( qflip, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
+               q_mul( gate->q[0], qflip, gate->q[0] );
+               q_mul( gate->q[1], qflip, gate->q[1] );
+               q_mul( gate2->q[1], qflip, gate2->q[1] );
+            }
+
+            gate_transform_update( gate );
+            gate_transform_update( gate2 );
+
+            goto matched;
+         }
+      } matched:;
+   }
+}
+
+#endif /* WORLD_GATE_C */
diff --git a/src/world_gate.h b/src/world_gate.h
new file mode 100644 (file)
index 0000000..a071e4b
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+
+#include "vg/vg_camera.h"
+#include "world.h"
+#include "shaders/model_gate.h"
+#include "entity.h"
+
+struct world_gates
+{
+   glmesh mesh;
+   mdl_submesh sm_surface, sm_marker[4];
+   vg_camera cam;
+
+   v3f userportal_co;
+}
+extern world_gates;
+
+void world_gates_init(void);
+void gate_transform_update( ent_gate *gate );
+int render_gate( world_instance *world, world_instance *world_inside,
+                    ent_gate *gate, vg_camera *cam );
+
+int gate_intersect( ent_gate *gate, v3f pos, v3f last );
+u32 world_intersect_gates( world_instance *world, v3f pos, v3f last );
+
+entity_call_result ent_gate_call( world_instance *world, ent_call *call );
+void ent_gate_get_mdl_mtx( ent_gate *gate, m4x3f mmdl );
+
+void world_link_gates_async( void *payload, u32 size );
+void world_unlink_nonlocal( world_instance *world );
+void render_gate_unlinked( world_instance *world,
+                           ent_gate *gate, vg_camera *cam );
diff --git a/src/world_gen.c b/src/world_gen.c
new file mode 100644 (file)
index 0000000..e51836d
--- /dev/null
@@ -0,0 +1,776 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ *
+ * World generation/population. Different to regular loading, since it needs to
+ * create geometry, apply procedural stuff and save that image to files etc.
+ */
+#include "world.h"
+#include "world_gen.h"
+#include "world_load.h"
+#include "world_volumes.h"
+#include "world_gate.h"
+#include <string.h>
+
+/*
+ * Add all triangles from the model, which match the material ID
+ * applies affine transform to the model
+ */
+static void world_add_all_if_material( m4x3f transform, scene_context *scene, 
+                                          mdl_context *mdl, u32 id )
+{
+   for( u32 i=0; i<mdl_arrcount(&mdl->meshs); i++ ){
+      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
+
+      for( u32 j=0; j<mesh->submesh_count; j++ ){
+         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, mesh->submesh_start+j );
+         if( sm->material_id == id ){
+            m4x3f transform2;
+            mdl_transform_m4x3( &mesh->transform, transform2 );
+            m4x3_mul( transform, transform2, transform2 );
+
+            scene_add_mdl_submesh( scene, mdl, sm, transform2 );
+         }
+      }
+   }
+}
+
+/*
+ * Adds a small blob shape to the world at a raycast location. This is for the
+ * grass sprites
+ *
+ *   /''''\
+ *  /      \
+ * |        |
+ * |________|
+ */
+static void world_gen_add_blob( vg_rand *rand, world_instance *world,
+                                   scene_context *scene, ray_hit *hit )
+{
+   m4x3f transform;
+   v4f qsurface, qrandom;
+   v3f axis;
+
+   v3_cross( (v3f){0.0f,1.0f,0.0f}, hit->normal, axis );
+
+   float angle = v3_dot(hit->normal,(v3f){0.0f,1.0f,0.0f});
+   q_axis_angle( qsurface, axis, angle );
+   q_axis_angle( qrandom, (v3f){0.0f,1.0f,0.0f}, vg_randf64(rand)*VG_TAUf );
+   q_mul( qsurface, qrandom, qsurface );
+   q_m3x3( qsurface, transform );
+   v3_copy( hit->pos, transform[3] );
+
+   scene_vert verts[] = 
+   {
+      { .co = { -1.00f, 0.0f, 0.0f } },
+      { .co = {  1.00f, 0.0f, 0.0f } },
+      { .co = { -1.00f, 1.2f, 0.0f } },
+      { .co = {  1.00f, 1.2f, 0.0f } },
+      { .co = { -0.25f, 2.0f, 0.0f } },
+      { .co = {  0.25f, 2.0f, 0.0f } }
+   };
+
+   const u32 indices[] = { 0,1,3, 0,3,2, 2,3,5, 2,5,4 };
+
+   if( scene->vertex_count + VG_ARRAY_LEN(verts) > scene->max_vertices )
+      vg_fatal_error( "Scene vertex buffer overflow" );
+
+   if( scene->indice_count + VG_ARRAY_LEN(indices) > scene->max_indices )
+      vg_fatal_error( "Scene index buffer overflow" );
+
+   scene_vert *dst_verts = &scene->arrvertices[ scene->vertex_count ];
+   u32 *dst_indices      = &scene->arrindices [ scene->indice_count ];
+
+   scene_vert *ref       = &world->scene_geo.arrvertices[ hit->tri[0] ];
+
+   for( u32 i=0; i<VG_ARRAY_LEN(verts); i++ ){
+      scene_vert *pvert = &dst_verts[ i ],
+                 *src   = &verts[ i ];
+
+      m4x3_mulv( transform, src->co, pvert->co );
+      scene_vert_pack_norm( pvert, transform[1], 0.0f );
+
+      v2_copy( ref->uv, pvert->uv );
+   }
+
+   for( u32 i=0; i<VG_ARRAY_LEN(indices); i++ )
+      dst_indices[i] = indices[i] + scene->vertex_count;
+
+   scene->vertex_count += VG_ARRAY_LEN(verts);
+   scene->indice_count += VG_ARRAY_LEN(indices);
+}
+
+/* 
+ * Sprinkle foliage models over the map on terrain material 
+ */
+static void world_apply_procedural_foliage( world_instance *world,
+                                               scene_context *scene,
+                                               struct world_surface *mat )
+{
+   if( (vg.quality_profile == k_quality_profile_low) ||
+       (vg.quality_profile == k_quality_profile_min) )
+      return;
+
+   vg_info( "Applying foliage (%u)\n", mat->info.pstr_name );
+
+   v3f volume;
+   v3_sub( world->scene_geo.bbx[1], world->scene_geo.bbx[0], volume );
+   volume[1] = 1.0f;
+
+   int count = 0;
+
+   float area = volume[0]*volume[2];
+   u32 particles = 0.08f * area;
+
+   vg_info( "Map area: %f. Max particles: %u\n", area, particles );
+
+   u64 t0 = SDL_GetPerformanceCounter();
+#if 0
+   for( u32 i=0; i<particles; i++ ){
+      v3f pos;
+      v3_mul( volume, (v3f){ vg_randf64(), 1000.0f, vg_randf64() }, pos );
+      pos[1] = 1000.0f;
+      v3_add( pos, world->scene_geo.bbx[0], pos );
+      
+      ray_hit hit;
+      hit.dist = INFINITY;
+
+      if( ray_world( world, pos, (v3f){0.0f,-1.0f,0.0f}, &hit, 
+                     k_material_flag_ghosts )){
+         struct world_surface *m1 = ray_hit_surface( world, &hit );
+         if((hit.normal[1] > 0.8f) && (m1 == mat) && (hit.pos[1] > 0.0f+10.0f)){
+            world_gen_add_blob( world, scene, &hit );
+            count ++;
+         }
+      }
+   }
+#else
+
+   vg_rand rand;
+   vg_rand_seed( &rand, 3030 );
+   
+   const f32 tile_scale = 16.0f;
+   v2i tiles = { volume[0]/tile_scale, volume[2]/tile_scale };
+
+   u32 per_tile = particles/(tiles[0]*tiles[1]);
+
+   for( i32 x=0; x<tiles[0]; x ++ ){
+      for( i32 z=0; z<tiles[1]; z ++ ){
+         for( u32 i=0; i<per_tile; i ++ ){
+            v3f co = { (f32)x+vg_randf64(&rand), 0, (f32)z+vg_randf64(&rand) };
+            v3_muls( co, tile_scale, co );
+            co[1] = 1000.0f;
+            v3_add( co, world->scene_geo.bbx[0], co );
+
+            ray_hit hit;
+            hit.dist = INFINITY;
+
+            if( ray_world( world, co, (v3f){0.0f,-1.0f,0.0f}, &hit, 
+                           k_material_flag_ghosts )){
+               struct world_surface *m1 = ray_hit_surface( world, &hit );
+               if((hit.normal[1] > 0.8f) && (m1 == mat) &&
+                  (hit.pos[1] > 0.0f+10.0f)){
+                  world_gen_add_blob( &rand, world, scene, &hit );
+                  count ++;
+               }
+            }
+
+         }
+      }
+   }
+
+#endif
+
+
+
+   u64 t1 = SDL_GetPerformanceCounter(),
+       utime_blobs = t1-t0,
+       ufreq = SDL_GetPerformanceFrequency();
+   f64 ftime_blobs = ((f64)utime_blobs / (f64)ufreq)*1000.0;
+
+   vg_info( "%d foliage models added. %f%% (%fms)\n", count, 
+            100.0*((f64)count/(f64)particles), ftime_blobs);
+}
+
+static 
+void world_unpack_submesh_dynamic( world_instance *world,
+                                   scene_context *scene, mdl_submesh *sm ){
+   if( sm->flags & k_submesh_flag_consumed ) return;
+
+   m4x3f identity;
+   m4x3_identity( identity );
+   scene_add_mdl_submesh( scene, &world->meta, sm, identity );
+
+   scene_copy_slice( scene, sm );
+   sm->flags |= k_submesh_flag_consumed;
+}
+
+/*
+ * Create the main meshes for the world
+ */
+void world_gen_generate_meshes( world_instance *world )
+{
+   /* 
+    * Compile meshes into the world scenes
+    */
+   scene_init( &world->scene_geo, 320000, 1200000 );
+   u32 buf_size = scene_mem_required( &world->scene_geo );
+   u8 *buffer = vg_linear_alloc( world->heap, buf_size );
+   scene_supply_buffer( &world->scene_geo, buffer );
+
+   m4x3f midentity;
+   m4x3_identity( midentity );
+
+   /*
+    * Generate scene: collidable geometry
+    * ----------------------------------------------------------------
+    */
+
+   vg_info( "Generating collidable geometry\n" );
+   
+   for( u32 i=0; i<world->surface_count; i++ ){
+      struct world_surface *surf = &world->surfaces[ i ];
+
+      if( surf->info.flags & k_material_flag_collision )
+         world_add_all_if_material( midentity, &world->scene_geo, 
+                                    &world->meta, i );
+
+      scene_copy_slice( &world->scene_geo, &surf->sm_geo );
+      scene_set_vertex_flags( &world->scene_geo, 
+                              surf->sm_geo.vertex_start,
+                              surf->sm_geo.vertex_count, 
+                              (u16)(surf->info.flags & 0xffff) );
+   }
+
+   /* compress that bad boy */
+   u32 new_vert_max = world->scene_geo.vertex_count,
+       new_vert_size = vg_align8(new_vert_max*sizeof(scene_vert)),
+       new_indice_len = world->scene_geo.indice_count*sizeof(u32);
+
+   u32 *src_indices = world->scene_geo.arrindices,
+       *dst_indices = (u32 *)(buffer + new_vert_size);
+
+   memmove( dst_indices, src_indices, new_indice_len );
+
+   world->scene_geo.max_indices = world->scene_geo.indice_count;
+   world->scene_geo.max_vertices = world->scene_geo.vertex_count;
+   buf_size = scene_mem_required( &world->scene_geo );
+
+   buffer = vg_linear_resize( world->heap, buffer, buf_size );
+
+   world->scene_geo.arrvertices = (scene_vert *)(buffer);
+   world->scene_geo.arrindices = (u32 *)(buffer + new_vert_size);
+
+   scene_upload_async( &world->scene_geo, &world->mesh_geo );
+
+   /* need send off the memory to the gpu before we can create the bvh. */
+   vg_async_stall();
+   vg_info( "creating bvh\n" );
+   world->geo_bh = scene_bh_create( world->heap, &world->scene_geo );
+
+   /*
+    * Generate scene: non-collidable geometry
+    * ----------------------------------------------------------------
+    */
+   vg_info( "Generating non-collidable geometry\n" );
+
+   vg_async_item *call = scene_alloc_async( &world->scene_no_collide,
+                                            &world->mesh_no_collide,
+                                            250000, 500000 );
+
+   for( u32 i=0; i<world->surface_count; i++ ){
+      struct world_surface *surf = &world->surfaces[ i ];
+
+      if( !(surf->info.flags & k_material_flag_collision) ){
+         world_add_all_if_material( midentity, 
+                                    &world->scene_no_collide, &world->meta, i );
+      }
+
+      if( surf->info.flags & k_material_flag_grow_grass ){
+         world_apply_procedural_foliage( world, &world->scene_no_collide, 
+                                         surf );
+      }
+
+      scene_copy_slice( &world->scene_no_collide, &surf->sm_no_collide );
+   }
+
+   /* unpack traffic models.. TODO: should we just put all these submeshes in a
+    * dynamic models list? and then the actual entitities point to the 
+    * models. we only have 2 types at the moment which need dynamic models but
+    * would make sense to do this when/if we have more.
+    */
+   for( u32 i=0; i<mdl_arrcount( &world->ent_traffic ); i++ ){
+      ent_traffic *vehc = mdl_arritm( &world->ent_traffic, i );
+
+      for( u32 j=0; j<vehc->submesh_count; j++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       vehc->submesh_start+j );
+         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
+         world->surfaces[ sm->material_id ].flags |= WORLD_SURFACE_HAS_TRAFFIC;
+      }
+   }
+
+   /* unpack challenge models */
+   for( u32 i=0; i<mdl_arrcount( &world->ent_objective ); i++ ){
+      ent_objective *objective = mdl_arritm( &world->ent_objective, i );
+
+      for( u32 j=0; j<objective->submesh_count; j ++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       objective->submesh_start+j );
+         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
+      }
+   }
+
+   /* unpack region models */
+   for( u32 i=0; i<mdl_arrcount( &world->ent_region ); i++ ){
+      ent_region *region = mdl_arritm( &world->ent_region, i );
+
+      for( u32 j=0; j<region->submesh_count; j ++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       region->submesh_start+j );
+         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
+      }
+   }
+
+   /* unpack gate models */
+   for( u32 i=0; i<mdl_arrcount( &world->ent_gate ); i++ ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+
+      if( !(gate->flags & k_ent_gate_custom_mesh) ) continue;
+
+      for( u32 j=0; j<gate->submesh_count; j ++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       gate->submesh_start+j );
+         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
+      }
+   }
+
+   /* unpack prop models */
+   for( u32 i=0; i<mdl_arrcount( &world->ent_prop ); i++ ){
+      ent_prop *prop = mdl_arritm( &world->ent_prop, i );
+
+      for( u32 j=0; j<prop->submesh_count; j ++ ){
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       prop->submesh_start+j );
+         world->surfaces[ sm->material_id ].flags |= WORLD_SURFACE_HAS_PROPS;
+         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
+      }
+   }
+
+   vg_async_dispatch( call, async_scene_upload );
+}
+
+/* signed distance function for cone */
+static f32 fsd_cone_infinite( v3f p, v2f c ){
+   v2f q = { v2_length( (v2f){ p[0], p[2] } ), -p[1] };
+   float s = vg_maxf( 0.0f, v2_dot( q, c ) );
+
+   v2f v0;
+   v2_muls( c, s, v0 );
+   v2_sub( q, v0, v0 );
+
+   float d = v2_length( v0 );
+   return d * ((q[0]*c[1]-q[1]*c[0]<0.0f)?-1.0f:1.0f);
+}
+
+struct light_indices_upload_info{
+   world_instance *world;
+   v3i count;
+
+   void *data;
+};
+
+/*
+ * Async reciever to buffer light index data 
+ */
+static void async_upload_light_indices( void *payload, u32 size ){
+   struct light_indices_upload_info *info = payload;
+
+   glGenTextures( 1, &info->world->tex_light_cubes );
+   glBindTexture( GL_TEXTURE_3D, info->world->tex_light_cubes );
+   glTexImage3D( GL_TEXTURE_3D, 0, GL_RG32UI,
+                 info->count[0], info->count[1], info->count[2],
+                 0, GL_RG_INTEGER, GL_UNSIGNED_INT, info->data );
+   glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
+   glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
+}
+
+/*
+ * Computes light indices for world
+ */
+void world_gen_compute_light_indices( world_instance *world )
+{
+   /* light cubes */
+   v3f cubes_min, cubes_max;
+   v3_muls( world->scene_geo.bbx[0], 1.0f/k_world_light_cube_size, cubes_min );
+   v3_muls( world->scene_geo.bbx[1], 1.0f/k_world_light_cube_size, cubes_max );
+
+   v3_sub( cubes_min, (v3f){ 0.5f, 0.5f, 0.5f }, cubes_min );
+   v3_add( cubes_max, (v3f){ 0.5f, 0.5f, 0.5f }, cubes_max );
+
+   v3_floor( cubes_min, cubes_min );
+   v3_floor( cubes_max, cubes_max );
+
+   v3i icubes_min, icubes_max;
+
+   for( int i=0; i<3; i++ ){
+      icubes_min[i] = cubes_min[i];
+      icubes_max[i] = cubes_max[i];
+   }
+
+   v3f cube_size;
+
+   v3i icubes_count;
+   v3i_sub( icubes_max, icubes_min, icubes_count );
+   
+   for( int i=0; i<3; i++ ){
+      int clamped_count = VG_MIN( 128, icubes_count[i]+1 );
+      float clamped_max = icubes_min[i] + clamped_count,
+                    max = icubes_min[i] + icubes_count[i]+1;
+
+      icubes_count[i] = clamped_count;
+      cube_size[i] = (max / clamped_max) * k_world_light_cube_size;
+      cubes_max[i] = clamped_max;
+   }
+
+   v3_mul( cubes_min, cube_size, cubes_min );
+   v3_mul( cubes_max, cube_size, cubes_max );
+
+   for( int i=0; i<3; i++ ){
+      float range = cubes_max[i]-cubes_min[i];
+      world->ub_lighting.g_cube_inv_range[i] = 1.0f / range;
+      world->ub_lighting.g_cube_inv_range[i] *= (float)icubes_count[i];
+
+      vg_info( "cubes[%d]: %d\n", i, icubes_count[i] );
+   }
+
+   int total_cubes = icubes_count[0]*icubes_count[1]*icubes_count[2];
+
+   u32 data_size = vg_align8(total_cubes*sizeof(u32)*2),
+       hdr_size  = vg_align8(sizeof(struct light_indices_upload_info));
+
+   vg_async_item *call = vg_async_alloc( data_size + hdr_size );
+   struct light_indices_upload_info *info = call->payload;
+   info->data = ((u8*)call->payload) + hdr_size;
+   info->world = world;
+   u32 *cubes_index = info->data;
+
+   for( int i=0; i<3; i++ )
+      info->count[i] = icubes_count[i];
+                                       
+   vg_info( "Computing light cubes (%d) [%f %f %f] -> [%f %f %f]\n", 
+             total_cubes, cubes_min[0], -cubes_min[2], cubes_min[1],
+                          cubes_max[0], -cubes_max[2], cubes_max[1] );
+   v3_copy( cubes_min, world->ub_lighting.g_cube_min );
+
+   float bound_radius = v3_length( cube_size );
+
+   for( int iz = 0; iz<icubes_count[2]; iz ++ ){
+      for( int iy = 0; iy<icubes_count[1]; iy++ ){
+         for( int ix = 0; ix<icubes_count[0]; ix++ ){
+            boxf bbx;
+            v3_div( (v3f){ ix, iy, iz }, world->ub_lighting.g_cube_inv_range, 
+                  bbx[0] );
+            v3_div( (v3f){ ix+1, iy+1, iz+1 }, 
+                  world->ub_lighting.g_cube_inv_range, 
+                  bbx[1] );
+
+            v3_add( bbx[0], world->ub_lighting.g_cube_min, bbx[0] );
+            v3_add( bbx[1], world->ub_lighting.g_cube_min, bbx[1] );
+
+            v3f center;
+            v3_add( bbx[0], bbx[1], center );
+            v3_muls( center, 0.5f, center );
+            
+            u32 indices[6] = { 0, 0, 0, 0, 0, 0 };
+            u32 count = 0;
+
+            float influences[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
+            const int N = VG_ARRAY_LEN( influences );
+
+            for( u32 j=0; j<mdl_arrcount(&world->ent_light); j ++ ){
+               ent_light *light = mdl_arritm( &world->ent_light, j );
+               v3f closest;
+               closest_point_aabb( light->transform.co, bbx, closest );
+
+               f32 dist2 = v3_dist2( closest, light->transform.co );
+
+               if( dist2 > light->range*light->range )
+                  continue;
+
+               f32 dist = sqrtf(dist2),
+                   influence = 1.0f/(dist+1.0f);
+
+               if( light->type == k_light_type_spot){
+                  v3f local;
+                  m4x3_mulv( light->inverse_world, center, local );
+
+                  float r = fsd_cone_infinite( local, light->angle_sin_cos );
+
+                  if( r > bound_radius )
+                     continue;
+               }
+
+               int best_pos = N;
+               for( int k=best_pos-1; k>=0; k -- )
+                  if( influence > influences[k] )
+                     best_pos = k;
+
+               if( best_pos < N ){
+                  for( int k=N-1; k>best_pos; k -- ){
+                     influences[k] = influences[k-1];
+                     indices[k] = indices[k-1];
+                  }
+
+                  influences[best_pos] = influence;
+                  indices[best_pos] = j;
+               }
+            }
+
+            for( int j=0; j<N; j++ )
+               if( influences[j] > 0.0f )
+                  count ++;
+
+            int base_index = iz * (icubes_count[0]*icubes_count[1]) +
+                             iy * (icubes_count[0]) +
+                             ix;
+
+            int lower_count = VG_MIN( 3, count );
+            u32 packed_index_lower = lower_count;
+            packed_index_lower |= indices[0]<<2;
+            packed_index_lower |= indices[1]<<12;
+            packed_index_lower |= indices[2]<<22;
+
+            int upper_count = VG_MAX( 0, count - lower_count );
+            u32 packed_index_upper = upper_count;
+            packed_index_upper |= indices[3]<<2;
+            packed_index_upper |= indices[4]<<12;
+            packed_index_upper |= indices[5]<<22;
+
+            cubes_index[ base_index*2 + 0 ] = packed_index_lower;
+            cubes_index[ base_index*2 + 1 ] = packed_index_upper;
+         }
+      }
+   }
+
+   vg_async_dispatch( call, async_upload_light_indices );
+}
+
+/*
+ * Rendering pass needed to complete the world
+ */
+void async_world_postprocess( void *payload, u32 _size )
+{
+   /* create scene lighting buffer */
+   world_instance *world = payload;
+
+   u32 size = VG_MAX(mdl_arrcount(&world->ent_light),1) * sizeof(float)*12;
+   vg_info( "Upload %ubytes (lighting)\n", size );
+
+   glGenBuffers( 1, &world->tbo_light_entities );
+   glBindBuffer( GL_TEXTURE_BUFFER, world->tbo_light_entities );
+   glBufferData( GL_TEXTURE_BUFFER, size, NULL, GL_DYNAMIC_DRAW );
+   
+   /* buffer layout
+    *  
+    *  colour               position                direction (spots)
+    * | .   .   .   .     | .   .   .   .         | .   .   .   .  |
+    * | Re  Ge  Be  Night | Xco Yco Zco Range     | Dx  Dy  Dz  Da |
+    *
+    */
+
+   v4f *light_dst = glMapBuffer( GL_TEXTURE_BUFFER, GL_WRITE_ONLY );
+   for( u32 i=0; i<mdl_arrcount(&world->ent_light); i++ ){
+      ent_light *light = mdl_arritm( &world->ent_light, i );
+
+      /* colour  + night */
+      v3_muls( light->colour, light->colour[3] * 2.0f, light_dst[i*3+0] );
+      light_dst[i*3+0][3] = 2.0f;
+
+      if( !light->daytime ){
+         u32 hash = (i * 29986577u) & 0xffu;
+         float switch_on = hash;
+               switch_on *= (1.0f/255.0f);
+
+         light_dst[i*3+0][3] = 0.44f + switch_on * 0.015f;
+      }
+      
+      /* position + 1/range^2 */
+      v3_copy( light->transform.co, light_dst[i*3+1] );
+      light_dst[i*3+1][3] = 1.0f/(light->range*light->range);
+
+      /* direction + angle */
+      q_mulv( light->transform.q, (v3f){0.0f,-1.0f,0.0f}, light_dst[i*3+2]);
+      light_dst[i*3+2][3] = cosf( light->angle );
+   }
+
+   glUnmapBuffer( GL_TEXTURE_BUFFER );
+
+   glGenTextures( 1, &world->tex_light_entities );
+   glBindTexture( GL_TEXTURE_BUFFER, world->tex_light_entities );
+   glTexBuffer( GL_TEXTURE_BUFFER, GL_RGBA32F, world->tbo_light_entities );
+
+   /* Upload lighting uniform buffer */
+   if( world->water.enabled )
+      v4_copy( world->water.plane, world->ub_lighting.g_water_plane );
+
+   v4f info_vec;
+   v3f *bounds = world->scene_geo.bbx;
+
+   info_vec[0] = bounds[0][0];
+   info_vec[1] = bounds[0][2];
+   info_vec[2] = 1.0f/ (bounds[1][0]-bounds[0][0]);
+   info_vec[3] = 1.0f/ (bounds[1][2]-bounds[0][2]);
+   v4_copy( info_vec, world->ub_lighting.g_depth_bounds );
+
+   /* 
+    * Rendering the depth map
+    */
+   vg_camera ortho;
+
+   v3f extent;
+   v3_sub( world->scene_geo.bbx[1], world->scene_geo.bbx[0], extent );
+
+   float fl = world->scene_geo.bbx[0][0],
+         fr = world->scene_geo.bbx[1][0],
+         fb = world->scene_geo.bbx[0][2],
+         ft = world->scene_geo.bbx[1][2],
+         rl = 1.0f / (fr-fl),
+         tb = 1.0f / (ft-fb);
+
+   m4x4_zero( ortho.mtx.p );
+   ortho.mtx.p[0][0] = 2.0f * rl;
+   ortho.mtx.p[2][1] = 2.0f * tb;
+   ortho.mtx.p[3][0] = (fr + fl) * -rl;
+   ortho.mtx.p[3][1] = (ft + fb) * -tb;
+   ortho.mtx.p[3][3] = 1.0f;
+   m4x3_identity( ortho.transform );
+   vg_camera_update_view( &ortho );
+   vg_camera_finalize( &ortho );
+
+   glDisable(GL_DEPTH_TEST);
+   glDisable(GL_BLEND);
+   glDisable(GL_CULL_FACE);
+   vg_framebuffer_bind( world->heightmap, 1.0f );
+   shader_blitcolour_use();
+   shader_blitcolour_uColour( (v4f){-9999.0f,-9999.0f,-9999.0f,-9999.0f} );
+   render_fsquad();
+
+   glEnable(GL_BLEND);
+   glBlendFunc(GL_ONE, GL_ONE);
+   glBlendEquation(GL_MAX);
+
+   render_world_position( world, &ortho );
+   glDisable(GL_BLEND);
+   glEnable(GL_DEPTH_TEST);
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+
+   /* upload full buffer */
+   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
+   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
+                    sizeof(struct ub_world_lighting), &world->ub_lighting );
+
+   /*
+    * Allocate cubemaps
+    */
+   for( u32 i=0; i<mdl_arrcount(&world->ent_cubemap); i++ ){
+      ent_cubemap *cm = mdl_arritm(&world->ent_cubemap,i);
+
+      glGenTextures( 1, &cm->texture_id );
+      glBindTexture( GL_TEXTURE_CUBE_MAP, cm->texture_id );
+      glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+
+               for( u32 j=0; j<6; j ++ ) {
+                       glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + j, 0, GL_RGB, 
+                        WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES, 
+                        0, GL_RGB, GL_UNSIGNED_BYTE, NULL );
+               }
+
+               glGenFramebuffers( 1, &cm->framebuffer_id );
+               glBindFramebuffer( GL_FRAMEBUFFER, cm->framebuffer_id );
+               glGenRenderbuffers(1, &cm->renderbuffer_id );
+               glBindRenderbuffer( GL_RENDERBUFFER, cm->renderbuffer_id );
+               glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 
+                              WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES );
+
+               glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                       GL_TEXTURE_CUBE_MAP_POSITIVE_X, cm->texture_id, 0 );
+               glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, 
+                                 GL_RENDERBUFFER, cm->renderbuffer_id );
+
+               glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
+         GL_TEXTURE_CUBE_MAP_POSITIVE_X, cm->texture_id, 0 );
+
+               if( glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE ){
+         vg_error( "Cubemap framebuffer incomplete.\n" );
+      }
+   }
+
+   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+}
+
+/* Loads textures from the pack file */
+void world_gen_load_surfaces( world_instance *world )
+{
+   vg_info( "Loading textures\n" );
+   world->texture_count = 0;
+
+   world->texture_count = world->meta.textures.count+1;
+   world->textures = vg_linear_alloc( world->heap,
+                              vg_align8(sizeof(GLuint)*world->texture_count) );
+   world->textures[0] = vg.tex_missing;
+
+   for( u32 i=0; i<mdl_arrcount(&world->meta.textures); i++ )
+   {
+      mdl_texture *tex = mdl_arritm( &world->meta.textures, i );
+
+      if( !tex->file.pack_size )
+      {
+         vg_fatal_error( "World models must have packed textures!" );
+      }
+
+      vg_linear_clear( vg_mem.scratch );
+      void *src_data = vg_linear_alloc( vg_mem.scratch, 
+                                        tex->file.pack_size );
+      mdl_fread_pack_file( &world->meta, &tex->file, src_data );
+
+      vg_tex2d_load_qoi_async( src_data, tex->file.pack_size,
+                               VG_TEX2D_NEAREST|VG_TEX2D_REPEAT,
+                               &world->textures[i+1] );
+   }
+
+   vg_info( "Loading materials\n" );
+
+   world->surface_count = world->meta.materials.count+1;
+   world->surfaces = vg_linear_alloc( world->heap,
+               vg_align8(sizeof(struct world_surface)*world->surface_count) );
+
+   /* error material */
+   struct world_surface *errmat = &world->surfaces[0];
+   memset( errmat, 0, sizeof(struct world_surface) );
+                       
+   for( u32 i=0; i<mdl_arrcount(&world->meta.materials); i++ )
+   {
+      struct world_surface *surf = &world->surfaces[i+1];
+      surf->info = *(mdl_material *)mdl_arritm( &world->meta.materials, i );
+      surf->flags = 0;
+
+      if( surf->info.shader == k_shader_water )
+      {
+         struct shader_props_water *props = surf->info.props.compiled;
+         world->ub_lighting.g_water_fog = props->fog_scale;
+      }
+
+      if( surf->info.shader == k_shader_standard_cutout ||
+          surf->info.shader == k_shader_foliage )
+      {
+         struct shader_props_standard *props = surf->info.props.compiled;
+         surf->alpha_tex = props->tex_diffuse;
+      }
+      else
+         surf->alpha_tex = 0;
+   }
+}
diff --git a/src/world_gen.h b/src/world_gen.h
new file mode 100644 (file)
index 0000000..c6ffb92
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ *
+ * World generation/population. Different to regular loading, since it needs to
+ * create geometry, apply procedural stuff and save that image to files etc.
+ */
+
+#pragma once
+#include "world.h"
+
+void world_init_blank( world_instance *world );
+void world_gen_load_surfaces( world_instance *world );
+void world_gen_generate_meshes( world_instance *world );
+void world_gen_compute_light_indices( world_instance *world );
+void async_world_postprocess( void *payload, u32 _size );
diff --git a/src/world_info.h b/src/world_info.h
new file mode 100644 (file)
index 0000000..3b29b1f
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#ifndef WORLD_INFO_H
+#define WORLD_INFO_H
+
+/* Purely an information header, shares common strings across client and 
+ * server programs. */
+
+struct track_info
+{
+   const char *name, 
+              *achievement_id;
+}
+static track_infos[] = 
+{
+   {
+      .name = "Megapark Green",
+      .achievement_id = "ROUTE_MPG",
+   },
+   {
+      .name = "Megapark Blue",
+      .achievement_id = "ROUTE_MPB",
+   },
+   {
+      .name = "Megapark Yellow",
+      .achievement_id = "ROUTE_MPY",
+   },
+   {
+      .name = "Megapark Red",
+      .achievement_id = "ROUTE_MPR",
+   },
+   {
+      .name = "Coastal Run",
+      .achievement_id = "ROUTE_TC",
+   },
+   {
+      .name = "Docks Jumps",
+      .achievement_id = "ROUTE_TO",
+   }
+};
+
+#endif
diff --git a/src/world_load.c b/src/world_load.c
new file mode 100644 (file)
index 0000000..30c2b2c
--- /dev/null
@@ -0,0 +1,553 @@
+#include "world_load.h"
+#include "world_routes.h"
+#include "world_gate.h"
+#include "ent_skateshop.h"
+#include "addon.h"
+#include "save.h"
+#include "vg/vg_msg.h"
+#include "network.h"
+#include "player_remote.h"
+#include "vg/vg_loader.h"
+#include "vg/vg_io.h"
+#include <string.h>
+
+/* 
+ * load the .mdl file located in path as a world instance
+ */
+static void world_instance_load_mdl( u32 instance_id, const char *path ){
+   world_instance *world = &world_static.instances[ instance_id ];
+   world_init_blank( world );
+   world->status = k_world_status_loading;
+
+   vg_info( "Loading instance[%u]: %s\n", instance_id, path );
+
+   void *allocator = NULL;
+   if( instance_id == 0 ) allocator = world_static.heap;
+   else allocator = world_static.instances[instance_id-1].heap;
+
+   u32 heap_availible = vg_linear_remaining( allocator );
+   u32 min_overhead = sizeof(vg_linear_allocator);
+
+   if( heap_availible < (min_overhead+1024) ){
+      vg_fatal_error( "out of memory" );
+   }
+
+   u32 size = heap_availible - min_overhead;
+   void *heap = vg_create_linear_allocator( allocator, size, VG_MEMORY_SYSTEM );
+
+   world->heap = heap;
+   mdl_context *meta = &world->meta;
+
+   mdl_open( meta, path, world->heap );
+   mdl_load_metadata_block( meta, world->heap );
+   mdl_load_animation_block( meta, world->heap );
+   mdl_load_mesh_block( meta, world->heap );
+
+   vg_info( "%u\n", sizeof(ent_cubemap) );
+
+   MDL_LOAD_ARRAY( meta, &world->ent_gate,      ent_gate,       heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_camera,    ent_camera,     heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_spawn,     ent_spawn,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_light,     ent_light,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_route_node,ent_route_node, heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_path_index,ent_path_index, heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_checkpoint,ent_checkpoint, heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_route,     ent_route,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_water,     ent_water,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_audio_clip,ent_audio_clip, heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_audio,     ent_audio,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_volume,    ent_volume,     heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_traffic,   ent_traffic,    heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_marker,    ent_marker,     heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_skateshop, ent_skateshop,  heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_swspreview,ent_swspreview, heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_ccmd,      ent_ccmd,       heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_objective, ent_objective,  heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_challenge, ent_challenge,  heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_relay,     ent_relay,      heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_cubemap,   ent_cubemap,    heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_miniworld, ent_miniworld,  heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_prop,      ent_prop,       heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_region,    ent_region,     heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_glider,    ent_glider,     heap );
+   MDL_LOAD_ARRAY( meta, &world->ent_npc,       ent_npc,        heap );
+
+   mdl_array_ptr infos;
+   MDL_LOAD_ARRAY( meta, &infos, ent_worldinfo, vg_mem.scratch );
+
+   world->skybox = k_skybox_default;
+   if( mdl_arrcount(&infos) )
+   {
+      world->info = *((ent_worldinfo *)mdl_arritm(&infos,0));
+
+      if( world->meta.info.version >= 104 )
+      {
+         if( MDL_CONST_PSTREQ( &world->meta, world->info.pstr_skybox,"space"))
+         {
+            world->skybox = k_skybox_space;
+         }
+      }
+   }
+   else
+   {
+      world->info.pstr_author = 0;
+      world->info.pstr_desc = 0;
+      world->info.pstr_name = 0;
+      world->info.timezone = 0.0f;
+      world->info.flags = 0;
+   }
+
+   time_t seconds = time(NULL) % ((u32)vg_maxf(1.0f,k_day_length)*60);
+   world->time  = ((f64)(seconds)/(k_day_length*60.0));
+   world->time += (world->info.timezone/24.0);
+
+   /* process resources from pack */
+   u64 t4 = SDL_GetPerformanceCounter();
+   world_gen_load_surfaces( world );
+   u64 t5 = SDL_GetPerformanceCounter();
+   world_gen_routes_ent_init( world );
+   world_gen_entities_init( world );
+   u64 t6 = SDL_GetPerformanceCounter();
+   
+   /* main bulk */
+   u64 t0 = SDL_GetPerformanceCounter();
+   world_gen_generate_meshes( world );
+   u64 t1 = SDL_GetPerformanceCounter();
+   world_gen_routes_generate( instance_id );
+   u64 t2 = SDL_GetPerformanceCounter();
+   world_gen_compute_light_indices( world );
+   u64 t3 = SDL_GetPerformanceCounter();
+   mdl_close( meta );
+
+   u64 utime_mesh = t1-t0,
+       utime_route = t2-t1,
+       utime_indices = t3-t2,
+       utime_tex = t5-t4,
+       utime_ent = t6-t5,
+       ufreq = SDL_GetPerformanceFrequency();
+
+   f64 ftime_mesh = ((f64)utime_mesh / (f64)ufreq)*1000.0,
+       ftime_route = ((f64)utime_route / (f64)ufreq)*1000.0,
+       ftime_ind = ((f64)utime_route / (f64)ufreq)*1000.0,
+       ftime_tex = ((f64)utime_tex / (f64)ufreq)*1000.0,
+       ftime_ent = ((f64)utime_ent / (f64)ufreq)*1000.0;
+
+   vg_info( "wtime:mesh %.2fms route %.2fms ind %.2fms tex %.2fms ent %.2fms\n",
+               ftime_mesh, ftime_route, ftime_ind, ftime_tex, ftime_ent );
+
+   /* init player position.
+    *   - this is overriden by the save state when(if) it loads */
+   world_default_spawn_pos( world, world->player_co );
+
+   /* allocate leaderboard buffers */
+   u32 bs = mdl_arrcount(&world->ent_route)*sizeof(struct leaderboard_cache);
+   world->leaderboard_cache = vg_linear_alloc( heap, bs );
+
+   for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i ++ )
+   {
+      struct leaderboard_cache *board = &world->leaderboard_cache[i];
+      board->data = vg_linear_alloc( heap, NETWORK_REQUEST_MAX );
+      board->status = k_request_status_client_error;
+      board->cache_time = 0.0;
+      board->data_len = 0;
+   }
+
+   world->routes_ui = vg_linear_alloc( heap, 
+         sizeof(struct route_ui)*mdl_arrcount(&world->ent_route) );
+
+   vg_async_call( async_world_postprocess, world, 0 );
+   vg_async_stall();
+}
+
+struct world_load_complete_data{
+   savedata_file save;
+   enum world_purpose purpose;
+};
+
+static void skaterift_world_load_done( void *payload, u32 size )
+{
+   struct world_load_complete_data *data = payload;
+   world_instance *world = &world_static.instances[ data->purpose ];
+
+   vg_msg sav;
+   vg_msg_init( &sav, data->save.buf, data->save.len );
+
+   if( data->purpose != k_world_purpose_hub )
+   {
+      vg_msg player_frame = sav;
+      if( vg_msg_seekframe( &player_frame, "player" ) )
+      {
+         vg_msg_getkvvecf( &player_frame, "position", k_vg_msg_v3f, 
+                           world->player_co, NULL );
+      }
+   }
+
+   world_entity_start( world, &sav );
+   world->status = k_world_status_loaded;
+   world_static.load_state = k_world_loader_none;
+
+   if( world_static.clear_async_op_when_done )
+   {
+      g_client.loaded = 1;
+      world_static.clear_async_op_when_done = 0;
+   }
+}
+
+/*
+ * Does a complete world switch using the remaining free slots
+ */
+void skaterift_world_load_thread( void *_args )
+{
+   struct world_load_args args = *((struct world_load_args *)_args);
+
+   addon_reg *reg = args.reg;
+   world_static.instance_addons[ args.purpose ] = reg;
+
+   char uid[ADDON_UID_MAX];
+   addon_alias_uid( &reg->alias, uid );
+   vg_info( "LOAD WORLD %s @%d\n", uid, args.purpose );
+
+   char path_buf[4096];
+   vg_str path;
+   vg_strnull( &path, path_buf, 4096 );
+
+   addon_get_content_folder( reg, &path, 1 );
+
+   vg_str folder = path;
+   if( !vg_strgood( &folder ) ) {
+      vg_error( "Load target too long\n" );
+      return;
+   }
+
+   char worlds[k_world_max-1][4096];
+   u32 i=0;
+
+   vg_dir dir;
+   if( !vg_dir_open(&dir, folder.buffer) ){
+      vg_error( "opendir('%s') failed\n", folder.buffer );
+      return;
+   }
+
+   while( vg_dir_next_entry(&dir) ){
+      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file ){
+         const char *d_name = vg_dir_entry_name(&dir);
+         if( d_name[0] == '.' ) continue;
+
+         vg_str file = folder;
+         vg_strcat( &file, "/" );
+         vg_strcat( &file, d_name );
+         if( !vg_strgood( &file ) ) continue;
+
+         char *ext = vg_strch( &file, '.' );
+         if( !ext ) continue;
+         if( strcmp(ext,".mdl") ) continue;
+
+         if( i == k_world_max-1 ){
+            vg_warn( "There are too many .mdl files in the map folder!(3)\n" );
+            break;
+         }
+
+         strcpy( worlds[i++], file.buffer );
+      }
+   }
+   vg_dir_close(&dir);
+
+   if( i == 0 ){
+      vg_warn( "There are no .mdl files in the map folder.\n" );
+   }
+
+   u32 first_index = 0;
+   for( u32 j=0; j<i; j++ ){
+      vg_str name = { .buffer = worlds[j], .i=strlen(worlds[j]), 
+                      sizeof(worlds[j]) };
+      char *fname = vg_strch( &name, '/' );
+      if( fname ){
+         if( !strcmp( fname+1, "main.mdl" ) ){
+            first_index = j;
+         }
+      }
+   }
+
+   world_instance_load_mdl( args.purpose, worlds[first_index] );
+
+   vg_async_item *final_call = 
+      vg_async_alloc( sizeof(struct world_load_complete_data) );
+
+   struct world_load_complete_data *data = final_call->payload;
+   data->purpose = args.purpose;
+
+   skaterift_world_get_save_path( args.purpose, data->save.path );
+   savedata_file_read( &data->save );
+
+   vg_async_dispatch( final_call, skaterift_world_load_done );
+   vg_async_stall();
+}
+
+void skaterift_change_client_world_preupdate(void)
+{
+   if( world_static.load_state != k_world_loader_preload )
+      return;
+
+   /* holding pattern before we can start loading the new world, since we might 
+    * be waiting for audio to stop */
+   for( u32 i=1; i<k_world_max; i++ )
+   {
+      world_instance *inst = &world_static.instances[i];
+      
+      if( inst->status == k_world_status_unloading )
+      {
+         if( world_freeable( inst ) )
+         {
+            world_free( inst );
+         }
+         return;
+      }
+   }
+
+   if( vg_loader_availible() )
+   {
+      vg_info( "worlds cleared, begining load\n" );
+      world_static.load_state = k_world_loader_load;
+
+      vg_linear_clear( vg_async.buffer );
+      struct world_load_args *args = 
+         vg_linear_alloc( vg_async.buffer, sizeof(struct world_load_args) );
+      args->purpose = k_world_purpose_client;
+      args->reg = world_static.instance_addons[ k_world_purpose_client ];
+
+      /* this is replaces the already correct reg but we have to set it again
+       * TOO BAD */
+
+      /* finally can start the loader */
+      vg_loader_start( skaterift_world_load_thread, args );
+   }
+}
+
+/* 
+ * places all loaded worlds into unloading state, pass NULL to reload the world 
+ */
+void skaterift_change_world_start( addon_reg *reg )
+{
+   if( world_static.instance_addons[ k_world_purpose_client ] == reg )
+   {
+      vg_warn( "World is already loaded\n" );
+      return;
+   }
+
+   if( !reg )
+   {
+      if( world_static.instance_addons[ k_world_purpose_client ] )
+      {
+         reg = world_static.instance_addons[ k_world_purpose_client ];
+         world_static.clear_async_op_when_done = 1;
+      }
+      else 
+      {
+         vg_warn( "No client world loaded\n" );
+         return;
+      }
+   }
+
+   world_static.load_state = k_world_loader_preload;
+
+   if( world_static.active_instance != 0 )
+      g_client.loaded = 0;
+
+   char buf[76];
+   addon_alias_uid( &reg->alias, buf );
+   vg_info( "switching to: %s\n", buf );
+   skaterift_autosave(1);
+
+   vg_linear_clear( vg_mem.scratch ); /* ?? */
+   vg_info( "unloading old worlds\n" );
+
+   world_instance *client_world = 
+      &world_static.instances[ k_world_purpose_client ];
+
+   if( client_world->status == k_world_status_loaded )
+   {
+      client_world->status = k_world_status_unloading;
+      world_fadeout_audio( client_world );
+   }
+
+   world_static.instance_addons[ k_world_purpose_client ] = reg;
+   network_send_item( k_netmsg_playeritem_world1 );
+   relink_all_remote_player_worlds();
+   world_unlink_nonlocal( &world_static.instances[k_world_purpose_hub] );
+}
+
+/* console command for the above function */
+int skaterift_load_world_command( int argc, const char *argv[] )
+{
+   if( !vg_loader_availible() ) 
+   {
+      vg_error( "Loading thread is currently unavailible\n" );
+      return 0;
+   }
+
+   if( argc == 1 )
+   {
+      if( !strcmp( argv[0], "reload" ) )
+      {
+         skaterift_change_world_start( NULL );
+         return 0;
+      }
+
+      addon_alias q;
+      addon_uid_to_alias( argv[0], &q );
+
+      u32 reg_id = addon_match( &q );
+      if( reg_id != 0xffffffff )
+      {
+         addon_reg *reg = get_addon_from_index( k_addon_type_world, reg_id, 0 );
+         skaterift_change_world_start( reg );
+      }
+      else 
+      {
+         vg_error( "Addon '%s' is not installed or not found.\n", argv[0] );
+      }
+   }
+   else 
+   {
+      vg_info( "worlds availible to load:\n" );
+         
+      for( int i=0; i<addon_count(k_addon_type_world,0); i ++ )
+      {
+         addon_reg *w = get_addon_from_index( k_addon_type_world, i, 0);
+
+         char buf[ADDON_UID_MAX];
+         addon_alias_uid( &w->alias, buf );
+
+         if( w->flags & ADDON_REG_HIDDEN )
+            vg_info( "  %s [hidden]\n", buf );
+         else
+            vg_info( "  %s\n", buf );
+      }
+   }
+
+   return 0;
+}
+
+/* 
+ * checks:
+ *  1. to see if all audios owned by the world have been stopped
+ *  2. that this is the least significant world
+ */
+int world_freeable( world_instance *world )
+{
+   if( world->status != k_world_status_unloading ) return 0;
+   u8 world_id = (world - world_static.instances) + 1;
+
+   for( u32 i=world_id; i<VG_ARRAY_LEN(world_static.instances); i++ ){
+      if( world_static.instances[i].status != k_world_status_unloaded ){
+         return 0;
+      }
+   }
+
+   int freeable = 1;
+   audio_lock();
+   for( u32 i=0; i<AUDIO_CHANNELS; i++ ){
+      audio_channel *ch = &vg_audio.channels[i];
+      
+      if( ch->allocated && (ch->world_id == world_id)){
+         if( !audio_channel_finished( ch ) ){
+            freeable = 0;
+            break;
+         }
+      }
+   }
+   audio_unlock();
+   return freeable;
+}
+
+/*
+ * Free all resources for world instance
+ */
+void world_free( world_instance *world )
+{
+   vg_info( "Free world @%p\n", world );
+
+   /* free meshes */
+   mesh_free( &world->mesh_route_lines );
+   mesh_free( &world->mesh_geo );
+   mesh_free( &world->mesh_no_collide );
+   
+   /* glDeleteBuffers silently ignores 0's and names that do not correspond to 
+    * existing buffer objects. 
+    * */
+   glDeleteBuffers( 1, &world->tbo_light_entities );
+   glDeleteTextures( 1, &world->tex_light_entities );
+   glDeleteTextures( 1, &world->tex_light_cubes );
+
+   /* delete textures and meshes */
+   glDeleteTextures( world->texture_count-1, world->textures+1 );
+
+   u32 world_index = world - world_static.instances;
+   if( world_index ){
+      vg_linear_del( world_static.instances[world_index-1].heap, 
+                     vg_linear_header(world->heap) );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_cubemap); i++ ){
+      ent_cubemap *cm = mdl_arritm(&world->ent_cubemap,i);
+      glDeleteTextures( 1, &cm->texture_id );
+      glDeleteFramebuffers( 1, &cm->framebuffer_id );
+      glDeleteRenderbuffers( 1, &cm->renderbuffer_id );
+   }
+
+   world->status = k_world_status_unloaded;
+}
+
+/* 
+ * reset the world structure without deallocating persistent buffers 
+ * TODO: Make this a memset(0), and have persistent items live in a static loc
+ */
+void world_init_blank( world_instance *world )
+{
+   memset( &world->meta, 0, sizeof(mdl_context) );
+
+   world->textures = NULL;
+   world->texture_count = 0;
+   world->surfaces = NULL;
+   world->surface_count = 0;
+
+   world->geo_bh = NULL;
+   world->entity_bh = NULL;
+   world->entity_list = NULL;
+   world->rendering_gate = NULL;
+
+   world->water.enabled = 0;
+   world->time = 0.0;
+
+   /* default lighting conditions 
+    * -------------------------------------------------------------*/
+   struct ub_world_lighting *state = &world->ub_lighting;
+
+   state->g_light_preview = 0;
+   state->g_shadow_samples = 8;
+   state->g_water_fog = 0.04f;
+
+   v4_zero( state->g_water_plane );
+   v4_zero( state->g_depth_bounds );
+
+   state->g_shadow_length = 9.50f;
+   state->g_shadow_spread = 0.65f;
+
+#if 0
+   /* 2023 style */
+   v3_copy( (v3f){0.37f, 0.54f, 0.97f}, state->g_daysky_colour );
+   v3_copy( (v3f){0.03f, 0.05f, 0.20f}, state->g_nightsky_colour );
+   v3_copy( (v3f){1.00f, 0.32f, 0.01f}, state->g_sunset_colour );
+   v3_copy( (v3f){0.13f, 0.17f, 0.35f}, state->g_ambient_colour );
+   v3_copy( (v3f){0.25f, 0.17f, 0.51f}, state->g_sunset_ambient );
+   v3_copy( (v3f){1.10f, 0.89f, 0.35f}, state->g_sun_colour );
+#else
+   /* 2024 style */
+   v3_copy( (v3f){0.308f, 0.543f, 0.904f}, state->g_daysky_colour );
+   v3_copy( (v3f){0.030f, 0.050f, 0.200f}, state->g_nightsky_colour );
+   v3_copy( (v3f){1.000f, 0.320f, 0.010f}, state->g_sunset_colour );
+   v3_copy( (v3f){0.130f, 0.170f, 0.350f}, state->g_ambient_colour );
+   v3_copy( (v3f){0.25f, 0.17f, 0.51f}, state->g_sunset_ambient );
+   v3_copy( (v3f){1.000f, 0.809f, 0.318f}, state->g_sun_colour );
+#endif
+}
diff --git a/src/world_load.h b/src/world_load.h
new file mode 100644 (file)
index 0000000..038233d
--- /dev/null
@@ -0,0 +1,11 @@
+#pragma once
+#include <time.h>
+
+#include "world.h"
+#include "addon.h"
+
+void world_free( world_instance *world );
+int world_freeable( world_instance *world );
+int skaterift_load_world_command( int argc, const char *argv[] );
+void skaterift_change_world_start( addon_reg *reg );
+void skaterift_change_client_world_preupdate(void);
diff --git a/src/world_map.c b/src/world_map.c
new file mode 100644 (file)
index 0000000..2799428
--- /dev/null
@@ -0,0 +1,313 @@
+#include "skaterift.h"
+#include "world_map.h"
+#include "world.h"
+#include "input.h"
+#include "gui.h"
+#include "menu.h"
+#include "scene.h"
+
+struct world_map world_map;
+
+static void world_map_get_dir( v3f dir )
+{
+   /* idk */
+   dir[0] = -sqrtf(0.5f);
+   dir[2] =  sqrtf(0.5f);
+   dir[1] =  1.0f;
+   v3_normalize(dir);
+}
+
+static void world_map_get_plane( v4f plane )
+{
+   world_instance *world = &world_static.instances[ world_map.world_id ];
+   f32 h = localplayer.rb.co[1];
+   if( world_map.world_id != world_static.active_instance )
+      h = (world->scene_geo.bbx[0][1] + world->scene_geo.bbx[1][1]) * 0.5f;
+
+   v4_copy( (v4f){0.0f,1.0f,0.0f,h}, plane );
+}
+
+static void respawn_world_to_plane_pos( v3f pos, v2f plane_pos )
+{
+   v3f dir;
+   world_map_get_dir( dir );
+   v3_negate(dir,dir);
+   v4f plane;
+   world_map_get_plane( plane );
+
+   v3f co;
+   f32 t = ray_plane( plane, pos, dir );
+   v3_muladds( pos, dir, t, co );
+   plane_pos[0] = co[0];
+   plane_pos[1] = co[2];
+}
+
+static void respawn_map_draw_icon( vg_camera *cam, 
+                                   enum gui_icon icon, v3f pos, f32 size )
+{
+   v4f v;
+   v3_copy( pos, v );
+   v[3] = 1.0f;
+   m4x4_mulv( cam->mtx.pv, v, v );
+   v2_divs( v, v[3], v );
+
+   gui_draw_icon( icon, (v2f){ v[0]*0.5f+0.5f,v[1]*0.5f+0.5f }, size );
+}
+
+static void world_map_select_close(void)
+{
+   world_map.sel_spawn = world_map.close_spawn;
+   gui_helper_clear();
+
+   vg_str text;
+   if( gui_new_helper( input_button_list[k_srbind_maccept], &text ) )
+      vg_strcat( &text, "Spawn Here" );
+   if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
+      vg_strcat( &text, "Back" );
+}
+
+void world_map_click(void)
+{
+   world_map_select_close();
+}
+
+static void world_map_help_normal(void)
+{
+   gui_helper_clear();
+
+   vg_str text;
+   if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ) )
+      vg_strcat( &text, "Move" );
+
+   if( gui_new_helper( input_button_list[k_srbind_maccept], &text ) )
+      vg_strcat( &text, "Select" );
+
+   if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
+      vg_strcat( &text, "Exit" );
+
+   if( world_static.instances[1].status == k_world_status_loaded )
+   {
+      if( gui_new_helper( input_button_list[k_srbind_mhub], &text ) )
+         vg_strcat( &text, world_static.active_instance? 
+                              "Go to Hub": "Go to Active World" );
+   }
+}
+
+void world_map_pre_update(void)
+{
+   if( menu_viewing_map() )
+   {
+      if( !world_map.view_ready )
+      {
+         world_map.world_id = world_static.active_instance;
+
+         world_instance *world = &world_static.instances[ world_map.world_id ];
+         v3f *bbx = world->scene_geo.bbx;
+
+         v3_copy( localplayer.rb.co, world->player_co );
+         respawn_world_to_plane_pos( localplayer.rb.co, world_map.plane_pos );
+         world_map.boom_dist = 400.0f;
+         world_map.home_select = 0;
+         world_map.view_ready = 1;
+         world_map.sel_spawn = NULL;
+         world_map.close_spawn = NULL;
+
+         world_map_help_normal();
+      }
+   }
+   else
+   {
+      if( world_map.view_ready )
+      {
+         gui_helper_clear();
+         world_map.view_ready = 0;
+      }
+
+      return;
+   }
+
+   world_instance *world = &world_static.instances[ world_map.world_id ];
+   v3f *bbx = world->scene_geo.bbx;
+   f32 *pos = world_map.plane_pos;
+
+   v2f steer;
+   joystick_state( k_srjoystick_steer, steer );
+   v2_normalize_clamp( steer );
+
+   if( !world_map.sel_spawn )
+   {
+      f32 *pos = world_map.plane_pos;
+      m2x2f rm;
+      m2x2_create_rotation( rm, -0.25f*VG_PIf );
+      m2x2_mulv( rm, steer, steer );
+      v2_muladds( pos, steer, vg.time_frame_delta * 200.0f, pos );
+   }
+
+   f32 bd_target = 400.0f,
+       interp = vg.time_frame_delta*2.0f;
+
+   if( world_map.sel_spawn )
+   {
+      v2f pp;
+      respawn_world_to_plane_pos( world_map.sel_spawn->transform.co, pp );
+      v2_lerp( pos, pp, interp, pos );
+
+      bd_target = 200.0f;
+   }
+   world_map.boom_dist = vg_lerpf( world_map.boom_dist, bd_target, interp );
+
+   v2_minv( (v2f){ bbx[1][0], bbx[1][2] }, pos, pos );
+   v2_maxv( (v2f){ bbx[0][0], bbx[0][2] }, pos, pos );
+
+   /* update camera */
+   vg_camera *cam = &world_map.cam;
+   v3f dir;
+   world_map_get_dir(dir);
+
+   v4f plane;
+   world_map_get_plane( plane );
+
+   v3f co = { pos[0], plane[3]*plane[1], pos[1] };
+   v3_muladds( co, dir, world_map.boom_dist, cam->pos );
+
+   vg_line_cross( co, VG__RED, 10.0f );
+
+   cam->angles[0] = 0.25f * VG_PIf;
+   cam->angles[1] = 0.25f * VG_PIf;
+   cam->farz = 5000.0f;
+   cam->nearz = 10.0f;
+   cam->fov = 40.0f;
+
+   vg_camera_update_transform( cam );
+   vg_camera_update_view( cam );
+   vg_camera_update_projection( cam );
+   vg_camera_finalize( cam );
+
+   /* pick spawn */
+   f32 closest2 = INFINITY;
+   v2f centroid = { 0, 0 };
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ )
+   {
+      ent_spawn *spawn = mdl_arritm(&world->ent_spawn,i);
+
+      v4f v;
+      v3_copy( spawn->transform.co, v );
+      v[3] = 1.0f;
+      m4x4_mulv( cam->mtx.pv, v, v );
+      v2_divs( v, v[3], v );
+
+      f32 d2 = v2_dist2(v, centroid);
+      if( d2 < closest2 )
+      {
+         world_map.close_spawn = spawn;
+         closest2 = d2;
+      }
+      spawn->transform.s[0] = d2;
+   }
+
+   if( button_down( k_srbind_maccept ) )
+   {
+      if( world_map.sel_spawn )
+      {
+         skaterift.activity = k_skaterift_default;
+         world_static.active_instance = world_map.world_id;
+         srinput.state = k_input_state_resume;
+         player__spawn( world_map.sel_spawn );
+         return;
+      }
+      else
+      {
+         world_map_select_close();
+      }
+   }
+
+   if( button_down( k_srbind_mback ) )
+   {
+      if( world_map.sel_spawn )
+      {
+         world_map.sel_spawn = NULL;
+         world_map_help_normal();
+      }
+      else
+      {
+         srinput.state = k_input_state_resume;
+         skaterift.activity = k_skaterift_default;
+         return;
+      }
+   }
+
+   /* icons
+    * ---------------------*/
+   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ )
+   {
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
+
+      enum gui_icon icon = k_gui_icon_exclaim_2d;
+      if( challenge->status )
+         icon = k_gui_icon_tick_2d;
+
+      respawn_map_draw_icon( cam, icon, challenge->transform.co, 1.0f );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i ++ )
+   {
+      ent_spawn *spawn = mdl_arritm( &world->ent_spawn, i );
+
+      if( spawn->transform.s[0] > 0.3f )
+         continue;
+
+      f32 s = 1.0f-(spawn->transform.s[0] / 0.3f);
+      respawn_map_draw_icon( cam, 
+            spawn==world_map.sel_spawn? 
+               k_gui_icon_spawn_select: k_gui_icon_spawn,
+            spawn->transform.co, s );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_skateshop); i++ )
+   {
+      ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, i );
+      if( shop->type == k_skateshop_type_boardshop )
+      {
+         respawn_map_draw_icon( cam, k_gui_icon_board, shop->transform.co, 1 );
+      }
+      else if( shop->type == k_skateshop_type_worldshop )
+      {
+         respawn_map_draw_icon( cam, k_gui_icon_world, shop->transform.co, 1 );
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ )
+   {
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+      if( gate->flags & k_ent_gate_nonlocal )
+      {
+         respawn_map_draw_icon( cam, k_gui_icon_rift, gate->co[0], 1 );
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ )
+   {
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+
+      v4f colour;
+      v4_copy( route->colour, colour );
+      v3_muls( colour, 1.6666f, colour );
+      gui_icon_setcolour( colour );
+      respawn_map_draw_icon( cam, k_gui_icon_rift_run_2d, 
+                             route->board_transform[3], 1 );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_glider); i ++ )
+   {
+      ent_glider *glider = mdl_arritm( &world->ent_glider, i );
+
+      v4f colour = { 1,1,1,1 };
+
+      if( !(glider->flags & 0x1) )
+         v3_muls( colour, 0.5f, colour );
+      gui_icon_setcolour( colour );
+
+      respawn_map_draw_icon( cam, k_gui_icon_glider, glider->transform.co, 1 );
+   }
+}
diff --git a/src/world_map.h b/src/world_map.h
new file mode 100644 (file)
index 0000000..3899363
--- /dev/null
@@ -0,0 +1,19 @@
+#pragma once
+#include "vg/vg_platform.h"
+#include "vg/vg_camera.h"
+#include "world_entity.h"
+
+struct world_map
+{
+   v2f plane_pos;
+   f32 boom_dist;
+   u32 world_id;
+   u32 home_select;
+
+   ent_spawn *sel_spawn, *close_spawn;
+   vg_camera cam;
+
+   bool view_ready;
+}
+extern world_map;
+void world_map_pre_update(void);
diff --git a/src/world_physics.c b/src/world_physics.c
new file mode 100644 (file)
index 0000000..03be1fc
--- /dev/null
@@ -0,0 +1,107 @@
+#ifndef WORLD_PHYSICS_C
+#define WORLD_PHYSICS_C
+
+#include "world.h"
+#include "world_physics.h"
+
+void ray_world_get_tri( world_instance *world, ray_hit *hit, v3f tri[3] )
+{
+   for( int i=0; i<3; i++ )
+      v3_copy( world->scene_geo.arrvertices[ hit->tri[i] ].co, tri[i] );
+}
+
+int ray_world( world_instance *world,
+               v3f pos, v3f dir, ray_hit *hit, u16 ignore )
+{
+   return scene_raycast( &world->scene_geo, world->geo_bh, pos, dir, hit, 
+                         ignore );
+}
+
+/*
+ * Cast a sphere from a to b and see what time it hits
+ */
+int spherecast_world( world_instance *world,
+                      v3f pa, v3f pb, float r, float *t, v3f n, u16 ignore )
+{
+   boxf region;
+   box_init_inf( region );
+   box_addpt( region, pa );
+   box_addpt( region, pb );
+   
+   v3_add( (v3f){ r, r, r}, region[1], region[1] );
+   v3_add( (v3f){-r,-r,-r}, region[0], region[0] );
+
+   v3f dir;
+   v3_sub( pb, pa, dir );
+
+   v3f dir_inv;
+   dir_inv[0] = 1.0f/dir[0];
+   dir_inv[1] = 1.0f/dir[1];
+   dir_inv[2] = 1.0f/dir[2];
+
+   int hit = -1;
+   float min_t = 1.0f;
+
+   bh_iter it;
+   bh_iter_init_box( 0, &it, region );
+   i32 idx;
+   while( bh_next( world->geo_bh, &it, &idx ) ){
+      u32 *ptri = &world->scene_geo.arrindices[ idx*3 ];
+      if( world->scene_geo.arrvertices[ptri[0]].flags & ignore ) continue;
+
+      v3f tri[3];
+      boxf box;
+      box_init_inf( box );
+      for( int j=0; j<3; j++ ){
+         v3_copy( world->scene_geo.arrvertices[ptri[j]].co, tri[j] );
+         box_addpt( box, tri[j] );
+      }
+
+      v3_add( (v3f){ r, r, r}, box[1], box[1] );
+      v3_add( (v3f){-r,-r,-r}, box[0], box[0] );
+
+      if( !ray_aabb1( box, pa, dir_inv, 1.0f ) )
+         continue;
+      
+      float t;
+      v3f n1;
+      if( spherecast_triangle( tri, pa, dir, r, &t, n1 ) ){
+         if( t < min_t ){
+            min_t = t;
+            hit = idx;
+            v3_copy( n1, n );
+         }
+      }
+   }
+
+   *t = min_t;
+   return hit;
+}
+
+struct world_surface *world_tri_index_surface( world_instance *world, 
+                                               u32 index )
+{
+   for( int i=1; i<world->surface_count; i++ ){
+      struct world_surface *surf = &world->surfaces[i];
+
+      if( (index >= surf->sm_geo.vertex_start) &&
+          (index  < surf->sm_geo.vertex_start+surf->sm_geo.vertex_count ) )
+      {
+         return surf;
+      }
+   }
+
+   return &world->surfaces[0];
+}
+
+struct world_surface *world_contact_surface( world_instance *world, rb_ct *ct )
+{
+   return world_tri_index_surface( world, ct->element_id );
+}
+
+struct world_surface *ray_hit_surface( world_instance *world, ray_hit *hit )
+{
+   return world_tri_index_surface( world, hit->tri[0] );
+}
+
+#endif /* WORLD_PHYSICS_C */
diff --git a/src/world_physics.h b/src/world_physics.h
new file mode 100644 (file)
index 0000000..06143b9
--- /dev/null
@@ -0,0 +1,24 @@
+#pragma once
+#include "world.h"
+#include "vg/vg_rigidbody.h"
+#include "vg/vg_rigidbody_collision.h"
+#include "vg/vg_bvh.h"
+
+void ray_world_get_tri( world_instance *world,
+                           ray_hit *hit, v3f tri[3] );
+
+int ray_world( world_instance *world,
+                  v3f pos, v3f dir, ray_hit *hit, u16 ignore );
+
+int spherecast_world( world_instance *world,
+                         v3f pa, v3f pb, float r, float *t, v3f n,
+                         u16 ignore );
+
+struct world_surface *world_tri_index_surface( world_instance *world, 
+                                                  u32 index );
+
+struct world_surface *world_contact_surface( world_instance *world,
+                                                  rb_ct *ct );
+
+struct world_surface *ray_hit_surface( world_instance *world,
+                                                 ray_hit *hit );
diff --git a/src/world_render.c b/src/world_render.c
new file mode 100644 (file)
index 0000000..3eb07a1
--- /dev/null
@@ -0,0 +1,1377 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#include "world.h"
+#include "world_render.h"
+#include "font.h"
+#include "gui.h"
+#include "world_map.h"
+#include "ent_miniworld.h"
+#include "player_remote.h"
+#include "ent_skateshop.h"
+#include "ent_npc.h"
+#include "shaders/model_entity.h"
+
+struct world_render world_render;
+
+static int ccmd_set_time( int argc, const char *argv[] ){
+   world_instance *world = world_current_instance();
+   if( argc == 1 )
+      world->time = atof( argv[0] );
+   else 
+      vg_error( "Usage set_time <0-1.0> (current time: %f)\n", world->time );
+   return 0;
+}
+
+static void async_world_render_init( void *payload, u32 size )
+{
+   vg_info( "Allocate uniform buffers\n" );
+   for( int i=0; i<k_world_max; i++ )
+   {
+      world_instance *world = &world_static.instances[i];
+      world->ubo_bind_point = i;
+
+      glGenBuffers( 1, &world->ubo_lighting );
+      glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
+      glBufferData( GL_UNIFORM_BUFFER, sizeof(struct ub_world_lighting), 
+                    NULL, GL_DYNAMIC_DRAW );
+
+      glBindBufferBase( GL_UNIFORM_BUFFER, i, world->ubo_lighting );
+   }
+}
+
+void world_render_init(void)
+{
+   VG_VAR_F32( k_day_length );
+   VG_VAR_I32( k_debug_light_indices );
+   VG_VAR_I32( k_debug_light_complexity );
+   VG_VAR_I32( k_light_preview );
+   VG_VAR_I32( k_light_editor );
+   vg_console_reg_cmd( "set_time", ccmd_set_time, NULL );
+
+   world_render.sky_rate = 1.0;
+   world_render.sky_target_rate = 1.0;
+
+   vg_info( "Loading world resources\n" );
+   vg_linear_clear( vg_mem.scratch );
+
+   mdl_context msky;
+   mdl_open( &msky, "models/rs_skydome.mdl", vg_mem.scratch );
+   mdl_load_metadata_block( &msky, vg_mem.scratch );
+   mdl_async_load_glmesh( &msky, &world_render.skydome, NULL );
+   mdl_close( &msky );
+
+   vg_info( "Loading default world textures\n" );
+   vg_tex2d_load_qoi_async_file( "textures/garbage.qoi", 
+                                 VG_TEX2D_NEAREST|VG_TEX2D_REPEAT, 
+                                 &world_render.tex_terrain_noise );
+
+   vg_info( "Allocate frame buffers\n" );
+   for( int i=0; i<k_world_max; i++ )
+   {
+      world_instance *world = &world_static.instances[i];
+      world->heightmap = vg_framebuffer_allocate( vg_mem.rtmemory, 1, 0 );
+      world->heightmap->display_name = NULL;
+      world->heightmap->fixed_w = 1024;
+      world->heightmap->fixed_h = 1024;
+      world->heightmap->resolution_div = 0;
+      world->heightmap->attachments[0] = (vg_framebuffer_attachment)
+      {
+         NULL, k_framebuffer_attachment_type_texture,
+         .internalformat = GL_RG16F,
+         .format         = GL_RG,
+         .type           = GL_FLOAT,
+         .attachment     = GL_COLOR_ATTACHMENT0
+      };
+      vg_framebuffer_create( world->heightmap );
+   }
+
+   vg_async_call( async_world_render_init, NULL, 0 );
+}
+
+/* 
+ * standard uniform bindings
+ * ----------------------------------------------------------------------------
+ */
+void world_link_lighting_ub( world_instance *world, GLuint shader )
+{
+   GLuint idx = glGetUniformBlockIndex( shader, "ub_world_lighting" );   
+   glUniformBlockBinding( shader, idx, world->ubo_bind_point );
+}
+
+void world_bind_position_texture( world_instance *world, 
+                                  GLuint shader, GLuint location,
+                                  int slot )
+{
+   vg_framebuffer_bind_texture( world->heightmap, 0, slot );
+   glUniform1i( location, slot );
+}
+
+void world_bind_light_array( world_instance *world,
+                             GLuint shader, GLuint location, 
+                             int slot )
+{
+   glActiveTexture( GL_TEXTURE0 + slot );
+   glBindTexture( GL_TEXTURE_BUFFER, world->tex_light_entities );
+   glUniform1i( location, slot );
+}
+
+void world_bind_light_index( world_instance *world,
+                             GLuint shader, GLuint location,
+                             int slot )
+{
+   glActiveTexture( GL_TEXTURE0 + slot );
+   glBindTexture( GL_TEXTURE_3D, world->tex_light_cubes );
+   glUniform1i( location, slot );
+}
+
+void bind_terrain_noise(void)
+{
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
+}
+
+/* 
+ * Get OpenGL texture name from texture ID.
+ */
+static GLuint world_get_texture( world_instance *world, u32 id ){
+   if( id & 0x80000000 ) return skaterift.rt_textures[id & ~0x80000000];
+   else                  return world->textures[ id ];
+}
+
+/*
+ * Passes Rendering
+ * ----------------------------------------------------------------------------
+ */
+
+struct world_pass
+{
+   vg_camera *cam;
+   enum mdl_shader shader;
+   enum world_geo_type geo_type;
+
+   void (*fn_bind)( world_instance *world, struct world_surface *mat );
+   void (*fn_set_mdl)( m4x3f mdl );
+   void (*fn_set_uPvmPrev)( m4x4f pvm );
+   void (*fn_set_uNormalMtx)( m3x3f mnorm );
+};
+
+void render_world_depth( world_instance *world, vg_camera *cam );
+
+/*
+ * Render a run of submeshes, only of those which match material_id
+ */
+static void world_render_submeshes( world_instance *world,
+                                    struct world_pass *pass, 
+                                    mdl_transform *transform, 
+                                    u32 start, u32 count, u32 material_id )
+{
+   for( u32 k=0; k<count; k++ )
+   {
+      mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, start+k );
+      if( sm->material_id != material_id ) 
+         continue;
+
+      m4x3f mmdl;
+      mdl_transform_m4x3( transform, mmdl );
+
+      m4x4f m4mdl;
+      m4x3_expand( mmdl, m4mdl );
+      m4x4_mul( pass->cam->mtx_prev.pv, m4mdl, m4mdl );
+
+      pass->fn_set_mdl( mmdl );
+      pass->fn_set_uPvmPrev( m4mdl );
+
+      mdl_draw_submesh( sm );
+   }
+}
+
+/*
+ * Render props attached to this material
+ */
+static void world_render_props( world_instance *world, u32 material_id,
+                                struct world_pass *pass )
+{
+   struct world_surface *mat = &world->surfaces[ material_id ];
+   if( !(mat->flags & WORLD_SURFACE_HAS_PROPS) ) return;
+
+   pass->fn_bind( world, mat );
+
+   for( u32 j=0; j<mdl_arrcount( &world->ent_prop ); j++ ){
+      ent_prop *prop = mdl_arritm( &world->ent_prop, j );
+      if( prop->flags & 0x1 ) continue;
+
+      world_render_submeshes( world, pass, &prop->transform, 
+            prop->submesh_start, prop->submesh_count, material_id );
+   }
+}
+
+/*
+ * Render traffic models attactched to this material
+ */
+static void world_render_traffic( world_instance *world, u32 material_id,
+                                  struct world_pass *pass )
+{
+   struct world_surface *mat = &world->surfaces[ material_id ];
+   if( !(mat->flags & WORLD_SURFACE_HAS_TRAFFIC) ) return;
+
+   pass->fn_bind( world, mat );
+
+   for( u32 j=0; j<mdl_arrcount( &world->ent_traffic ); j++ ){
+      ent_traffic *traffic = mdl_arritm( &world->ent_traffic, j );
+
+      world_render_submeshes( world, pass, &traffic->transform,
+                              traffic->submesh_start, traffic->submesh_count,
+                              material_id );
+   }
+}
+
+/*
+ * Iterate and render all materials which match the passes shader and geometry
+ * type. Includes props/traffic.
+ */
+static void world_render_pass( world_instance *world, struct world_pass *pass )
+{
+   for( int i=0; i<world->surface_count; i++ )
+   {
+      struct world_surface *mat = &world->surfaces[i];
+
+      if( mat->info.shader == pass->shader )
+      {
+         mdl_submesh *sm;
+
+         if( pass->geo_type == k_world_geo_type_solid )
+         {
+            sm = &mat->sm_geo;
+         }
+         else
+         {
+            world_render_traffic( world, i, pass );
+            world_render_props( world, i, pass );
+            sm = &mat->sm_no_collide;
+         }
+
+         if( !sm->indice_count )
+            continue;
+
+         m4x3f mmdl;
+         m4x3_identity( mmdl );
+         pass->fn_set_mdl( mmdl );
+         pass->fn_set_uPvmPrev( pass->cam->mtx_prev.pv );
+         pass->fn_bind( world, mat );
+         mdl_draw_submesh( sm );
+      }
+   }
+}
+
+/*
+ * Specific shader instructions
+ * ----------------------------------------------------------------------------
+ */
+
+static void world_render_both_stages( world_instance *world, 
+                                      struct world_pass *pass )
+{
+   mesh_bind( &world->mesh_geo );
+   pass->geo_type = k_world_geo_type_solid;
+   world_render_pass( world, pass );
+
+   glDisable( GL_CULL_FACE );
+   mesh_bind( &world->mesh_no_collide );
+   pass->geo_type = k_world_geo_type_nonsolid;
+   world_render_pass( world, pass );
+   glEnable( GL_CULL_FACE );
+}
+
+static void bindpoint_world_vb( world_instance *world,
+                                struct world_surface *mat )
+{
+   struct shader_props_vertex_blend *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
+
+#if 0
+   shader_scene_vertex_blend_uOffset( props->blend_offset );
+#endif
+}
+
+static void render_world_vb( world_instance *world, vg_camera *cam )
+{
+   shader_scene_vertex_blend_use();
+   shader_scene_vertex_blend_uTexGarbage(0);
+   shader_scene_vertex_blend_uTexGradients(1);
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_vertex_blend );
+
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
+
+   shader_scene_vertex_blend_uPv( cam->mtx.pv );
+   shader_scene_vertex_blend_uCamera( cam->transform[3] );
+
+   struct world_pass pass = 
+   {
+      .shader = k_shader_standard_vertex_blend,
+      .cam = cam,
+      .fn_bind = bindpoint_world_vb,
+      .fn_set_mdl = shader_scene_vertex_blend_uMdl,
+      .fn_set_uPvmPrev = shader_scene_vertex_blend_uPvmPrev,
+   };
+
+   world_render_both_stages( world, &pass );
+}
+
+static void world_shader_standard_bind( world_instance *world, vg_camera *cam )
+{
+   shader_scene_standard_use();
+   shader_scene_standard_uTexGarbage(0);
+   shader_scene_standard_uTexMain(1);
+   shader_scene_standard_uPv( cam->mtx.pv );
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_standard );
+
+   bind_terrain_noise();
+   shader_scene_standard_uCamera( cam->transform[3] );
+}
+
+static void bindpoint_standard( world_instance *world, 
+                                struct world_surface *mat )
+{
+   struct shader_props_standard *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
+}
+
+static void render_world_standard( world_instance *world, vg_camera *cam )
+{
+   world_shader_standard_bind( world, cam );
+   struct world_pass pass = 
+   {
+      .shader = k_shader_standard,
+      .cam = cam,
+      .fn_bind = bindpoint_standard,
+      .fn_set_mdl = shader_scene_standard_uMdl,
+      .fn_set_uPvmPrev = shader_scene_standard_uPvmPrev,
+   };
+
+   world_render_both_stages( world, &pass );
+}
+
+static void bindpoint_world_cubemapped( world_instance *world,
+                                        struct world_surface *mat )
+{
+   struct shader_props_cubemapped *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, 
+                  world_get_texture( world,props->tex_diffuse ) );
+
+   u32 cubemap_id = props->cubemap_entity,
+       cubemap_index = 0;
+
+   if( mdl_entity_id_type( cubemap_id ) == k_ent_cubemap )
+   {
+      cubemap_index = mdl_entity_id_id( cubemap_id );
+   }
+
+   ent_cubemap *cm = mdl_arritm( &world->ent_cubemap, cubemap_index );
+   glActiveTexture( GL_TEXTURE10 );
+   glBindTexture( GL_TEXTURE_CUBE_MAP, cm->texture_id );
+
+   shader_scene_cubemapped_uColour( props->tint );
+}
+
+static void bindpoint_world_cubemapped_disabled( world_instance *world,
+                                                 struct world_surface *mat )
+{
+   struct shader_props_cubemapped *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, 
+                  world_get_texture( world, props->tex_diffuse ) );
+}
+
+static void render_world_cubemapped( world_instance *world, vg_camera *cam,
+                                     int enabled )
+{
+   if( !mdl_arrcount( &world->ent_cubemap ) )
+      return;
+
+   if( !enabled )
+   {
+      world_shader_standard_bind( world, cam );
+
+      struct world_pass pass = 
+      {
+         .shader = k_shader_cubemap,
+         .cam = cam,
+         .fn_bind = bindpoint_world_cubemapped_disabled,
+         .fn_set_mdl = shader_scene_standard_uMdl,
+         .fn_set_uPvmPrev = shader_scene_standard_uPvmPrev,
+      };
+
+      world_render_both_stages( world, &pass );
+   }
+   else 
+   {
+      shader_scene_cubemapped_use();
+      shader_scene_cubemapped_uTexGarbage(0);
+      shader_scene_cubemapped_uTexMain(1);
+      shader_scene_cubemapped_uTexCubemap(10);
+      shader_scene_cubemapped_uPv( cam->mtx.pv );
+
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_cubemapped );
+
+      bind_terrain_noise();
+      shader_scene_cubemapped_uCamera( cam->transform[3] );
+      
+      struct world_pass pass = 
+      {
+         .shader = k_shader_cubemap,
+         .cam = cam,
+         .fn_bind = bindpoint_world_cubemapped,
+         .fn_set_mdl = shader_scene_cubemapped_uMdl,
+         .fn_set_uPvmPrev = shader_scene_cubemapped_uPvmPrev,
+      };
+
+      world_render_both_stages( world, &pass );
+   }
+}
+
+static void render_world_alphatest( world_instance *world, vg_camera *cam )
+{
+   shader_scene_standard_alphatest_use();
+   shader_scene_standard_alphatest_uTexGarbage(0);
+   shader_scene_standard_alphatest_uTexMain(1);
+   shader_scene_standard_alphatest_uPv( cam->mtx.pv );
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_standard_alphatest );
+
+   bind_terrain_noise();
+   shader_scene_standard_alphatest_uCamera( cam->transform[3] );
+   glDisable(GL_CULL_FACE);
+
+   struct world_pass pass = 
+   {
+      .shader = k_shader_standard_cutout,
+      .cam = cam,
+      .fn_bind = bindpoint_standard,
+      .fn_set_mdl = shader_scene_standard_alphatest_uMdl,
+      .fn_set_uPvmPrev = shader_scene_standard_alphatest_uPvmPrev,
+   };
+
+   world_render_both_stages( world, &pass );
+   glEnable(GL_CULL_FACE);
+}
+
+static void render_world_foliage( world_instance *world, vg_camera *cam )
+{
+   shader_scene_foliage_use();
+   shader_scene_foliage_uTexGarbage(0);
+   shader_scene_foliage_uTexMain(1);
+   shader_scene_foliage_uPv( cam->mtx.pv );
+   shader_scene_foliage_uTime( vg.time );
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_foliage );
+   bind_terrain_noise();
+
+   shader_scene_foliage_uCamera( cam->transform[3] );
+   glDisable(GL_CULL_FACE);
+   struct world_pass pass = 
+   {
+      .shader = k_shader_foliage,
+      .cam = cam,
+      .fn_bind = bindpoint_standard,
+      .fn_set_mdl = shader_scene_foliage_uMdl,
+      .fn_set_uPvmPrev = shader_scene_foliage_uPvmPrev,
+   };
+   world_render_both_stages( world, &pass );
+   glEnable(GL_CULL_FACE);
+}
+
+static void world_render_challenges( world_instance *world, 
+                                     struct world_pass *pass, v3f pos )
+{
+   if( !world ) return;
+   if( skaterift.activity == k_skaterift_replay ) return;
+   if( world != world_current_instance() ) return;
+
+   /* sort lists */
+   f32 radius = 40.0f;
+
+   u32 objective_list[ 32 ],
+       challenge_list[ 16 ];
+
+   v2f objective_uv_offsets[ 32 ];
+
+   u32 objective_count = 0,
+       challenge_count = 0;
+
+   ent_challenge *active_challenge = NULL;
+   int running = 0;
+   if( mdl_entity_id_type( world_static.focused_entity ) == k_ent_challenge ){
+      if( (skaterift.activity == k_skaterift_default) &&
+           world_static.challenge_target ){
+         running = 1;
+      }
+
+      if( !((skaterift.activity != k_skaterift_ent_focus) &&
+            !world_static.challenge_target) ){
+         world_instance *challenge_world = world_current_instance();
+         u32 index = mdl_entity_id_id( world_static.focused_entity );
+         active_challenge = mdl_arritm(&challenge_world->ent_challenge, index);
+      }
+   }
+
+   if( active_challenge ){
+      shader_scene_fxglow_uUvOffset( (v2f){ 8.0f/256.0f, 0.0f } );
+      challenge_list[ challenge_count ++ ] = world_static.focused_entity;
+
+      u32 next = active_challenge->first;
+      while( mdl_entity_id_type(next) == k_ent_objective ){
+         u32 index = mdl_entity_id_id( next );
+         objective_list[ objective_count ++ ] = index;
+
+         ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+         next = objective->id_next;
+      }
+
+      radius = 10000.0f;
+   }
+   else {
+      shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
+      bh_iter it;
+      bh_iter_init_range( 0, &it, pos, radius+10.0f );
+      i32 idx;
+      while( bh_next( world->entity_bh, &it, &idx ) ){
+         u32 id    = world->entity_list[ idx ],
+             type  = mdl_entity_id_type( id ),
+             index = mdl_entity_id_id( id );
+
+         if( type == k_ent_objective ) {
+            if( objective_count < VG_ARRAY_LEN(objective_list) )
+               objective_list[ objective_count ++ ] = index;
+         }
+         else if( type == k_ent_challenge ){
+            if( challenge_count < VG_ARRAY_LEN(challenge_list) )
+               challenge_list[ challenge_count ++ ] = index;
+         }
+      }
+   }
+
+   /* render objectives */
+   glDisable( GL_CULL_FACE );
+   mesh_bind( &world->mesh_no_collide );
+   u32 last_material = 0;
+   for( u32 i=0; i<objective_count; i++ )
+   {
+      u32 index = objective_list[ i ];
+      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
+      if( (objective->flags & k_ent_objective_hidden) &&
+          !active_challenge ) continue;
+
+      f32 scale = 1.0f;
+
+      if( running )
+      {
+         u32 passed = objective->flags & k_ent_objective_passed;
+         f32 target = passed? 0.0f: 1.0f;
+         vg_slewf(&objective->transform.s[0], target, vg.time_frame_delta*4.0f);
+         scale = vg_smoothstepf( objective->transform.s[0] );
+
+         if( (objective == world_static.challenge_target) || passed )
+            shader_scene_fxglow_uUvOffset( (v2f){ 16.0f/256.0f, 0.0f } );
+         else
+            shader_scene_fxglow_uUvOffset( (v2f){ 8.0f/256.0f, 0.0f } );
+      }
+      else 
+      {
+         f32 dist = v3_dist( objective->transform.co, pos ) * (1.0f/radius);
+         scale = vg_smoothstepf( vg_clampf( 5.0f-dist*5.0f, 0.0f,1.0f ) );
+      }
+
+      m4x3f mmdl;
+      q_m3x3( objective->transform.q, mmdl );
+      m3x3_scalef( mmdl, scale );
+      v3_copy( objective->transform.co, mmdl[3] );
+      shader_scene_fxglow_uMdl( mmdl );
+
+      for( u32 j=0; j<objective->submesh_count; j++ )
+      {
+         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                       objective->submesh_start + j );
+
+         if( sm->material_id != last_material )
+         {
+            last_material = sm->material_id;
+            pass->fn_bind( world, &world->surfaces[sm->material_id] );
+         }
+         mdl_draw_submesh( sm );
+      }
+   }
+
+   /* render texts */
+   font3d_bind( &gui.font, k_font_shader_world, 0, world, &g_render.cam );
+
+   u32 count = 0;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ )
+   {
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
+      if( challenge->status ) count ++;
+   }
+
+   char buf[32];
+   vg_str str;
+   vg_strnull( &str, buf, sizeof(buf) );
+   vg_strcati32( &str, count );
+   vg_strcatch( &str, '/' );
+   vg_strcati32( &str, mdl_arrcount(&world->ent_challenge) );
+
+   f32 w = font3d_string_width( 1, buf );
+   m4x3f mlocal;
+   m3x3_identity( mlocal );
+   mlocal[3][0] = -w*0.5f;
+   mlocal[3][1] = 0.0f;
+   mlocal[3][2] = 0.0f;
+
+   for( u32 i=0; i<challenge_count; i++ )
+   {
+      u32 index = challenge_list[ i ];
+      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
+      m4x3f mmdl;
+      mdl_transform_m4x3( &challenge->transform, mmdl );
+      m4x3_mul( mmdl, mlocal, mmdl );
+
+      vg_line_point( challenge->transform.co, 0.25f, VG__RED );
+
+      f32 dist = v3_dist( challenge->transform.co, pos ) * (1.0f/radius),
+          scale = vg_smoothstepf( vg_clampf( 10.0f-dist*10.0f, 0.0f,1.0f ) ),
+          colour = 0.0f;
+
+      if( challenge->status )
+         colour = 1.0f;
+
+      shader_scene_font_uOpacity( scale );
+      shader_scene_font_uColourize( colour );
+      font3d_simple_draw( 1, buf, &g_render.cam, mmdl );
+   }
+}
+
+static void bindpoint_fxglow( world_instance *world,
+                              struct world_surface *mat )
+{
+   struct shader_props_standard *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
+}
+
+static void render_world_fxglow( world_instance *host_world, 
+                                 world_instance *world, vg_camera *cam,
+                                 m4x3f world_mmdl,
+                                 int generic, int challenges, int regions )
+{
+   shader_scene_fxglow_use();
+   shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
+   shader_scene_fxglow_uTexMain(1);
+   shader_scene_fxglow_uPv( cam->mtx.pv );
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_fxglow );
+
+   shader_scene_fxglow_uCamera( cam->transform[3] );
+   glDisable(GL_CULL_FACE);
+
+   struct world_pass pass = 
+   {
+      .shader = k_shader_fxglow,
+      .cam = cam,
+      .fn_bind = bindpoint_fxglow,
+      .fn_set_mdl = shader_scene_fxglow_uMdl,
+      .fn_set_uPvmPrev = shader_scene_fxglow_uPvmPrev,
+   };
+
+   if( generic )
+      world_render_both_stages( world, &pass );
+
+   if( regions ){
+      mesh_bind( &world->mesh_no_collide );
+
+      u32 last_material = 0;
+      for( u32 i=0; i<mdl_arrcount(&world->ent_region); i ++ ){
+         shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
+         ent_region *region = mdl_arritm( &world->ent_region, i );
+
+         f32 offset = 0.0f;
+         if( region->flags & k_ent_route_flag_achieve_gold )
+            offset = 2.0f;
+         else if( region->flags & k_ent_route_flag_achieve_silver )
+            offset = 1.0f;
+
+         shader_scene_fxglow_uUvOffset( (v2f){ (8.0f/256.0f)*offset, 0.0f } );
+
+         m4x3f mmdl;
+         mdl_transform_m4x3( &region->transform, mmdl );
+         m4x3_mul( world_mmdl, mmdl, mmdl );
+         shader_scene_fxglow_uMdl( mmdl );
+
+         for( u32 j=0; j<region->submesh_count; j++ )
+         {
+            mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
+                                          region->submesh_start + j );
+
+            if( sm->material_id != last_material )
+            {
+               last_material = sm->material_id;
+               pass.fn_bind( world, &world->surfaces[sm->material_id] );
+            }
+            mdl_draw_submesh( sm );
+         }
+      }
+   }
+
+   if( challenges )
+      world_render_challenges( world, &pass, cam->pos );
+
+   glEnable(GL_CULL_FACE);
+}
+
+static void bindpoint_terrain( world_instance *world,
+                               struct world_surface *mat )
+{
+   struct shader_props_terrain *props = mat->info.props.compiled;
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
+   shader_scene_terrain_uBlendOffset( props->blend_offset );
+   shader_scene_terrain_uSandColour( props->sand_colour );
+}
+
+static void bindpoint_override( world_instance *world,
+                                   struct world_surface *mat )
+{
+   if( mat->info.flags & k_material_flag_collision )
+   {
+      shader_scene_override_uAlphatest(0);
+   }
+   else
+   {
+      glActiveTexture( GL_TEXTURE1 );
+      glBindTexture( GL_TEXTURE_2D, world_get_texture(world, mat->alpha_tex) );
+      shader_scene_override_uAlphatest(1);
+   }
+}
+
+static void render_terrain( world_instance *world, vg_camera *cam )
+{
+   shader_scene_terrain_use();
+   shader_scene_terrain_uTexGarbage(0);
+   shader_scene_terrain_uTexGradients(1);
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_terrain );
+   glActiveTexture( GL_TEXTURE0 );
+   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
+
+   shader_scene_terrain_uPv( cam->mtx.pv );
+   shader_scene_terrain_uCamera( cam->transform[3] );
+
+   struct world_pass pass = 
+   {
+      .shader = k_shader_terrain_blend,
+      .cam = cam,
+      .fn_bind = bindpoint_terrain,
+      .fn_set_mdl = shader_scene_terrain_uMdl,
+      .fn_set_uPvmPrev = shader_scene_terrain_uPvmPrev,
+   };
+
+   world_render_both_stages( world, &pass );
+}
+
+static void render_sky( world_instance *world, vg_camera *cam )
+{
+   /* 
+    * Modify matrix to remove clipping and view translation
+    */
+   m4x4f v,
+         v_prev,
+         pv,
+         pv_prev;
+
+   m4x4_copy( cam->mtx.v, v );
+   m4x4_copy( cam->mtx_prev.v, v_prev );
+
+   for( int i=0; i<3; i++ ){
+      v3_normalize(v[i]);
+      v3_normalize(v_prev[i]);
+   }
+   v3_zero( v[3] );
+   v3_zero( v_prev[3] );
+
+   m4x4_copy( cam->mtx.p,      pv );
+   m4x4_copy( cam->mtx_prev.p, pv_prev );
+   m4x4_reset_clipping( pv,      100.0f, 0.1f );
+   m4x4_reset_clipping( pv_prev, 100.0f, 0.1f );
+
+   m4x4_mul( pv,      v,      pv );
+   m4x4_mul( pv_prev, v_prev, pv_prev );
+
+   m4x3f identity_matrix;
+   m4x3_identity( identity_matrix );
+   
+   /*
+    * Draw
+    */
+   if( world->skybox == k_skybox_default ){
+      shader_model_sky_use();
+      shader_model_sky_uMdl( identity_matrix );
+      shader_model_sky_uPv( pv );
+      shader_model_sky_uPvmPrev( pv_prev );
+      shader_model_sky_uTexGarbage(0);
+      world_link_lighting_ub( world, _shader_model_sky.id );
+
+      glActiveTexture( GL_TEXTURE0 );
+      glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
+   }
+   else if( world->skybox == k_skybox_space ){
+      shader_model_sky_space_use();
+
+      shader_model_sky_space_uMdl( identity_matrix );
+      shader_model_sky_space_uPv( pv );
+      shader_model_sky_space_uPvmPrev( pv_prev );
+      shader_model_sky_space_uTexGarbage(0);
+      world_link_lighting_ub( world, _shader_model_sky_space.id );
+
+      glActiveTexture( GL_TEXTURE0 );
+      glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
+   }
+   else {
+      vg_fatal_error( "Programming error\n" );
+   }
+
+   glDepthMask( GL_FALSE );
+   glDisable( GL_DEPTH_TEST );
+
+   mesh_bind( &world_render.skydome );
+   mesh_draw( &world_render.skydome );
+   
+   glEnable( GL_DEPTH_TEST );
+   glDepthMask( GL_TRUE );
+}
+
+void render_world_gates( world_instance *world, vg_camera *cam )
+{
+   float closest = INFINITY;
+   struct ent_gate *gate = NULL;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
+      ent_gate *gi = mdl_arritm( &world->ent_gate, i );
+
+      if( !(gi->flags & k_ent_gate_nonlocal) )
+         if( !(gi->flags & k_ent_gate_linked) )
+            continue;
+
+      float dist = v3_dist2( gi->co[0], cam->transform[3] );
+
+      vg_line_point( gi->co[0], 0.25f, VG__BLUE );
+
+      if( dist < closest ){
+         closest = dist;
+         gate = gi;
+      }
+   }
+   
+   world->rendering_gate = gate;
+
+   if( gate ){
+      if( gate->flags & k_ent_gate_locked ){
+         world->rendering_gate = NULL;
+         return;
+      }
+
+      if( gate->flags & k_ent_gate_nonlocal ){
+         if( !(gate->flags & k_ent_gate_linked) ||
+             (world_static.load_state != k_world_loader_none) ){
+            world->rendering_gate = NULL;
+            render_gate_unlinked( world, gate, cam );
+            return;
+         }
+
+         world_instance *dest_world = &world_static.instances[ gate->target ];
+         render_gate( world, dest_world, gate, cam );
+      }
+      else
+         render_gate( world, world, gate, cam );
+   }
+}
+
+void world_prerender( world_instance *world )
+{
+   if( mdl_arrcount( &world->ent_light ) ){
+      f32 rate = vg_maxf(0.1f, fabsf(k_day_length)) * vg_signf(k_day_length);
+      world->time += vg.time_frame_delta * (1.0/(rate*60.0));
+   }
+   else{
+      world->time = 0.834;
+   }
+
+   if( world->info.flags & 0x1 ){
+      world->time = world->info.timezone;
+   }
+
+   struct ub_world_lighting *state = &world->ub_lighting;
+
+   state->g_time = world->time;
+   state->g_realtime = vg.time_real;
+   state->g_debug_indices = k_debug_light_indices;
+   state->g_light_preview = k_light_preview;
+   state->g_debug_complexity = k_debug_light_complexity;
+   state->g_time_of_day = vg_fractf( world->time );
+
+   if( vg.quality_profile == k_quality_profile_high )
+      state->g_shadow_samples = 8;
+   else if( vg.quality_profile == k_quality_profile_low )
+      state->g_shadow_samples = 2;
+   else
+      state->g_shadow_samples = 0;
+
+   state->g_day_phase   = cosf( state->g_time_of_day * VG_PIf * 2.0f );
+   state->g_sunset_phase= cosf( state->g_time_of_day * VG_PIf * 4.0f + VG_PIf );
+
+   state->g_day_phase    =       state->g_day_phase    * 0.5f + 0.5f;
+   state->g_sunset_phase = powf( state->g_sunset_phase * 0.5f + 0.5f, 6.0f );
+
+   float a = state->g_time_of_day * VG_PIf * 2.0f;
+   state->g_sun_dir[0] = sinf( a );
+   state->g_sun_dir[1] = cosf( a );
+   state->g_sun_dir[2] = 0.2f;
+   v3_normalize( state->g_sun_dir );
+
+   world->probabilities[ k_probability_curve_constant ] = 1.0f;
+   float dp = state->g_day_phase;
+
+   world->probabilities[ k_probability_curve_wildlife_day ] =
+      (dp*dp*0.8f+state->g_sunset_phase)*0.8f;
+   world->probabilities[ k_probability_curve_wildlife_night ] = 
+      1.0f-powf(fabsf((state->g_time_of_day-0.5f)*5.0f),5.0f);
+      
+   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
+   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
+                    sizeof(struct ub_world_lighting), &world->ub_lighting );
+}
+
+static void render_other_entities( world_instance *world, vg_camera *cam )
+{
+   f32 radius = 40.0f;
+   bh_iter it;
+   bh_iter_init_range( 0, &it, cam->pos, radius+10.0f );
+
+   u32 glider_list[4],
+       glider_count = 0,
+       npc_list[4],
+       npc_count = 0;
+
+   i32 idx;
+   while( bh_next( world->entity_bh, &it, &idx ) ){
+      u32 id    = world->entity_list[ idx ],
+          type  = mdl_entity_id_type( id ),
+          index = mdl_entity_id_id( id );
+
+      if( type == k_ent_glider ) 
+      {
+         if( glider_count < VG_ARRAY_LEN(glider_list) )
+            glider_list[ glider_count ++ ] = index;
+      }
+      else if( type == k_ent_npc ) 
+      {
+         if( npc_count < VG_ARRAY_LEN(npc_list) )
+            npc_list[ npc_count ++ ] = index;
+      }
+   }
+
+   shader_model_entity_use();
+   shader_model_entity_uTexMain( 0 );
+   shader_model_entity_uCamera( cam->transform[3] );
+   shader_model_entity_uPv( cam->mtx.pv );
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
+
+   for( u32 j=0; j<glider_count; j ++ )
+   {
+      ent_glider *glider = mdl_arritm( &world->ent_glider, glider_list[j] );
+
+      if( !(glider->flags & 0x1) )
+         continue;
+
+      m4x3f mdl;
+      mdl_transform_m4x3( &glider->transform, mdl );
+
+      f32 dist  = v3_dist( glider->transform.co, cam->pos ) * (1.0f/radius),
+          scale = vg_smoothstepf( vg_clampf( 5.0f-dist*5.0f, 0.0f,1.0f ) );
+      m3x3_scalef( mdl, scale );
+
+      render_glider_model( cam, world, mdl, k_board_shader_entity );
+   }
+
+   for( u32 j=0; j<npc_count; j ++ )
+   {
+      u32 index = npc_list[j];
+      ent_npc *npc = mdl_arritm( &world->ent_npc, npc_list[j] );
+      npc_update( npc );
+      npc_render( npc, world, cam );
+   }
+}
+
+void render_world( world_instance *world, vg_camera *cam,
+                   int stenciled, int viewing_from_gate, 
+                   int with_water, int with_cubemaps )
+{
+   if( stenciled ){
+      glClear( GL_DEPTH_BUFFER_BIT );
+      glStencilFunc( GL_EQUAL, 1, 0xFF );
+      glStencilMask( 0x00 ); 
+      glEnable( GL_CULL_FACE );
+      glEnable( GL_STENCIL_TEST );
+   }
+   else {
+      glStencilMask( 0xFF );
+      glStencilFunc( GL_ALWAYS, 1, 0xFF );
+      glDisable( GL_STENCIL_TEST );
+   }
+
+   render_sky( world, cam );
+
+   m4x3f identity;
+   m4x3_identity(identity);
+   render_world_routes( world, world, identity, cam, viewing_from_gate, 0 );
+   render_world_standard( world, cam );
+   render_world_cubemapped( world, cam, with_cubemaps );
+
+   render_world_vb( world, cam );
+   render_world_alphatest( world, cam );
+   render_world_foliage( world, cam );
+   render_terrain( world, cam );
+
+   if( !viewing_from_gate ){
+      world_entity_focus_render();
+
+      /* Render SFD's */
+      u32 closest = 0;
+      float min_dist = INFINITY;
+
+      if( mdl_arrcount( &world->ent_route ) ){
+         for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
+            ent_route *route = mdl_arritm( &world->ent_route, i );
+            float dist = v3_dist2( route->board_transform[3], cam->pos );
+
+            if( dist < min_dist ){
+               min_dist = dist;
+               closest = i;
+            }
+         }
+
+         ent_route *route = mdl_arritm( &world->ent_route, closest );
+         sfd_render( world, cam, route->board_transform );
+      }
+   }
+
+   if( !viewing_from_gate ){
+      f32 greyout = 0.0f;
+      if( mdl_entity_id_type(world_static.focused_entity) == k_ent_challenge )
+         greyout = world_static.focus_strength;
+
+      if( greyout > 0.0f ){
+         glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
+         glEnable(GL_BLEND);
+         glDisable(GL_DEPTH_TEST);
+         glDepthMask(GL_FALSE);
+         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+         glBlendEquation(GL_FUNC_ADD);
+
+         shader_blitcolour_use();
+         shader_blitcolour_uColour( (v4f){ 0.5f, 0.5f, 0.5f, greyout*0.56f } );
+         render_fsquad();
+         
+         glDisable(GL_BLEND);
+         glEnable(GL_DEPTH_TEST);
+         glDepthMask(GL_TRUE);
+         glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, 
+                                       GL_COLOR_ATTACHMENT1 } );
+      }
+
+      render_world_fxglow( world, world, cam, NULL, 1, 1, 0 );
+   }
+   
+   if( with_water )
+   {
+      render_water_texture( world, cam );
+      vg_framebuffer_bind( g_render.fb_main, k_render_scale );
+   }
+
+   if( stenciled )
+   {
+      glStencilFunc( GL_EQUAL, 1, 0xFF );
+      glStencilMask( 0x00 ); 
+      glEnable( GL_CULL_FACE );
+      glEnable( GL_STENCIL_TEST );
+   }
+
+   if( with_water )
+   {
+      render_water_surface( world, cam );
+   }
+
+   render_remote_players( world, cam );
+   render_other_entities( world, cam );
+   ent_miniworld_render( world, cam );
+
+   if( stenciled )
+   {
+      glStencilMask( 0xFF );
+      glStencilFunc( GL_ALWAYS, 1, 0xFF );
+      glDisable( GL_STENCIL_TEST );
+   }
+}
+
+
+static void render_world_override_pass( world_instance *world,
+                                        struct world_pass *pass,
+                                        m4x3f mmdl, m3x3f mnormal,
+                                        m4x4f mpvm_prev )
+{
+   for( int i=0; i<world->surface_count; i++ )
+   {
+      struct world_surface *mat = &world->surfaces[i];
+      if( mat->info.flags & k_material_flag_ghosts ) continue;
+
+      mdl_submesh *sm;
+      if( pass->geo_type == k_world_geo_type_solid )
+         sm = &mat->sm_geo;
+      else
+         sm = &mat->sm_no_collide;
+
+      if( !sm->indice_count )
+         continue;
+
+      pass->fn_set_mdl( mmdl );
+      pass->fn_set_uNormalMtx( mnormal );
+      pass->fn_set_uPvmPrev( mpvm_prev );
+      pass->fn_bind( world, mat );
+      mdl_draw_submesh( sm );
+   }
+}
+
+void render_world_override( world_instance *world,
+                            world_instance *lighting_source,
+                            m4x3f mmdl,
+                            vg_camera *cam,
+                            ent_spawn *dest_spawn, v4f map_info )
+{
+   struct world_pass pass = 
+   {
+      .cam = cam,
+      .fn_bind = bindpoint_override,
+      .fn_set_mdl = shader_scene_override_uMdl,
+      .fn_set_uPvmPrev = shader_scene_override_uPvmPrev,
+      .fn_set_uNormalMtx = shader_scene_override_uNormalMtx,
+      .shader = k_shader_override
+   };
+
+   shader_scene_override_use();
+   shader_scene_override_uTexGarbage(0);
+   shader_scene_override_uTexMain(1);
+   shader_scene_override_uPv( pass.cam->mtx.pv );
+   shader_scene_override_uMapInfo( map_info );
+
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( lighting_source, scene_override );
+   bind_terrain_noise();
+
+   shader_scene_override_uCamera( pass.cam->transform[3] );
+
+   m4x4f mpvm_prev;
+   m4x3_expand( mmdl, mpvm_prev );
+   m4x4_mul( cam->mtx_prev.pv, mpvm_prev, mpvm_prev );
+
+   m3x3f mnormal;
+   m3x3_inv( mmdl, mnormal );
+   m3x3_transpose( mnormal, mnormal );
+   v3_normalize( mnormal[0] );
+   v3_normalize( mnormal[1] );
+   v3_normalize( mnormal[2] );
+
+   v4f uPlayerPos, uSpawnPos;
+   v4_zero( uPlayerPos );
+   v4_zero( uSpawnPos );
+   v3_copy( world->player_co, uPlayerPos );
+   
+   if( dest_spawn && (v3_dist2(dest_spawn->transform.co,uPlayerPos) > 0.1f) )
+      v3_copy( dest_spawn->transform.co, uSpawnPos );
+   else
+      v3_add( uPlayerPos, (v3f){0,-1,0}, uSpawnPos );
+
+   uPlayerPos[3] = v3_dist(uPlayerPos,uSpawnPos);
+   uSpawnPos[3] = 1.0f/uPlayerPos[3];
+
+   shader_scene_override_uPlayerPos( uPlayerPos );
+   shader_scene_override_uSpawnPos( uSpawnPos );
+
+
+   glDisable( GL_CULL_FACE );
+   mesh_bind( &world->mesh_geo );
+   pass.geo_type = k_world_geo_type_solid;
+   render_world_override_pass( world, &pass, mmdl, mnormal, mpvm_prev );
+   mesh_bind( &world->mesh_no_collide );
+   pass.geo_type = k_world_geo_type_nonsolid;
+   render_world_override_pass( world, &pass, mmdl, mnormal, mpvm_prev );
+   glEnable( GL_CULL_FACE );
+
+   render_world_fxglow( world, world, cam, mmdl, 0, 0, 1 );
+}
+
+static void render_cubemap_side( world_instance *world, ent_cubemap *cm, 
+                                    u32 side ){
+   vg_camera cam;
+   glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+         GL_TEXTURE_CUBE_MAP_POSITIVE_X + side, cm->texture_id, 0 );
+   glClear( GL_DEPTH_BUFFER_BIT );
+
+   v3f forward[6] = {
+      { -1.0f,  0.0f,  0.0f },
+      {  1.0f,  0.0f,  0.0f },
+      {  0.0f, -1.0f,  0.0f },
+      {  0.0f,  1.0f,  0.0f },
+      {  0.0f,  0.0f, -1.0f },
+      {  0.0f,  0.0f,  1.0f }
+   };
+   v3f up[6] = {
+      { 0.0f, -1.0f,  0.0f },
+      { 0.0f, -1.0f,  0.0f },
+      { 0.0f,  0.0f,  1.0f },
+      { 0.0f,  0.0f, -1.0f },
+      { 0.0f, -1.0f,  0.0f },
+      { 0.0f, -1.0f,  0.0f }
+   };
+
+   v3_zero( cam.angles );
+   v3_copy( cm->co, cam.pos );
+
+   v3_copy( forward[side], cam.transform[2] );
+   v3_copy( up[side], cam.transform[1] );
+   v3_cross( up[side], forward[side], cam.transform[0] );
+   v3_copy( cm->co, cam.transform[3] );
+   m4x3_invert_affine( cam.transform, cam.transform_inverse );
+
+   vg_camera_update_view( &cam );
+
+   cam.nearz = 0.1f;
+   cam.farz = 1000.0f;
+   cam.fov = 90.0f;
+   m4x4_copy( cam.mtx.p,  cam.mtx_prev.p );
+   m4x4_projection( cam.mtx.p, cam.fov, 1.0f, cam.nearz, cam.farz );
+   vg_camera_finalize( &cam );
+   vg_camera_finalize( &cam );
+
+   render_world( world, &cam, 0, 1, 1, 0 );
+}
+
+void render_world_cubemaps( world_instance *world )
+{
+   if( world->cubemap_cooldown )
+      world->cubemap_cooldown --;
+   else{
+      world->cubemap_cooldown = 60;
+
+      glViewport( 0, 0, WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES );
+      for( u32 i=0; i<mdl_arrcount( &world->ent_cubemap ); i++ ){
+         ent_cubemap *cm = mdl_arritm( &world->ent_cubemap, i );
+         glBindFramebuffer( GL_FRAMEBUFFER, cm->framebuffer_id );
+
+         world->cubemap_side ++;
+         if( world->cubemap_side >= 6 )
+            world->cubemap_side = 0;
+
+         render_cubemap_side( world, cm, world->cubemap_side );
+      }
+   }
+}
+
+/*
+ * Geo shaders
+ * ---------------------------------------------
+ */
+
+void render_world_depth( world_instance *world, vg_camera *cam )
+{
+   m4x3f identity_matrix;
+   m4x3_identity( identity_matrix );
+
+   shader_scene_depth_use();
+   shader_scene_depth_uCamera( cam->transform[3] );
+   shader_scene_depth_uPv( cam->mtx.pv );
+   shader_scene_depth_uPvmPrev( cam->mtx_prev.pv );
+   shader_scene_depth_uMdl( identity_matrix );
+   world_link_lighting_ub( world, _shader_scene_depth.id );
+
+   mesh_bind( &world->mesh_geo );
+   mesh_draw( &world->mesh_geo );
+}
+
+void render_world_position( world_instance *world, vg_camera *cam )
+{
+   m4x3f identity_matrix;
+   m4x3_identity( identity_matrix );
+
+   shader_scene_position_use();
+   shader_scene_position_uCamera( cam->transform[3] );
+   shader_scene_position_uPv( cam->mtx.pv );
+   shader_scene_position_uPvmPrev( cam->mtx_prev.pv );
+   shader_scene_position_uMdl( identity_matrix );
+   world_link_lighting_ub( world, _shader_scene_position.id );
+
+   mesh_bind( &world->mesh_geo );
+   mesh_draw( &world->mesh_geo );
+}
+
+struct ui_enum_opt skybox_setting_options[] = { 
+   { 0, "g_daysky_colour" },
+   { 1, "g_nightsky_colour" },
+   { 2, "g_sunset_colour" },
+   { 3, "g_ambient_colour" },
+   { 4, "g_sun_colour" },
+};
+
+static f32 *skybox_prop_location( world_instance *world, i32 index ){
+   switch( index ){
+      case 0: return world->ub_lighting.g_daysky_colour; break;
+      case 1: return world->ub_lighting.g_nightsky_colour; break;
+      case 2: return world->ub_lighting.g_sunset_colour; break;
+      case 3: return world->ub_lighting.g_ambient_colour; break;
+      case 4: return world->ub_lighting.g_sun_colour; break;
+      default: return NULL;
+   }
+}
+
+void imgui_world_light_edit( ui_context *ctx, world_instance *world )
+{
+   ui_rect panel = { vg.window_x-400, 0, 400, vg.window_y };
+   ui_fill( ctx, panel, ui_colour( ctx, k_ui_bg+1 ) );
+   ui_outline( ctx, panel, 1, ui_colour( ctx, k_ui_bg+7 ), 0 );
+   ui_rect_pad( panel, (ui_px[2]){ 8, 8 } );
+   ui_capture_mouse(ctx, 1);
+
+   static i32 option_to_edit = 0;
+   ui_enum( ctx, panel, "option", skybox_setting_options, 5, &option_to_edit );
+   ui_colourpicker( ctx, panel, "colour", 
+                    skybox_prop_location( world, option_to_edit ) );
+
+   if( ui_button( ctx, panel, "save tweaker file ('/tmp/tweaker.txt')\n" ) == 1 )
+   {
+      FILE *fp = fopen( "/tmp/tweaker.txt", "w" );
+      
+      for( i32 i=0; i<5; i ++ ){
+         struct ui_enum_opt *opt = &skybox_setting_options[i];
+         f32 *val = skybox_prop_location( world, i );
+         fprintf( fp, "%s = {%.3ff, %.3ff, %.3ff, %.3ff},\n",
+                  opt->alias, val[0], val[1], val[2], val[3] );
+      }
+      fclose( fp );
+   }
+}
diff --git a/src/world_render.h b/src/world_render.h
new file mode 100644 (file)
index 0000000..1eb5ab1
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+
+#define WORLD_CUBEMAP_RES 32
+
+#include "vg/vg_camera.h"
+#include "world.h"
+#include "shaders/scene_standard.h"
+#include "shaders/scene_standard_alphatest.h"
+#include "shaders/scene_foliage.h"
+#include "shaders/scene_override.h"
+#include "shaders/scene_cubemapped.h"
+#include "shaders/scene_vertex_blend.h"
+#include "shaders/scene_terrain.h"
+#include "shaders/scene_fxglow.h"
+#include "shaders/scene_depth.h"
+#include "shaders/scene_position.h"
+#include "shaders/scene_font.h"
+#include "shaders/model_sky.h"
+#include "shaders/model_sky_space.h"
+
+static const float k_world_light_cube_size = 8.0f;
+
+struct world_render
+{
+   GLuint tex_terrain_noise;
+
+   /* rendering */
+   glmesh skydome;
+
+   double sky_time, sky_rate, sky_target_rate;
+
+   v3f render_gate_pos;
+   struct timer_text{
+      char text[8];
+      m4x3f transform;
+      ent_gate *gate;
+      ent_route *route;
+   }
+   timer_texts[4];
+   u32 timer_text_count;
+
+   struct text_particle{
+      rigidbody rb;
+      m4x3f mlocal;
+      ent_glyph *glyph;
+      v4f colour;
+      m4x3f mdl;
+      f32 radius;
+   }
+   text_particles[6*4];
+   u32 text_particle_count;
+}
+extern world_render;
+
+void world_render_init(void);
+
+void world_prerender( world_instance *world );
+void world_link_lighting_ub( world_instance *world, GLuint shader );
+void world_bind_position_texture( world_instance *world, 
+                                  GLuint shader, GLuint location,
+                                  int slot );
+void world_bind_light_array( world_instance *world,
+                             GLuint shader, GLuint location, 
+                             int slot );
+void world_bind_light_index( world_instance *world,
+                             GLuint shader, GLuint location,
+                             int slot );
+void render_world_position( world_instance *world, vg_camera *cam );
+void render_world_depth( world_instance *world, vg_camera *cam );
+void render_world( world_instance *world, vg_camera *cam,
+                   int stenciled, int viewing_from_gate, 
+                   int with_water, int with_cubemaps );
+void render_world_cubemaps( world_instance *world );
+void bind_terrain_noise(void);
+void render_world_override( world_instance *world,
+                            world_instance *lighting_source,
+                            m4x3f mmdl,
+                            vg_camera *cam,
+                            ent_spawn *dest_spawn, v4f map_info );
+void render_world_gates( world_instance *world, vg_camera *cam );
+void imgui_world_light_edit( ui_context *ctx, world_instance *world );
+
+#define WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( WORLD, SHADER )            \
+   world_link_lighting_ub( WORLD, _shader_##SHADER.id );                \
+   world_bind_position_texture( WORLD, _shader_##SHADER.id,             \
+                                _uniform_##SHADER##_g_world_depth, 2 ); \
+   world_bind_light_array( WORLD, _shader_##SHADER.id,                  \
+                           _uniform_##SHADER##_uLightsArray, 3 );       \
+   world_bind_light_index( WORLD, _shader_##SHADER.id,                  \
+                           _uniform_##SHADER##_uLightsIndex, 4 );
+
diff --git a/src/world_routes.c b/src/world_routes.c
new file mode 100644 (file)
index 0000000..e4fd80e
--- /dev/null
@@ -0,0 +1,1089 @@
+#pragma once
+
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
+ *
+ * World routes
+ */
+
+#include <time.h>
+#include "entity.h"
+#include "world_routes.h"
+#include "world_gate.h"
+#include "world_load.h"
+#include "network.h"
+
+#include "font.h"
+#include "gui.h"
+#include "steam.h"
+#include "network_msg.h"
+#include "network_common.h"
+
+#include "shaders/scene_route.h"
+#include "shaders/routeui.h"
+#include "ent_region.h"
+#include "scene_rigidbody.h"
+
+void world_routes_clear( world_instance *world )
+{
+   for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+      route->active_checkpoint = 0xffff;
+   }
+
+   for( u32 i=0; i<mdl_arrcount( &world->ent_gate ); i++ ){
+      ent_gate *rg = mdl_arritm( &world->ent_gate, i );
+      rg->timing_version = 0;
+      rg->timing_time = 0.0;
+   }
+
+   world_static.current_run_version += 4;
+   world_static.last_use = 0.0;
+}
+
+static void world_routes_time_lap( world_instance *world, ent_route *route ){
+   vg_info( "------- time lap %s -------\n", 
+            mdl_pstr(&world->meta,route->pstr_name) );
+
+   double start_time = 0.0;
+   u32 last_version=0;
+   f64 last_time = 0.0;
+   ent_checkpoint *last_cp = NULL;
+
+   u32 valid_sections=0;
+   int clean = !localplayer.rewinded_since_last_gate;
+
+   for( u32 i=0; i<route->checkpoints_count; i++ ){
+      u32 cpid  = (i+route->active_checkpoint) % route->checkpoints_count;
+          cpid += route->checkpoints_start;
+
+      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, cpid );
+      ent_gate *rg = mdl_arritm( &world->ent_gate, cp->gate_index );
+                rg = mdl_arritm( &world->ent_gate, rg->target );
+
+      if( i == 1 ){
+         route->timing_base = rg->timing_time;
+      }
+
+      if( i == 0 )
+         start_time = rg->timing_time;
+      else{
+         if( last_version+1 == rg->timing_version ) valid_sections ++;
+         else valid_sections = 0;
+      }
+
+      vg_info( "%u %f [%s]\n", rg->timing_version, rg->timing_time,
+            i? ((rg->flags & k_ent_gate_clean_pass)? "CLEAN": "     "):
+                " N/A ");
+
+      if( !(rg->flags & k_ent_gate_clean_pass) )
+         clean = 0;
+
+      last_version = rg->timing_version;
+      last_time = rg->timing_time;
+      last_cp = cp;
+   }
+
+   if( world_static.current_run_version == last_version+1 ){
+      valid_sections ++;
+
+      if( route->checkpoints_count == 1 ){
+         route->timing_base = world_static.time;
+      }
+
+      f32 section = world_static.time - last_time;
+      if( (section < last_cp->best_time) || (last_cp->best_time == 0.0f) ){
+         last_cp->best_time = section;
+      }
+   }
+   else valid_sections = 0;
+
+   vg_info( "%u %f [%s]\n", 
+            world_static.current_run_version, world_static.time,
+           !localplayer.rewinded_since_last_gate? "CLEAN": "     " );
+
+   if( valid_sections==route->checkpoints_count ){
+      f64 lap_time = world_static.time - start_time;
+
+      if( (route->best_laptime == 0.0) || (lap_time < route->best_laptime) ){
+         route->best_laptime = lap_time;
+      }
+
+      route->flags |= k_ent_route_flag_achieve_silver;
+      if( clean ) route->flags |= k_ent_route_flag_achieve_gold;
+      ent_region_re_eval( world );
+
+      /* for steam achievements. */
+      if( route->anon.official_track_id != 0xffffffff ){
+         struct track_info *ti = &track_infos[ route->anon.official_track_id ];
+         if( ti->achievement_id ){
+            steam_set_achievement( ti->achievement_id );
+            steam_store_achievements();
+         }
+      }
+
+      addon_alias *alias = 
+         &world_static.instance_addons[ world_static.active_instance ]->alias;
+
+      char mod_uid[ ADDON_UID_MAX ];
+      addon_alias_uid( alias, mod_uid );
+      network_publish_laptime( mod_uid, 
+                               mdl_pstr( &world->meta, route->pstr_name ),
+                               lap_time );
+   }
+
+   route->valid_checkpoints = valid_sections+1;
+
+   vg_info( "valid sections: %u\n", valid_sections );
+   vg_info( "----------------------------\n" );
+
+   route->ui_residual = 1.0f;
+   route->ui_residual_block_w = route->ui_first_block_width;
+}
+
+/*
+ * When going through a gate this is called for bookkeeping purposes
+ */
+void world_routes_activate_entry_gate( world_instance *world, ent_gate *rg )
+{
+   world_static.last_use = world_static.time;
+   ent_gate *dest = mdl_arritm( &world->ent_gate, rg->target );
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+
+      u32 active_prev = route->active_checkpoint;
+      route->active_checkpoint = 0xffff;
+
+      for( u32 j=0; j<4; j++ ){
+         if( dest->routes[j] == i ){
+            for( u32 k=0; k<route->checkpoints_count; k++ ){
+               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, 
+                                                 route->checkpoints_start+k );
+
+               ent_gate *gk = mdl_arritm( &world->ent_gate, cp->gate_index );
+                         gk = mdl_arritm( &world->ent_gate, gk->target );
+               if( gk == dest ){
+                  route->active_checkpoint = k;
+                  world_routes_time_lap( world, route );
+                  break;
+               }
+            }
+            break;
+         }
+      }
+   }
+
+   dest->timing_version = world_static.current_run_version;
+   dest->timing_time = world_static.time;
+
+   if( localplayer.rewinded_since_last_gate ){
+      localplayer.rewinded_since_last_gate = 0;
+      dest->flags &= ~k_ent_gate_clean_pass;
+   }
+   else
+      dest->flags |= k_ent_gate_clean_pass;
+
+   world_static.current_run_version ++;
+}
+
+/* draw lines along the paths */
+static void world_routes_debug( world_instance *world )
+{
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route_node); i++ ){
+      ent_route_node *rn = mdl_arritm(&world->ent_route_node,i);
+      vg_line_point( rn->co, 0.25f, VG__WHITE );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm(&world->ent_route, i);
+
+      u32 colours[] = { 0xfff58142, 0xff42cbf5, 0xff42f56c, 0xfff542b3,
+                        0xff5442f5 };
+
+      u32 cc = 0xffcccccc;
+      if( route->active_checkpoint != 0xffff ){
+         cc = colours[i%VG_ARRAY_LEN(colours)];
+      }
+
+      for( int i=0; i<route->checkpoints_count; i++ ){
+         int i0 = route->checkpoints_start+i,
+             i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
+
+         ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
+                        *c1 = mdl_arritm(&world->ent_checkpoint, i1);
+
+         ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
+         ent_gate *end_gate = mdl_arritm( &world->ent_gate, c1->gate_index );
+
+         v3f p0, p1;
+         v3_copy( start_gate->co[1], p0 );
+
+         for( int j=0; j<c0->path_count; j ++ ){
+            ent_path_index *index = mdl_arritm( &world->ent_path_index, 
+                                                c0->path_start+j );
+
+            ent_route_node *rn = mdl_arritm( &world->ent_route_node,
+                                             index->index );
+
+            v3_copy( rn->co, p1 );
+            vg_line( p0, p1, cc );
+            v3_copy( p1, p0 );
+         }
+
+         v3_copy( end_gate->co[0], p1 );
+         vg_line( p0, p1, cc );
+      }
+   }
+}
+
+
+static 
+void world_routes_place_curve( world_instance *world, ent_route *route,
+                               v4f h[3], v3f n0, v3f n2, scene_context *scene )
+{
+   float t;
+   v3f p, pd;
+   int last_valid=0;
+
+   float total_length = 0.0f,
+         travel_length = 0.0;
+
+   v3f last;
+   v3_copy( h[0], last );
+   for( int it=0; it<128; it ++ ){
+      t = (float)(it+1) * (1.0f/128.0f);
+      eval_bezier3( h[0], h[1], h[2], t, p );
+      total_length += v3_dist( p, last );
+      v3_copy( p, last );
+   }
+
+   float patch_size = 4.0f,
+         patch_count = ceilf( total_length / patch_size );
+
+   t = 0.0f;
+   v3_copy( h[0], last );
+
+   for( int it=0; it<128; it ++ ){
+      float const k_sample_dist = 0.0025f,
+                  k_line_width = 1.5f;
+
+      eval_bezier3( h[0], h[1], h[2], t, p );
+      eval_bezier3( h[0], h[1], h[2], t+k_sample_dist, pd );
+
+      travel_length += v3_dist( p, last );
+
+      float mod = k_sample_dist / v3_dist( p, pd );
+
+      v3f v0,up, right;
+
+      v3_muls( n0, -(1.0f-t), up );
+      v3_muladds( up, n2, -t, up );
+      v3_normalize( up );
+
+      v3_sub( pd,p,v0 );
+      v3_cross( up, v0, right );
+      v3_normalize( right );
+
+      float cur_x = (1.0f-t)*h[0][3] + t*h[2][3];
+      
+      v3f sc, sa, sb, down;
+      v3_muladds( p, right, cur_x * k_line_width, sc );
+      v3_muladds( sc, up, 1.5f, sc );
+      v3_muladds( sc, right, k_line_width*0.95f, sa );
+      v3_muladds( sc, right, 0.0f, sb );
+      v3_muls( up, -1.0f, down );
+      
+      ray_hit ha, hb;
+      ha.dist = 8.0f;
+      hb.dist = 8.0f;
+
+      int resa = ray_world( world, sa, down, &ha, k_material_flag_ghosts ),
+          resb = ray_world( world, sb, down, &hb, k_material_flag_ghosts );
+
+      if( resa && resb ){
+         struct world_surface *surfa = ray_hit_surface( world, &ha ),
+                              *surfb = ray_hit_surface( world, &hb );
+
+         if( (surfa->info.flags & k_material_flag_skate_target) &&
+             (surfb->info.flags & k_material_flag_skate_target) )
+         {
+            scene_vert va, vb;
+
+            float gap = vg_fractf(cur_x*0.5f)*0.02f;
+            
+            v3_muladds( ha.pos, up, 0.06f+gap, va.co );
+            v3_muladds( hb.pos, up, 0.06f+gap, vb.co );
+
+            scene_vert_pack_norm( &va, up, 0.0f );
+            scene_vert_pack_norm( &vb, up, 0.0f );
+
+            float t1 = (travel_length / total_length) * patch_count;
+            va.uv[0] = t1;
+            va.uv[1] = 0.0f;
+            vb.uv[0] = t1;
+            vb.uv[1] = 1.0f;
+
+            scene_push_vert( scene, &va );
+            scene_push_vert( scene, &vb );
+
+            if( last_valid ){
+               /* Connect them with triangles */
+               scene_push_tri( scene, (u32[3]){ 
+                     last_valid+0-2, last_valid+1-2, last_valid+2-2} );
+               scene_push_tri( scene, (u32[3]){ 
+                     last_valid+1-2, last_valid+3-2, last_valid+2-2} );
+            }
+            
+            last_valid = scene->vertex_count;
+         }
+         else
+            last_valid = 0;
+      }
+      else
+         last_valid = 0;
+
+      if( t == 1.0f )
+         return;
+
+      t += 1.0f*mod;
+      if( t > 1.0f )
+         t = 1.0f;
+
+      v3_copy( p, last );
+   }
+}
+
+static void world_routes_gen_meshes( world_instance *world, u32 route_id, 
+                                        scene_context *sc )
+{
+   ent_route *route = mdl_arritm( &world->ent_route, route_id );
+   u8 colour[4];
+   colour[0] = route->colour[0] * 255.0f;
+   colour[1] = route->colour[1] * 255.0f;
+   colour[2] = route->colour[2] * 255.0f;
+   colour[3] = route->colour[3] * 255.0f;
+
+   u32 last_valid = 0;
+
+   for( int i=0; i<route->checkpoints_count; i++ ){
+      int i0 = route->checkpoints_start+i,
+          i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
+
+      ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
+                     *c1 = mdl_arritm(&world->ent_checkpoint, i1);
+
+      ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
+      start_gate = mdl_arritm( &world->ent_gate, start_gate->target );
+
+      ent_gate *end_gate = mdl_arritm( &world->ent_gate, c1->gate_index ),
+               *collector = mdl_arritm( &world->ent_gate, end_gate->target );
+
+      v4f p[3];
+
+      v3_add( (v3f){0.0f,0.1f,0.0f}, start_gate->co[0], p[0] );
+      p[0][3]  = start_gate->ref_count;
+      p[0][3] -= (float)start_gate->route_count * 0.5f;
+      start_gate->ref_count ++;
+
+      if( !c0->path_count )
+         continue;
+
+      /* this is so that we get nice flow through the gates */
+      v3f temp_alignments[2];
+      ent_gate *both[] = { start_gate, end_gate };
+
+      for( int j=0; j<2; j++ ){
+         int pi = c0->path_start + ((j==1)? c0->path_count-1: 0);
+
+         ent_path_index *index = mdl_arritm( &world->ent_path_index, pi );
+         ent_route_node *rn = mdl_arritm( &world->ent_route_node,
+                                          index->index );
+         v3f v0;
+         v3_sub( rn->co, both[j]->co[0], v0 );
+         float d = v3_dot( v0, both[j]->to_world[2] );
+
+         v3_muladds( both[j]->co[0], both[j]->to_world[2], d, 
+                     temp_alignments[j] );
+         v3_add( (v3f){0.0f,0.1f,0.0f}, temp_alignments[j], temp_alignments[j]);
+      }
+
+
+      for( int j=0; j<c0->path_count; j ++ ){
+         ent_path_index *index = mdl_arritm( &world->ent_path_index, 
+                                             c0->path_start+j );
+         ent_route_node *rn = mdl_arritm( &world->ent_route_node,
+                                          index->index );
+         if( j==0 || j==c0->path_count-1 )
+            if( j == 0 )
+               v3_copy( temp_alignments[0], p[1] );
+            else
+               v3_copy( temp_alignments[1], p[1] );
+         else
+            v3_copy( rn->co, p[1] );
+
+         p[1][3] = rn->ref_count;
+         p[1][3] -= (float)rn->ref_total * 0.5f;
+         rn->ref_count ++;
+
+         if( j+1 < c0->path_count ){
+            index = mdl_arritm( &world->ent_path_index, 
+                                c0->path_start+j+1 );
+            rn = mdl_arritm( &world->ent_route_node, index->index );
+
+            if( j+1 == c0->path_count-1 )
+               v3_lerp( p[1], temp_alignments[1], 0.5f, p[2] );
+            else
+               v3_lerp( p[1], rn->co, 0.5f, p[2] );
+
+            p[2][3] = rn->ref_count;
+            p[2][3] -= (float)rn->ref_total * 0.5f;
+         }
+         else{
+            v3_copy( end_gate->co[0], p[2] );
+            v3_add( (v3f){0.0f,0.1f,0.0f}, p[2], p[2] );
+            p[2][3] = collector->ref_count;
+
+            if( i == route->checkpoints_count-1)
+               p[2][3] -= 1.0f;
+
+            p[2][3] -= (float)collector->route_count * 0.5f;
+            //collector->ref_count ++;
+         }
+
+         /* p0,p1,p2 bezier patch is complete
+          * --------------------------------------*/
+         v3f surf0, surf2, n0, n2;
+
+         if( bh_closest_point( world->geo_bh, p[0], surf0, 5.0f ) == -1 )
+            v3_add( (v3f){0.0f,-0.1f,0.0f}, p[0], surf0 );
+
+         if( bh_closest_point( world->geo_bh, p[2], surf2, 5.0f ) == -1 )
+            v3_add( (v3f){0.0f,-0.1f,0.0f}, p[2], surf2 );
+
+         v3_sub( surf0, p[0], n0 );
+         v3_sub( surf2, p[2], n2 );
+         v3_normalize( n0 );
+         v3_normalize( n2 );
+
+         world_routes_place_curve( world, route, p, n0, n2, sc );
+
+         /* --- */
+         v4_copy( p[2], p[0] );
+      }
+   }
+
+   scene_copy_slice( sc, &route->sm );
+}
+
+struct world_surface *world_tri_index_surface( world_instance *world, 
+                                                 u32 index );
+
+/* 
+ * Create the strips of colour that run through the world along course paths
+ */
+void world_gen_routes_generate( u32 instance_id )
+{
+   world_instance *world = &world_static.instances[ instance_id ];
+   vg_info( "Generating route meshes\n" );
+   vg_async_stall();
+
+   vg_async_item *call_scene = scene_alloc_async( &world->scene_lines, 
+                                                  &world->mesh_route_lines,
+                                                  200000, 300000 );
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+      gate->ref_count = 0;
+      gate->route_count = 0;
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route_node); i++ ){
+      ent_route_node *rn = mdl_arritm( &world->ent_route_node, i );
+      rn->ref_count = 0;
+      rn->ref_total = 0;
+   }
+
+   for( u32 k=0; k<mdl_arrcount(&world->ent_route); k++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, k );
+
+      for( int i=0; i<route->checkpoints_count; i++ ){
+         int i0 = route->checkpoints_start+i,
+             i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
+
+         ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
+                        *c1 = mdl_arritm(&world->ent_checkpoint, i1);
+
+         ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
+         start_gate = mdl_arritm( &world->ent_gate, start_gate->target );
+         start_gate->route_count ++;
+
+         if( !c0->path_count )
+            continue;
+
+         for( int j=0; j<c0->path_count; j ++ ){
+            ent_path_index *index = mdl_arritm( &world->ent_path_index, 
+                                                c0->path_start+j );
+            ent_route_node *rn = mdl_arritm( &world->ent_route_node,
+                                             index->index );
+            rn->ref_total ++;
+         }
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      world_routes_gen_meshes( world, i, &world->scene_lines );
+   }
+
+   vg_async_dispatch( call_scene, async_scene_upload );
+   world_routes_clear( world );
+}
+
+/* load all routes from model header */
+void world_gen_routes_ent_init( world_instance *world )
+{
+   vg_info( "Initializing routes\n" );
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+      for( u32 j=0; j<4; j++ ){
+         gate->routes[j] = 0xffff;
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm(&world->ent_route,i);
+      mdl_transform_m4x3( &route->anon.transform, route->board_transform );
+
+      route->flags = 0x00;
+      route->best_laptime = 0.0;
+      route->ui_stopper = 0.0f;
+      route->ui_residual = 0.0f;
+      
+      if( mdl_arrcount(&world->ent_region) )
+         route->flags |= k_ent_route_flag_out_of_zone;
+
+      route->anon.official_track_id = 0xffffffff;
+      for( u32 j=0; j<VG_ARRAY_LEN(track_infos); j ++ ){
+         if( !strcmp(track_infos[j].name, 
+                     mdl_pstr(&world->meta,route->pstr_name))){
+            route->anon.official_track_id = j;
+         }
+      }
+
+      for( u32 j=0; j<route->checkpoints_count; j++ ){
+         u32 id = route->checkpoints_start + j;
+         ent_checkpoint *cp = mdl_arritm(&world->ent_checkpoint,id);
+         
+         ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
+
+         for( u32 k=0; k<4; k++ ){
+            if( gate->routes[k] == 0xffff ){
+               gate->routes[k] = i;
+               break;
+            }
+         }
+
+         if( (gate->flags & k_ent_gate_linked) &
+            !(gate->flags & k_ent_gate_nonlocal) ){
+            gate = mdl_arritm(&world->ent_gate, gate->target );
+
+            for( u32 k=0; k<4; k++ ){
+               if( gate->routes[k] == i ){
+                  vg_error( "already assigned route to gate\n" );
+                  break;
+               }
+               if( gate->routes[k] == 0xffff ){
+                  gate->routes[k] = i;
+                  break;
+               }
+            }
+         }
+      }
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
+      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
+   }
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_checkpoint); i++ ){
+      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, i );
+      cp->best_time = 0.0;
+   }
+
+   world_routes_clear( world );
+}
+
+void world_routes_recv_scoreboard( world_instance *world, 
+                                   vg_msg *body, u32 route_id,
+                                   enum request_status status )
+{
+   if( route_id >= mdl_arrcount( &world->ent_route ) ){
+      vg_error( "Scoreboard route_id out of range (%u)\n", route_id );
+      return;
+   }
+
+   struct leaderboard_cache *board = &world->leaderboard_cache[ route_id ];
+   board->status = status;
+
+   if( body == NULL ){
+      board->data_len = 0;
+      return;
+   }
+
+   if( body->max > NETWORK_REQUEST_MAX ){
+      vg_error( "Scoreboard leaderboard too big (%u>%u)\n", body->max,
+                NETWORK_REQUEST_MAX );
+      return;
+   }
+
+   memcpy( board->data, body->buf, body->max );
+   board->data_len = body->max;
+}
+
+/* 
+ * -----------------------------------------------------------------------------
+ *                                    Events
+ * -----------------------------------------------------------------------------
+ */
+
+void world_routes_init(void)
+{
+   world_static.current_run_version = 200;
+   world_static.time = 300.0;
+   world_static.last_use = 0.0;
+}
+
+void world_routes_update( world_instance *world )
+{
+   world_static.time += vg.time_delta;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+      
+      int target = route->active_checkpoint == 0xffff? 0: 1;
+      route->factive = vg_lerpf( route->factive, target, 
+                                 0.6f*vg.time_frame_delta );
+   }
+
+   for( u32 i=0; i<world_render.text_particle_count; i++ ){
+      struct text_particle *particle = &world_render.text_particles[i];
+      //rb_object_debug( &particle->obj, VG__RED );
+   }
+}
+
+void world_routes_fixedupdate( world_instance *world )
+{
+   rb_solver_reset();
+
+   rigidbody _null = {0};
+   _null.inv_mass = 0.0f;
+   m3x3_zero( _null.iI );
+
+   for( u32 i=0; i<world_render.text_particle_count; i++ ){
+      struct text_particle *particle = &world_render.text_particles[i];
+
+      if( rb_global_has_space() ){
+         rb_ct *buf = rb_global_buffer();
+
+         int l = rb_sphere__scene( particle->rb.to_world,
+                                   particle->radius,
+                                   NULL, world->geo_bh, buf,
+                                   k_material_flag_ghosts );
+
+         for( int j=0; j<l; j++ ){
+            buf[j].rba = &particle->rb;
+            buf[j].rbb = &_null;
+         }
+
+         rb_contact_count += l;
+      }
+   }
+
+   rb_presolve_contacts( rb_contact_buffer, 
+                         vg.time_fixed_delta, rb_contact_count );
+
+   for( int i=0; i<rb_contact_count; i++ ){
+      rb_contact_restitution( rb_contact_buffer+i, vg_randf64(&vg.rand) );
+   }
+
+   for( int i=0; i<6; i++ ){
+      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
+   }
+
+   for( u32 i=0; i<world_render.text_particle_count; i++ ){
+      struct text_particle *particle = &world_render.text_particles[i];
+      rb_iter( &particle->rb );
+   }
+
+   for( u32 i=0; i<world_render.text_particle_count; i++ ){
+      struct text_particle *particle = &world_render.text_particles[i];
+      rb_update_matrices( &particle->rb );
+   }
+}
+
+void bind_terrain_noise(void);
+void world_bind_light_array( world_instance *world,
+                             GLuint shader, GLuint location, 
+                             int slot );
+void world_bind_light_index( world_instance *world,
+                             GLuint shader, GLuint location, 
+                             int slot );
+
+void world_routes_update_timer_texts( world_instance *world )
+{
+   world_render.timer_text_count = 0;
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+
+      if( route->active_checkpoint != 0xffff ){
+         u32 next = route->active_checkpoint+1;
+             next = next % route->checkpoints_count;
+             next += route->checkpoints_start;
+
+         ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, next );
+         ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
+         ent_gate *dest = mdl_arritm( &world->ent_gate, gate->target );
+         
+         u32 j=0;
+         for( ; j<4; j++ ){
+            if( dest->routes[j] == i ){
+               break;
+            }
+         }
+
+         float h0 = 0.8f,
+               h1 = 1.2f,
+               depth = 0.4f,
+               size = 0.4f;
+
+         struct timer_text *text = 
+            &world_render.timer_texts[ world_render.timer_text_count ++ ];
+
+         text->gate = gate;
+         text->route = route;
+
+         vg_str str;
+         vg_strnull( &str, text->text, sizeof(text->text) );
+
+         if( route->valid_checkpoints >= route->checkpoints_count )
+         {
+            double lap_time = world_static.time - route->timing_base,
+                   time_centiseconds = lap_time * 100.0;
+
+            if( time_centiseconds > (float)0xfffe ) time_centiseconds = 0.0;
+
+            u16 centiseconds = time_centiseconds,
+                seconds      = centiseconds / 100,
+                minutes      = seconds / 60;
+
+            centiseconds %= 100;
+            seconds     %= 60;
+            minutes     %= 60;
+
+            if( minutes > 9 ) 
+               minutes = 9;
+            
+            if( minutes )
+            {
+               vg_strcati32r( &str, minutes, 1, ' ' );
+               vg_strcatch( &str, ':' );
+            }
+            
+            if( seconds >= 10 || minutes )
+            {
+               vg_strcati32r( &str, seconds, 2, '0' );
+            }
+            else
+            {
+               vg_strcati32r( &str, seconds, 1, '0' );
+            }
+
+            vg_strcatch( &str, '.' );
+            vg_strcati32r( &str, centiseconds, 1, '0' );
+         }
+         else
+         {
+            vg_strcati32r( &str, route->valid_checkpoints, 1, ' ' );
+            vg_strcatch( &str, '/' );
+            vg_strcati32r( &str, route->checkpoints_count + 1, 1, ' ' );
+         }
+   
+         gui_font3d.font = &gui.font;
+         float align_r  = font3d_string_width( 0, text->text );
+               align_r *= size;
+
+         v3f positions[] = {
+            { -0.92f, h0, depth },
+            {  0.92f - align_r, h0, depth },
+            { -0.92f, h1, depth },
+            {  0.92f - align_r, h1, depth },
+         };
+
+         if( dest->route_count == 1 ){
+            positions[0][0] = -align_r*0.5f;
+            positions[0][1] = h1;
+         }
+
+         m4x3f mmdl;
+         ent_gate_get_mdl_mtx( gate, mmdl );
+
+         m3x3_copy( mmdl, text->transform );
+         float ratio = v3_length(text->transform[0]) / 
+                        v3_length(text->transform[1]);
+
+         m3x3_scale( text->transform, (v3f){ size, size*ratio, 0.1f } );
+         m4x3_mulv( mmdl, positions[j], text->transform[3] );
+      }
+   }
+}
+
+void world_routes_fracture( world_instance *world, ent_gate *gate,
+                            v3f imp_co, v3f imp_v )
+{
+   world_render.text_particle_count = 0;
+   
+   for( u32 i=0; i<world_render.timer_text_count; i++ ){
+      struct timer_text *text = &world_render.timer_texts[i];
+
+      if( text->gate != gate ) continue;
+
+      m4x3f transform;
+      m4x3_mul( gate->transport, text->transform, transform );
+
+      v3f co, s;
+      v4f q;
+      m4x3_decompose( transform, co, q, s );
+
+      v3f offset;
+      v3_zero( offset );
+
+      v4f colour;
+      float brightness = 0.3f + world->ub_lighting.g_day_phase;
+      v3_muls( text->route->colour, brightness, colour );
+      colour[3] = 1.0f-text->route->factive;
+
+      for( u32 j=0;; j++ ){
+         char c = text->text[j];
+         if( !c ) break;
+
+         ent_glyph *glyph = font3d_glyph( &gui.font, 0, c );
+         if( !glyph ) continue;
+
+         if( c >= (u32)'0' && c <= (u32)'9' && glyph->indice_count ){
+            struct text_particle *particle = 
+               &world_render.text_particles[world_render.text_particle_count++];
+
+            particle->glyph = glyph;
+            v4_copy( colour, particle->colour );
+
+            v3f origin;
+            v2_muls( glyph->size, 0.5f, origin );
+            origin[2] = -0.5f;
+
+            v3f world_co;
+
+            v3_add( offset, origin, world_co );
+            m4x3_mulv( transform, world_co, world_co );
+
+
+            m3x3_identity( particle->mlocal );
+            m3x3_scale( particle->mlocal, s );
+            origin[2] *= s[2];
+            v3_muls( origin, -1.0f, particle->mlocal[3] );
+
+            v3_copy( world_co, particle->rb.co );
+            v3_muls( imp_v, 1.0f+vg_randf64(&vg.rand), particle->rb.v );
+            particle->rb.v[1] += 2.0f;
+
+            v4_copy( q, particle->rb.q );
+            particle->rb.w[0] = vg_randf64(&vg.rand)*2.0f-1.0f;
+            particle->rb.w[1] = vg_randf64(&vg.rand)*2.0f-1.0f;
+            particle->rb.w[2] = vg_randf64(&vg.rand)*2.0f-1.0f;
+
+            f32 r = vg_maxf( s[0]*glyph->size[0], s[1]*glyph->size[1] )*0.5f;
+            particle->radius = r*0.6f;
+            rb_setbody_sphere( &particle->rb, particle->radius, 1.0f, 1.0f );
+         }
+         offset[0] += glyph->size[0];
+      }
+   }
+}
+
+static void render_gate_markers( m4x3f world_mmdl, int run_id, ent_gate *gate ){
+   for( u32 j=0; j<4; j++ ){
+      if( gate->routes[j] == run_id ){
+         m4x3f mmdl;
+         m4x3_copy( gate->to_world, mmdl );
+         m3x3_scale( mmdl, (v3f){ gate->dimensions[0], 
+                                  gate->dimensions[1], 1.0f } );
+
+         m4x3_mul( world_mmdl, mmdl, mmdl );
+         shader_model_gate_uMdl( mmdl );
+         mdl_draw_submesh( &world_gates.sm_marker[j] );
+         break;
+      }
+   }
+}
+
+void render_world_routes( world_instance *world, 
+                          world_instance *host_world,
+                          m4x3f mmdl, vg_camera *cam, 
+                          int viewing_from_gate, int viewing_from_hub )
+{
+   shader_scene_route_use();
+   shader_scene_route_uTexGarbage(0);
+   world_link_lighting_ub( host_world, _shader_scene_route.id );
+   world_bind_position_texture( host_world, _shader_scene_route.id, 
+                        _uniform_scene_route_g_world_depth, 2 );
+   world_bind_light_array( host_world, _shader_scene_route.id,
+                        _uniform_scene_route_uLightsArray, 3 );
+   world_bind_light_index( host_world, _shader_scene_route.id,
+                                 _uniform_scene_route_uLightsIndex, 4 );
+   bind_terrain_noise();
+
+   shader_scene_route_uPv( cam->mtx.pv );
+
+   if( viewing_from_hub ){
+      m4x4f m4mdl, pvm;
+      m4x3_expand( mmdl, m4mdl );
+      m4x4_mul( cam->mtx_prev.pv, m4mdl, pvm );
+      shader_scene_route_uMdl( mmdl );
+      shader_scene_route_uPvmPrev( pvm );
+
+      m3x3f mnormal;
+      m3x3_inv( mmdl, mnormal );
+      m3x3_transpose( mnormal, mnormal );
+      v3_normalize( mnormal[0] );
+      v3_normalize( mnormal[1] );
+      v3_normalize( mnormal[2] );
+      shader_scene_route_uNormalMtx( mnormal );
+   }
+   else{
+      shader_scene_route_uMdl( mmdl );
+      shader_scene_route_uPvmPrev( cam->mtx_prev.pv );
+      m3x3f ident;
+      m3x3_identity( ident );
+      shader_scene_route_uNormalMtx( ident );
+   }
+
+   shader_scene_route_uCamera( cam->transform[3] );
+
+   mesh_bind( &world->mesh_route_lines );
+
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+      ent_route *route = mdl_arritm( &world->ent_route, i );
+
+      f32 t = viewing_from_hub? 1.0f: route->factive;
+
+      v4f colour;
+      v3_lerp( (v3f){0.7f,0.7f,0.7f}, route->colour, t, colour );
+      colour[3] = t*0.2f;
+
+      shader_scene_route_uColour( colour );
+      mdl_draw_submesh( &route->sm );
+   }
+
+   /* timers
+    * ---------------------------------------------------- */
+   if( !viewing_from_gate && !viewing_from_hub ){
+      font3d_bind( &gui.font, k_font_shader_default, 0, world, cam );
+
+      for( u32 i=0; i<world_render.timer_text_count; i++ ){
+         struct timer_text *text = &world_render.timer_texts[i];
+
+         v4f colour;
+         float brightness = 0.3f + world->ub_lighting.g_day_phase;
+         v3_muls( text->route->colour, brightness, colour );
+         colour[3] = 1.0f-text->route->factive;
+
+         shader_model_font_uColour( colour );
+         font3d_simple_draw( 0, text->text, cam, text->transform );
+      }
+
+      shader_model_font_uOffset( (v4f){0.0f,0.0f,0.0f,1.0f} );
+
+      for( u32 i=0; i<world_render.text_particle_count; i++ ){
+         struct text_particle *particle = &world_render.text_particles[i];
+
+         m4x4f prev_mtx;
+
+         m4x3_expand( particle->mdl, prev_mtx );
+         m4x4_mul( cam->mtx_prev.pv, prev_mtx, prev_mtx );
+
+         shader_model_font_uPvmPrev( prev_mtx );
+
+         v4f q;
+         m4x3f model;
+         rb_extrapolate( &particle->rb, model[3], q );
+         q_m3x3( q, model );
+
+         m4x3_mul( model, particle->mlocal, particle->mdl );
+         shader_model_font_uMdl( particle->mdl );
+         shader_model_font_uColour( particle->colour );
+
+         mesh_drawn( particle->glyph->indice_start, 
+                     particle->glyph->indice_count );
+      }
+   }
+
+   /* gate markers 
+    * ---------------------------------------------------- */
+
+   shader_model_gate_use();
+   shader_model_gate_uPv( cam->mtx.pv );
+   shader_model_gate_uCam( cam->pos );
+   shader_model_gate_uTime( vg.time*0.25f );
+   shader_model_gate_uInvRes( (v2f){
+         1.0f / (float)vg.window_x,
+         1.0f / (float)vg.window_y });
+
+   mesh_bind( &world_gates.mesh );
+
+   /* skip writing into the motion vectors for this */
+   glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
+   glDisable( GL_CULL_FACE );
+
+   if( viewing_from_hub ){
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+
+         v4f colour;
+         v3_muls( route->colour, 1.6666f, colour );
+         colour[3] = 0.0f;
+
+         shader_model_gate_uColour( colour );
+
+         for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ ){
+            ent_gate *gate = mdl_arritm( &world->ent_gate, j );
+            if( !(gate->flags & k_ent_gate_nonlocal) )
+               render_gate_markers( mmdl, i, gate );
+         }
+      }
+   }
+   else{
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+
+         if( route->active_checkpoint != 0xffff ){
+            v4f colour;
+            float brightness = 0.3f + world->ub_lighting.g_day_phase;
+            v3_muls( route->colour, brightness, colour );
+            colour[3] = 1.0f-route->factive;
+
+            shader_model_gate_uColour( colour );
+
+            u32 next = route->active_checkpoint+1+viewing_from_gate;
+                next = next % route->checkpoints_count;
+                next += route->checkpoints_start;
+
+            ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, next );
+            ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
+            render_gate_markers( mmdl, i, gate );
+         }
+      }
+   }
+   glEnable( GL_CULL_FACE );
+   glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 } );
+}
diff --git a/src/world_routes.h b/src/world_routes.h
new file mode 100644 (file)
index 0000000..621c28a
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+#include "vg/vg_camera.h"
+#include "vg/vg_msg.h"
+#include "world.h"
+#include "network_msg.h"
+
+void world_routes_init(void);
+void world_routes_fracture( world_instance *world, ent_gate *gate,
+                               v3f imp_co, v3f imp_v );
+void world_routes_activate_entry_gate( world_instance *world, 
+                                          ent_gate *rg );
+void render_world_routes( world_instance *world, 
+                          world_instance *host_world,
+                          m4x3f mmdl, vg_camera *cam, 
+                          int viewing_from_gate, int viewing_from_hub );
+
+void world_gen_routes_ent_init( world_instance *world );
+void world_gen_routes_generate( u32 instance_id );
+void world_routes_update_timer_texts( world_instance *world );
+void world_routes_update( world_instance *world );
+void world_routes_fixedupdate( world_instance *world );
+void world_routes_clear( world_instance *world );
+void world_routes_recv_scoreboard( world_instance *world, 
+                                   vg_msg *body, u32 route_id,
+                                   enum request_status status );
diff --git a/src/world_routes_ui.c b/src/world_routes_ui.c
new file mode 100644 (file)
index 0000000..0afbeae
--- /dev/null
@@ -0,0 +1,168 @@
+#include "skaterift.h"
+#include "world_routes_ui.h"
+#include "world_routes.h"
+#include "player.h"
+
+static u32 v4_rgba( v4f colour ){
+   u32 r = vg_minf(1.0f,colour[0])*255.0f,
+       g = vg_minf(1.0f,colour[1])*255.0f,
+       b = vg_minf(1.0f,colour[2])*255.0f,
+       a = vg_minf(1.0f,colour[3])*255.0f;
+
+   return r | (g<<8) | (b<<16) | (a<<24);
+}
+
+static void ent_route_imgui( ui_context *ctx, 
+                             world_instance *world, ent_route *route, 
+                             ui_point inout_cursor ){
+   if( route->flags & k_ent_route_flag_out_of_zone )
+      return;
+
+   u32 last_version=0;
+   f64 last_time = 0.0;
+   ent_checkpoint *last_cp = NULL;
+
+   u32 valid_sections=0;
+
+   struct time_block{
+      f32 length, best;
+      int clean;
+   }
+   blocks[ route->checkpoints_count ];
+
+   for( u32 i=0; i<route->checkpoints_count; i++ ){
+      u32 cpid  = i+route->active_checkpoint+1;
+          cpid %= route->checkpoints_count;
+          cpid += route->checkpoints_start;
+
+      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, cpid );
+      ent_gate *rg = mdl_arritm( &world->ent_gate, cp->gate_index );
+                rg = mdl_arritm( &world->ent_gate, rg->target );
+
+      if( last_version+1 == rg->timing_version ) {
+         struct time_block *block = &blocks[ valid_sections ++ ];
+         block->clean  = (rg->flags & k_ent_gate_clean_pass)? 1: 0;
+         block->length = rg->timing_time - last_time;
+         block->best = last_cp? last_cp->best_time: 0.0f;
+      }
+      else valid_sections = 0;
+
+      last_version = rg->timing_version;
+      last_time = rg->timing_time;
+      last_cp = cp;
+   }
+
+   if( last_version+1 == world_static.current_run_version ){
+      struct time_block *block = &blocks[ valid_sections ++ ];
+      block->clean = localplayer.rewinded_since_last_gate? 0: 1;
+      block->length = world_static.time - last_time;
+      block->best = last_cp->best_time;
+   }
+   else 
+      valid_sections = 0;
+
+   u32 colour = v4_rgba( route->colour ) | 0xff000000;
+
+   ui_px x = 0,
+         h = route->factive * 16.0f,
+         base = inout_cursor[0];//(f32)vg.window_x*0.5f - route->ui_stopper;
+
+   if( route->ui_residual > 0.0f )
+   {
+      ui_px w = route->ui_residual_block_w,
+            total = w + 4;
+
+      f32 t = vg_smoothstepf(1.0f-route->ui_residual);
+      
+      x -= (f32)total * t;
+
+      ui_rect rect = { base+x, inout_cursor[1], w, h };
+
+      v4f fadecolour;
+      v4_copy( route->colour, fadecolour );
+      fadecolour[3] *= route->ui_residual;
+
+      ui_fill( ctx, rect, v4_rgba(fadecolour) );
+
+      x += total;
+   }
+
+   int got_first = 0;
+
+   for( u32 i=0; i<valid_sections; i ++ )
+   {
+      struct time_block *block = &blocks[ i ];
+      ui_px w = 20 + (block->length * 6.0f);
+      ui_rect rect = { base+x, inout_cursor[1], w, h };
+      ui_fill( ctx, rect, colour );
+   
+      if( block->clean )
+         ui_outline( ctx, rect, 1, 0xff00ffff, 0 );
+
+      if( block->best != 0.0f )
+      {
+         char buf[32];
+         vg_str str;
+         vg_strnull( &str, buf, 32 );
+         
+         f32 diff = block->length - block->best,
+             as = fabsf(diff),
+             s  = floorf( as ),
+             ds = floorf( vg_fractf( as ) * 10.0f );
+
+         if( (block->best != 0.0f) && (fabsf(diff) > 0.001f) )
+         {
+            if( diff > 0.0f )
+               vg_strcatch( &str, '+' );
+            else
+               vg_strcatch( &str, '-' );
+
+            vg_strcati32( &str, s );
+            vg_strcatch( &str, '.' );
+            vg_strcati32( &str, ds );
+
+            ui_text( ctx, rect, buf, 1, k_ui_align_middle_center, 0 );
+         }
+      }
+
+      x += w + 4;
+
+      if( !got_first ){
+         route->ui_first_block_width = w;
+         got_first = 1;
+      }
+   }
+
+   for( u32 i=0; i<route->checkpoints_count-valid_sections; i++ )
+   {
+      struct time_block *block = &blocks[ i ];
+
+      ui_px w = 20;
+      ui_rect rect = { base+x, inout_cursor[1], w, h };
+      ui_outline( ctx, rect, -1, colour, 0 );
+      x += w + 4;
+
+      if( !got_first )
+      {
+         route->ui_first_block_width = w;
+         got_first = 1;
+      }
+   }
+
+   inout_cursor[1] += h + 4;
+
+   vg_slewf( &route->ui_residual, 0.0f, vg.time_frame_delta );
+   route->ui_stopper = vg_lerpf( route->ui_stopper, (f32)x*0.5f, 
+                                 vg.time_frame_delta );
+}
+
+void world_routes_imgui( ui_context *ctx, world_instance *world )
+{
+   if( skaterift.activity == k_skaterift_menu ) return;
+
+   ui_point cursor = { 4, 4 };
+   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ )
+   {
+      ent_route_imgui( ctx, world, mdl_arritm( &world->ent_route, i ), cursor );
+   }
+}
diff --git a/src/world_routes_ui.h b/src/world_routes_ui.h
new file mode 100644 (file)
index 0000000..70c0fcd
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+#include "world_routes.h"
+
+struct route_ui{};
+void world_routes_imgui( ui_context *ctx, world_instance *world );
diff --git a/src/world_sfd.c b/src/world_sfd.c
new file mode 100644 (file)
index 0000000..6473d63
--- /dev/null
@@ -0,0 +1,362 @@
+#ifndef SFD_C
+#define SFD_C
+
+#include "world_sfd.h"
+#include "shaders/scene_scoretext.h"
+#include "shaders/scene_vertex_blend.h"
+#include "network.h"
+#include "entity.h"
+#include "network_common.h"
+#include "world_routes.h"
+
+struct world_sfd world_sfd;
+
+static f32 sfd_encode_glyph( char c ){
+   int value = 0;
+   if( c >= 'a' && c <= 'z' )
+      value = c-'a'+11;
+   else if( c >= '0' && c <= '9' )
+      value = c-'0'+1;
+   else if( c >= 'A' && c <= 'Z' )
+      value = c-'A'+11;
+   else if( c >= '\x01' && c <= '\x01'+10 )
+      value = 63-c;
+   else{
+      int base = 11+26;
+
+      switch( c ){
+         case '!': value=base+0; break;
+         case '?': value=base+1; break;
+         case ',': value=base+2; break;
+         case '.': value=base+3; break;
+         case '#': value=base+4; break;
+         case '$': value=base+5; break;
+         case '%': value=base+6; break;
+         case '*': value=base+7; break;
+         case '+': value=base+8; break;
+         case '-': value=base+9; break;
+         case '/': value=base+10; break;
+         case ':': value=base+11; break;
+         default: value=0; break;
+      }
+   }
+
+   return (float)value;
+}
+
+static void sfd_clear( u32 row ){
+   u32 row_h = world_sfd.h -1 -row;
+   for( int i=0; i<world_sfd.w; i++ ){
+      u32 idx = (world_sfd.w*row_h + i) * 2;
+      world_sfd.buffer[idx] = 0.0f;
+   }
+}
+
+void sfd_encode( v2i co, const char *str, enum world_sfd_align align )
+{
+   i32 row_h = world_sfd.h -1 -co[1];
+   i32 offset_x = 0;
+
+   i32 w = VG_MIN( strlen(str), world_sfd.w );
+   if( align == k_world_sfd_center )
+      offset_x = (world_sfd.w - w) / 2;
+   else if( align == k_world_sfd_right )
+      offset_x = world_sfd.w - w;
+   else
+      offset_x = co[0];
+
+   for( i32 i=0; i<world_sfd.w; i++ ){
+      i32 u = i + offset_x,
+          idx = (world_sfd.w*row_h + u) * 2;
+
+      if( (u >= world_sfd.w) || (u < 0) )
+         continue;
+
+      if( !str[i] ) 
+         return;
+
+      world_sfd.buffer[idx] = sfd_encode_glyph( str[i] );
+   }
+}
+
+void world_sfd_compile_scores( struct leaderboard_cache *board,
+                               const char *title )
+{
+   for( u32 i=0; i<13; i++ )
+      sfd_clear(i);
+
+   sfd_encode( (v2i){0,0}, title, k_world_sfd_left );
+
+   if( !board ){
+      sfd_encode( (v2i){-1,4}, "Error out of range", k_world_sfd_center );
+      return;
+   }
+
+   if( !network_connected() ){
+      sfd_encode( (v2i){-1,0}, "Offline", k_world_sfd_right );
+      return;
+   }
+
+   if( board->status == k_request_status_not_found ){
+      sfd_encode( (v2i){-1,4}, "No records", k_world_sfd_center );
+      return;
+   }
+
+   if( board->status != k_request_status_ok ){
+      char buf[32];
+      vg_str s;
+      vg_strnull( &s, buf, 32 );
+      vg_strcat( &s, "Error: " );
+      vg_strcati32( &s, board->status );
+      sfd_encode( (v2i){-1,4}, buf, k_world_sfd_center );
+      return;
+   }
+
+   vg_msg body;
+   vg_msg_init( &body, board->data, board->data_len );
+
+   const char *alias = "rows";
+
+   if( world_sfd.view_weekly ){
+      alias = "rows_weekly";
+      sfd_encode( (v2i){-1,0}, "Weekly", k_world_sfd_right );
+   }
+   else {
+      sfd_encode( (v2i){-1,0}, "All-Time", k_world_sfd_right );
+   }
+
+   u32 l = 1;
+   if( vg_msg_seekframe( &body, alias ) ){
+      while( vg_msg_seekframe( &body, NULL ) ){
+         /* name */
+         const char *username = vg_msg_getkvstr( &body, "username" );
+
+         char buf[100];
+         vg_str str;
+         vg_strnull( &str, buf, 100 );
+         vg_strcati32( &str, l );
+         vg_strcat( &str, " " );
+
+         if( username )
+            vg_strcat( &str, username );
+         else
+            vg_strcat( &str, "??????" );
+
+         sfd_encode( (v2i){0,l}, str.buffer, k_world_sfd_left );
+
+         /* time */
+         vg_strnull( &str, buf, 100 );
+         
+         u32 centiseconds;
+         vg_msg_getkvintg( &body, "time", k_vg_msg_i32, &centiseconds, NULL );
+
+         i32 seconds      = centiseconds / 100,
+             minutes      = seconds / 60;
+
+         centiseconds %= 100;
+         seconds     %= 60;
+         minutes     %= 60;
+         if( minutes > 9 ) vg_strcat( &str, "?" );
+         else vg_strcati32( &str, minutes );
+         vg_strcat( &str, ":" );
+         vg_strcati32r( &str, seconds, 2, '0' );
+         vg_strcat( &str, "." );
+         vg_strcati32r( &str, centiseconds, 2, '0' );
+         sfd_encode( (v2i){-1,l}, str.buffer, k_world_sfd_right );
+         l ++;
+
+         vg_msg_skip_frame( &body );
+      }
+   }
+   else {
+      sfd_encode( (v2i){-1,4}, "No records", k_world_sfd_center );
+   }
+}
+
+void world_sfd_compile_active_scores(void)
+{
+   world_instance *world = world_current_instance();
+   
+   struct leaderboard_cache *board = NULL;
+   const char *name = "Out of range";
+
+   if( world_sfd.active_route_board < mdl_arrcount( &world->ent_route ) ){
+      board = &world->leaderboard_cache[ world_sfd.active_route_board ];
+      ent_route *route = mdl_arritm( &world->ent_route, 
+                                     world_sfd.active_route_board );
+      name = mdl_pstr( &world->meta, route->pstr_name );
+   }
+         
+   world_sfd_compile_scores( board, name );
+}
+
+void world_sfd_update( world_instance *world, v3f pos )
+{
+   if( mdl_arrcount( &world->ent_route ) ){
+      u32 closest = 0;
+      float min_dist = INFINITY;
+
+      for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+         float dist = v3_dist2( route->board_transform[3], pos );
+
+         if( dist < min_dist ){
+            min_dist = dist;
+            closest = i;
+         }
+      }
+
+      struct leaderboard_cache *board = &world->leaderboard_cache[ closest ];
+
+      /* request new board if cache expires */
+      if( network_connected() ){
+         f64 delta = vg.time_real - board->cache_time;
+         if( (delta > 45.0) || (board->cache_time == 0.0) ){
+            board->cache_time = vg.time_real;
+            ent_route *route = mdl_arritm( &world->ent_route, closest );
+            addon_reg *world_reg = 
+               world_static.instance_addons[ world - world_static.instances ];
+
+            char mod_uid[ ADDON_UID_MAX ];
+            addon_alias_uid( &world_reg->alias, mod_uid );
+
+            network_request_scoreboard( 
+                  mod_uid, 
+                  mdl_pstr( &world->meta, route->pstr_name ),
+                  NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK, closest );
+         }
+      }
+
+      /* compile board text if we changed. */
+      if( world_sfd.active_route_board != closest ){
+         world_sfd_compile_active_scores();
+      }
+
+      world_sfd.active_route_board = closest;
+   }
+
+   for( int i=0; i<world_sfd.w*world_sfd.h; i++ ){
+      float *target = &world_sfd.buffer[i*2+0],
+            *cur =    &world_sfd.buffer[i*2+1];
+      
+      float const rate = vg.time_delta * 25.2313131414f;
+      float d1 = *target-*cur;
+      
+      if( fabsf(d1) > rate ){
+         *cur += rate;
+         if( *cur > 49.0f )
+            *cur -= 49.0f;
+      }
+      else
+         *cur = *target;
+   }
+}
+
+void bind_terrain_noise(void);
+void sfd_render( world_instance *world, vg_camera *cam, m4x3f transform )
+{
+   mesh_bind( &world_sfd.mesh_display );
+   shader_scene_scoretext_use();
+   shader_scene_scoretext_uTexMain(1);
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_scoretext );
+
+   bind_terrain_noise();
+
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_sfd.tex_scoretex );
+
+   m4x4f pvm_prev;
+   m4x3_expand( transform, pvm_prev );
+   m4x4_mul( cam->mtx_prev.pv, pvm_prev, pvm_prev );
+
+   shader_scene_scoretext_uPv( cam->mtx.pv );
+   shader_scene_scoretext_uPvmPrev( pvm_prev );
+   shader_scene_scoretext_uMdl( transform );
+   shader_scene_scoretext_uCamera( cam->transform[3] );
+
+   for( int y=0;y<world_sfd.h; y++ ){
+      for( int x=0; x<world_sfd.w; x++ ){
+         float value = world_sfd.buffer[(y*world_sfd.w+x)*2+1];
+         shader_scene_scoretext_uInfo( (v3f){ x,y, value } );
+         mesh_draw( &world_sfd.mesh_display );
+      }
+   }
+
+   shader_scene_vertex_blend_use();
+   shader_scene_vertex_blend_uTexGarbage(0);
+   shader_scene_vertex_blend_uTexGradients(1);
+   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_vertex_blend );
+
+   bind_terrain_noise();
+   glActiveTexture( GL_TEXTURE1 );
+   glBindTexture( GL_TEXTURE_2D, world_sfd.tex_scoretex );
+
+   shader_scene_vertex_blend_uPv( cam->mtx.pv );
+   shader_scene_vertex_blend_uPvmPrev( pvm_prev );
+   shader_scene_vertex_blend_uMdl( transform );
+   shader_scene_vertex_blend_uCamera( cam->transform[3] );
+   
+   mesh_bind( &world_sfd.mesh_base );
+   mdl_draw_submesh( &world_sfd.sm_base );
+}
+
+void world_sfd_init(void)
+{
+   vg_info( "world_sfd_init\n" );
+   vg_linear_clear( vg_mem.scratch );
+
+   mdl_context mscoreboard;
+   mdl_open( &mscoreboard, "models/rs_scoretext.mdl", vg_mem.scratch );
+   mdl_load_metadata_block( &mscoreboard, vg_mem.scratch );
+   mdl_async_load_glmesh( &mscoreboard, &world_sfd.mesh_base, NULL );
+
+   mdl_load_mesh_block( &mscoreboard, vg_mem.scratch );
+
+   scene_context *scene = &world_sfd.scene;
+   vg_async_item *call = scene_alloc_async( scene, &world_sfd.mesh_display,
+                                            3000, 8000 );
+
+
+   mdl_mesh *m_backer = mdl_find_mesh( &mscoreboard, "backer" ),
+            *m_card   = mdl_find_mesh( &mscoreboard, "score_card" );
+
+   mdl_submesh 
+      *sm_backer = mdl_arritm( &mscoreboard.submeshs, m_backer->submesh_start ),
+      *sm_card   = mdl_arritm( &mscoreboard.submeshs, m_card->submesh_start );
+   world_sfd.sm_base = *sm_backer;
+
+   m4x3f identity;
+   m4x3_identity( identity );
+
+   for( int i=0;i<4;i++ ){
+      u32 vert_start = scene->vertex_count;
+      scene_add_mdl_submesh( scene, &mscoreboard, sm_card, identity );
+
+      for( int j=0; j<sm_card->vertex_count; j++ ){
+         scene_vert *vert = &scene->arrvertices[ vert_start+j ];
+
+         float const k_glyph_uvw = 1.0f/64.0f;
+         vert->uv[0] -= k_glyph_uvw * (float)(i-1);
+         vert->norm[3] = i*42;
+      }
+   }
+
+   vg_async_dispatch( call, async_scene_upload );
+   vg_tex2d_load_qoi_async_file( "textures/scoretext.qoi", 
+                                 VG_TEX2D_CLAMP|VG_TEX2D_NEAREST,
+                                 &world_sfd.tex_scoretex );
+
+   mdl_close( &mscoreboard );
+
+   int w = 27,
+       h = 13;
+
+   world_sfd.w = w;
+   world_sfd.h = h;
+   world_sfd.buffer = vg_linear_alloc( vg_mem.rtmemory, 2*w*h*sizeof(float) );
+
+   for( int i=0; i<w*h*2; i++ )
+      world_sfd.buffer[i] = 0.0f;
+}
+
+#endif /* WORLD_SFD_C */
diff --git a/src/world_sfd.h b/src/world_sfd.h
new file mode 100644 (file)
index 0000000..e79fbbc
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+#pragma once
+#include "world.h"
+#include "world_routes.h"
+#include "scene.h"
+
+struct world_sfd{
+   GLuint tex_scoretex;
+
+   glmesh mesh_base, mesh_display;
+   mdl_submesh sm_base;
+
+   u32 active_route_board;
+   scene_context scene;
+
+   u32 view_weekly;
+
+   u32 w, h;
+   float *buffer;
+}
+extern world_sfd;
+void world_sfd_init(void);
+
+enum world_sfd_align {
+   k_world_sfd_left,
+   k_world_sfd_right,
+   k_world_sfd_center
+};
+
+void sfd_encode( v2i co, const char *str, enum world_sfd_align align );
+void world_sfd_update( world_instance *world, v3f pos );
+void sfd_render( world_instance *world, vg_camera *cam, m4x3f transform );
+void world_sfd_compile_scores( struct leaderboard_cache *leaderboard,
+                               const char *title );
+void world_sfd_compile_active_scores(void);
diff --git a/src/world_volumes.c b/src/world_volumes.c
new file mode 100644 (file)
index 0000000..b74e493
--- /dev/null
@@ -0,0 +1,103 @@
+#include "world_volumes.h"
+
+void world_volumes_update( world_instance *world, v3f pos )
+{
+   /* filter and check the existing ones */
+   u32 j=0;
+   for( u32 i=0; i<world_static.active_trigger_volume_count; i++ ){
+      i32 idx = world_static.active_trigger_volumes[i];
+      ent_volume *volume = mdl_arritm( &world->ent_volume, idx );
+
+      v3f local;
+      m4x3_mulv( volume->to_local, pos, local );
+      if( (fabsf(local[0]) <= 1.0f) &&
+          (fabsf(local[1]) <= 1.0f) &&
+          (fabsf(local[2]) <= 1.0f) )
+      {
+         world_static.active_trigger_volumes[ j ++ ] = idx;
+         boxf cube = {{-1.0f,-1.0f,-1.0f},{1.0f,1.0f,1.0f}};
+         vg_line_boxf_transformed( volume->to_world, cube, 0xff00ccff );
+      }
+      else{
+         /* 
+          * LEGACY BEHAVIOUR: < v104 does not have leave events
+          */
+         if( world->meta.info.version >= 104 ){
+            ent_call basecall;
+            basecall.function = k_ent_function_trigger_leave;
+            basecall.id = mdl_entity_id( k_ent_volume, idx );
+            basecall.data = NULL;
+
+            entity_call( world, &basecall );
+         }
+      }
+   }
+   world_static.active_trigger_volume_count = j;
+
+   static float random_accum = 0.0f;
+   random_accum += vg.time_delta;
+
+   u32 random_ticks = 0;
+
+   while( random_accum > 0.1f ){
+      random_accum -= 0.1f;
+      random_ticks ++;
+   }
+
+   float radius = 32.0f;
+
+   bh_iter it;
+   bh_iter_init_range( 0, &it, pos, radius );
+   i32 idx;
+
+   while( bh_next( world->entity_bh, &it, &idx ) ){
+      u32 id    = world->entity_list[ idx ],
+          type  = mdl_entity_id_type( id ),
+          index = mdl_entity_id_id( id );
+
+      if( type != k_ent_volume ) continue;
+
+      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
+      boxf cube = {{-1.0f,-1.0f,-1.0f},{1.0f,1.0f,1.0f}};
+      
+      if( volume->flags & k_ent_volume_flag_particles ){
+         vg_line_boxf_transformed( volume->to_world, cube, 0xff00c0ff );
+
+         for( int j=0; j<random_ticks; j++ ){
+            ent_call basecall;
+            basecall.id = id;
+            basecall.data = NULL;
+            basecall.function = 0;
+
+            entity_call( world, &basecall );
+         }
+      }
+      else{
+         for( u32 i=0; i<world_static.active_trigger_volume_count; i++ )
+            if( world_static.active_trigger_volumes[i] == index )
+               goto next_volume;
+
+         if( world_static.active_trigger_volume_count > 
+               VG_ARRAY_LEN(world_static.active_trigger_volumes) ) continue;
+
+         v3f local;
+         m4x3_mulv( volume->to_local, pos, local );
+
+         if( (fabsf(local[0]) <= 1.0f) &&
+             (fabsf(local[1]) <= 1.0f) &&
+             (fabsf(local[2]) <= 1.0f) ){
+            ent_call basecall;
+            basecall.function = 0;
+            basecall.id = id;
+            basecall.data = NULL;
+
+            entity_call( world, &basecall );
+            world_static.active_trigger_volumes[ 
+               world_static.active_trigger_volume_count ++ ] = index;
+         }
+         else
+            vg_line_boxf_transformed( volume->to_world, cube, 0xffcccccc );
+      }
+next_volume:;
+   }
+}
diff --git a/src/world_volumes.h b/src/world_volumes.h
new file mode 100644 (file)
index 0000000..2d84e9e
--- /dev/null
@@ -0,0 +1,5 @@
+#pragma once
+#include "world.h"
+#include "vg/vg_bvh.h"
+
+void world_volumes_update( world_instance *world, v3f pos );
diff --git a/src/world_water.c b/src/world_water.c
new file mode 100644 (file)
index 0000000..3d95f09
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#include "world_water.h"
+#include "world_render.h"
+#include "render.h"
+#include "shaders/scene_water.h"
+#include "shaders/scene_water_fast.h"
+#include "scene.h"
+#include "player.h"
+#include "player_walk.h"
+#include "player_dead.h"
+
+struct world_water world_water;
+
+void world_water_init(void)
+{
+   vg_info( "world_water_init\n" );
+
+   vg_tex2d_load_qoi_async_file( "textures/water_surf.qoi",
+                                 VG_TEX2D_LINEAR|VG_TEX2D_REPEAT,
+                                 &world_water.tex_water_surf );
+
+   vg_success( "done\n" );
+}
+
+void water_set_surface( world_instance *world, float height )
+{
+   world->water.height = height;
+   v4_copy( (v4f){ 0.0f, 1.0f, 0.0f, height }, world->water.plane );
+}
+
+void world_link_lighting_ub( world_instance *world, GLuint shader );
+void world_bind_position_texture( world_instance *world, 
+                                     GLuint shader, GLuint location,
+                                     int slot );
+void world_bind_light_array( world_instance *world,
+                                GLuint shader, GLuint location, 
+                                int slot );
+void world_bind_light_index( world_instance *world,
+                                       GLuint shader, GLuint location, 
+                                       int slot );
+
+/*
+ * Does not write motion vectors
+ */
+void render_water_texture( world_instance *world, vg_camera *cam )
+{
+   if( !world->water.enabled || (vg.quality_profile == k_quality_profile_low) )
+      return;
+
+   /* Draw reflection buffa */
+   vg_framebuffer_bind( g_render.fb_water_reflection, k_render_scale );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+
+   /* 
+    * Create flipped view matrix. Don't care about motion vectors
+    */
+   float cam_height = cam->transform[3][1] - world->water.height;
+
+   vg_camera water_cam;
+   water_cam.farz = cam->farz;
+   water_cam.nearz = cam->nearz;
+   v3_copy( cam->transform[3], water_cam.transform[3] );
+   water_cam.transform[3][1] -= 2.0f * cam_height;
+
+   m3x3f flip;
+   m3x3_identity( flip );
+   flip[1][1] = -1.0f;
+   m3x3_mul( flip, cam->transform, water_cam.transform );
+
+   vg_camera_update_view( &water_cam );
+
+   /* 
+    * Create clipped projection 
+    */
+   v4f clippa = { 0.0f, 1.0f, 0.0f, world->water.height-0.1f };
+   m4x3_mulp( water_cam.transform_inverse, clippa, clippa );
+   clippa[3] *= -1.0f;
+
+   m4x4_copy( cam->mtx.p, water_cam.mtx.p );
+   m4x4_clip_projection( water_cam.mtx.p, clippa );
+
+   vg_camera_finalize( &water_cam );
+
+   /*
+    * Draw world
+    */
+   glEnable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+   glCullFace( GL_FRONT );
+   render_world( world, &water_cam, 0, 1, 0, 1 );
+   glCullFace( GL_BACK );
+   
+   /*
+    * Create beneath view matrix
+    */
+   vg_camera beneath_cam;
+   vg_framebuffer_bind( g_render.fb_water_beneath, k_render_scale );
+   glClearColor( 1.0f, 0.0f, 0.0f, 0.0f );
+   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
+
+   m4x3_copy( cam->transform, beneath_cam.transform );
+   vg_camera_update_view( &beneath_cam );
+
+   float bias = -(cam->transform[3][1]-world->water.height)*0.1f;
+
+   v4f clippb = { 0.0f, -1.0f, 0.0f, -(world->water.height) + bias };
+   m4x3_mulp( beneath_cam.transform_inverse, clippb, clippb );
+   clippb[3] *= -1.0f;
+
+   m4x4_copy( cam->mtx.p, beneath_cam.mtx.p );
+   m4x4_clip_projection( beneath_cam.mtx.p, clippb );
+   vg_camera_finalize( &beneath_cam );
+
+   glEnable( GL_DEPTH_TEST );
+   glDisable( GL_BLEND );
+   render_world_depth( world, &beneath_cam );
+   //glViewport( 0,0, g_render_x, g_render_y );
+}
+
+void render_water_surface( world_instance *world, vg_camera *cam )
+{
+   if( !world->water.enabled )
+      return;
+
+   if( vg.quality_profile == k_quality_profile_high )
+   {
+      /* Draw surface */
+      shader_scene_water_use();
+      
+      vg_framebuffer_bind_texture( g_render.fb_water_reflection, 0, 0 );
+      shader_scene_water_uTexMain( 0 );
+   
+      glActiveTexture( GL_TEXTURE1 );
+      glBindTexture( GL_TEXTURE_2D, world_water.tex_water_surf );
+      shader_scene_water_uTexDudv( 1 );
+      
+      shader_scene_water_uInvRes( (v2f){
+            1.0f / (float)vg.window_x,
+            1.0f / (float)vg.window_y });
+
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_water );
+
+      vg_framebuffer_bind_texture( g_render.fb_water_beneath, 0, 5 );
+      shader_scene_water_uTexBack( 5 );
+      shader_scene_water_uTime( world_static.time );
+      shader_scene_water_uCamera( cam->transform[3] );
+      shader_scene_water_uSurfaceY( world->water.height );
+
+      shader_scene_water_uPv( cam->mtx.pv );
+      shader_scene_water_uPvmPrev( cam->mtx_prev.pv );
+
+      m4x3f full;
+      m4x3_identity( full );
+      shader_scene_water_uMdl( full );
+
+      glEnable(GL_BLEND);
+      glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+      glBlendEquation(GL_FUNC_ADD);
+
+      mesh_bind( &world->mesh_no_collide );
+
+      for( int i=0; i<world->surface_count; i++ )
+      {
+         struct world_surface *mat = &world->surfaces[i];
+         struct shader_props_water *props = mat->info.props.compiled;
+
+         if( mat->info.shader == k_shader_water )
+         {
+            shader_scene_water_uShoreColour( props->shore_colour );
+            shader_scene_water_uOceanColour( props->deep_colour );
+            shader_scene_water_uFresnel( props->fresnel );
+            shader_scene_water_uWaterScale( props->water_sale );
+            shader_scene_water_uWaveSpeed( props->wave_speed );
+
+            mdl_draw_submesh( &mat->sm_no_collide );
+         }
+      }
+
+      glDisable(GL_BLEND);
+   }
+   else if( (vg.quality_profile == k_quality_profile_low) ||
+            (vg.quality_profile == k_quality_profile_min) )
+   {
+      shader_scene_water_fast_use();
+
+      glActiveTexture( GL_TEXTURE1 );
+      glBindTexture( GL_TEXTURE_2D, world_water.tex_water_surf );
+      shader_scene_water_fast_uTexDudv( 1 );
+
+      shader_scene_water_fast_uTime( world_static.time );
+      shader_scene_water_fast_uCamera( cam->transform[3] );
+      shader_scene_water_fast_uSurfaceY( world->water.height );
+
+      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_water_fast );
+
+      m4x3f full;
+      m4x3_identity( full );
+      shader_scene_water_fast_uMdl( full );
+      shader_scene_water_fast_uPv( cam->mtx.pv );
+      shader_scene_water_fast_uPvmPrev( cam->mtx_prev.pv );
+
+      glEnable(GL_BLEND);
+      glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+      glBlendEquation(GL_FUNC_ADD);
+
+      mesh_bind( &world->mesh_no_collide );
+
+      for( int i=0; i<world->surface_count; i++ )
+      {
+         struct world_surface *mat = &world->surfaces[i];
+         struct shader_props_water *props = mat->info.props.compiled;
+
+         if( mat->info.shader == k_shader_water )
+         {
+            shader_scene_water_fast_uShoreColour( props->shore_colour );
+            shader_scene_water_fast_uOceanColour( props->deep_colour );
+
+            mdl_draw_submesh( &mat->sm_no_collide );
+         }
+      }
+
+      glDisable(GL_BLEND);
+   }
+}
+
+static void world_water_drown(void)
+{
+   if( localplayer.drowned ) return;
+   player__networked_sfx( k_player_subsystem_walk, 32, 
+                          k_player_walk_soundeffect_splash,
+                          localplayer.rb.co, 1.0f );
+   vg_info( "player fell of due to walking into walker\n" );
+   localplayer.drowned = 1;
+   player__dead_transition( k_player_die_type_generic );
+}
+
+bool world_water_player_safe( world_instance *world, f32 allowance )
+{
+   if( !world->water.enabled ) return 1;
+   if( world->info.flags & 0x2 ) return 1;
+
+   if( localplayer.rb.co[1]+allowance < world->water.height )
+   {
+      world_water_drown();
+      return 0;
+   }
+
+   return 1;
+}
+
+entity_call_result ent_water_call( world_instance *world, ent_call *call )
+{
+   if( call->function == 0 )
+   {
+      world_water_drown();
+      return k_entity_call_result_OK;
+   }
+
+   return k_entity_call_result_unhandled;
+}
diff --git a/src/world_water.h b/src/world_water.h
new file mode 100644 (file)
index 0000000..7ff9af5
--- /dev/null
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
+ */
+
+#pragma once
+#include "world.h"
+
+struct world_water{
+   GLuint tex_water_surf;
+}
+extern world_water;
+void world_water_init(void);
+
+void water_set_surface( world_instance *world, f32 height );
+void render_water_texture( world_instance *world, vg_camera *cam );
+void render_water_surface( world_instance *world, vg_camera *cam );
+entity_call_result ent_water_call( world_instance *world, ent_call *call );
+bool world_water_player_safe( world_instance *world, f32 allowance );
diff --git a/steam.c b/steam.c
deleted file mode 100644 (file)
index f28b8e7..0000000
--- a/steam.c
+++ /dev/null
@@ -1,303 +0,0 @@
-#include "vg/vg_steam.h"
-#include "vg/vg_steam_utils.h"
-#include "vg/vg_steam_networking.h"
-#include "vg/vg_steam_auth.h"
-#include "vg/vg_steam_http.h"
-#include "vg/vg_steam_friends.h"
-#include "vg/vg_steam_user_stats.h"
-#include "submodules/anyascii/impl/c/anyascii.c"
-#include "skaterift.h"
-#include <string.h>
-
-/*
- * We only want to use steamworks if building for the networked version,
- * theres not much point otherwise. We mainly want steamworks for setting
- * achievements etc.. so that includes our own server too.
- *
- * This file also wraps the functions and interfaces that we want to use to 
- * make them a bit easier to read, since they are the flat API they have very 
- * long names. in non-networked builds they will return default errors or do
- * nothing.
- */
-
-char steam_username_at_startup[128] = "Unassigned";
-
-static void recv_steam_warning( int severity, const char *msg )
-{
-   if( severity == 0 )
-      vg_low( "%s\n", msg );
-   else
-      vg_info( "%s\n", msg );
-}
-
-int steam_ready = 0,
-    steam_stats_ready = 0;
-
-void *hSteamNetworkingSockets, *hSteamUser, *hSteamUserStats;
-static HSteamPipe hSteamClientPipe;
-
-static const char *steam_achievement_names[] = 
-{
-   "ALBERT", "MARC", "JANET", "BERNADETTA",
-   "ROUTE_MPY", "ROUTE_MPG", "ROUTE_MPB", "ROUTE_MPR",
-   "ROUTE_TO", "ROUTE_TC", "CITY_COMPLETE", "MTZERO_SILVER", "MTZERO_GOLD",
-   "80FT"
-};
-
-void steam_store_achievements(void)
-{
-   if( steam_ready && steam_stats_ready ){
-      SteamAPI_ISteamUserStats_StoreStats( hSteamUserStats );
-   }
-}
-
-void update_ach_models(void);
-void steam_set_achievement( const char *name )
-{
-   if( skaterift.demo_mode )
-      return;
-
-   /* hack lol */
-   if( !strcmp(name,"MARC") ) skaterift.achievements |= 0x1;
-   if( !strcmp(name,"ALBERT") ) skaterift.achievements |= 0x2;
-   if( !strcmp(name,"JANET") ) skaterift.achievements |= 0x4;
-   if( !strcmp(name,"BERNADETTA") ) skaterift.achievements |= 0x8;
-   update_ach_models();
-
-   if( steam_ready && steam_stats_ready ){
-      if( SteamAPI_ISteamUserStats_SetAchievement( hSteamUserStats, name ) ){
-         vg_success( "Achievement set! '%s'\n", name );
-
-      }
-      else{
-         vg_warn( "Failed to set achievement: %s\n", name );
-      }
-   }
-   else{
-      vg_warn( "Failed to set achievement (steam not ready): %s\n", name );
-   }
-}
-
-void steam_clear_achievement( const char *name )
-{
-   if( steam_ready && steam_stats_ready ){
-      if( SteamAPI_ISteamUserStats_ClearAchievement( hSteamUserStats, name ) ){
-         vg_info( "Achievement cleared: '%s'\n", name );
-      }
-      else{
-         vg_warn( "Failed to clear achievement: %s\n", name );
-      }
-   }
-   else{
-      vg_warn( "Failed to clear achievement (steam not ready): %s\n", name );
-   }
-}
-
-
-void steam_print_all_achievements(void)
-{
-   vg_info( "Achievements: \n" );
-
-   if( steam_ready && steam_stats_ready ){
-      for( int i=0; i<VG_ARRAY_LEN(steam_achievement_names); i++ ){
-         steamapi_bool set = 0;
-         const char *name = steam_achievement_names[i];
-
-         if( SteamAPI_ISteamUserStats_GetAchievement( 
-                  hSteamUserStats, name, &set ) )
-         {
-            vg_info( "  %s %s\n", (set? "[YES]": "[   ]"), name );
-         }
-         else{
-            vg_warn( "  Error while fetching achievement status '%s'\n", name );
-         }
-      }
-   }
-   else{
-      vg_warn( "  Steam is not initialized, no results\n" );
-   }
-}
-
-int steam_achievement_ccmd( int argc, char const *argv[] )
-{
-   if( !(steam_ready && steam_stats_ready) ) return 1;
-
-   if( argc == 1 ){
-      if( !strcmp( argv[0], "list" ) ){
-         steam_print_all_achievements();
-         return 0;
-      }
-      else if( !strcmp( argv[0], "clearall" )){
-         for( int i=0; i<VG_ARRAY_LEN(steam_achievement_names); i++ )
-            steam_clear_achievement( steam_achievement_names[i] );
-         
-         steam_store_achievements();
-      }
-   }
-
-   if( argc == 2 ){
-      if( !strcmp( argv[0], "set" ) ){
-         steam_set_achievement( argv[1] );
-         steam_store_achievements();
-         return 0;
-      }
-      else if( strcmp( argv[0], "clear" ) ){
-         steam_clear_achievement( argv[1] );
-         steam_store_achievements();
-         return 0;
-      }
-   }
-
-   return 1;
-}
-
-static void steam_on_recieve_current_stats( CallbackMsg_t *msg )
-{
-   UserStatsReceived_t *rec = (UserStatsReceived_t *)msg->m_pubParam;
-
-   if( rec->m_eResult == k_EResultOK ){
-      vg_info( "Recieved stats for: %lu (user: %lu)\n", rec->m_nGameID,
-                                                        rec->m_steamIDUser );
-      steam_stats_ready = 1;
-
-      steamapi_bool set = 0;
-      if( SteamAPI_ISteamUserStats_GetAchievement( 
-               hSteamUserStats, "MARC", &set ) ){
-         if( set ) skaterift.achievements |= 0x1;
-      }
-      if( SteamAPI_ISteamUserStats_GetAchievement( 
-               hSteamUserStats, "ALBERT", &set ) ){
-         if( set ) skaterift.achievements |= 0x2;
-      }
-      if( SteamAPI_ISteamUserStats_GetAchievement( 
-               hSteamUserStats, "JANET", &set ) ){
-         if( set ) skaterift.achievements |= 0x4;
-      }
-      if( SteamAPI_ISteamUserStats_GetAchievement( 
-               hSteamUserStats, "BERNADETTA", &set ) ){
-         if( set ) skaterift.achievements |= 0x8;
-      }
-      update_ach_models();
-   }
-   else{
-      vg_error( "Error recieveing stats for user (%u)\n", rec->m_eResult );
-   }
-}
-
-static u32 utf8_byte0_byte_count( u8 char0 )
-{
-   for( u32 k=2; k<4; k++ ){
-      if( !(char0 & (0x80 >> k)) )
-         return k;
-   }
-
-   return 0;
-}
-
-u32 str_utf8_collapse( const char *str, char *buf, u32 length )
-{
-   u8 *ustr = (u8 *)str;
-   u32 utf32_code = 0x00000000;
-   u32 i=0, j=0, utf32_byte_ct=0;
-
-   for(;j < length-1;){
-      if( ustr[i] == 0x00 )
-         break;
-      
-      if( ustr[i] & 0x80 ){
-         if( utf32_byte_ct ){
-            utf32_byte_ct --;
-            utf32_code |= (ustr[i] & 0x3F) << (utf32_byte_ct*6);
-
-            if( !utf32_byte_ct ){
-               const char *match;
-               size_t chars = anyascii( utf32_code, &match );
-
-               for( u32 k=0; k<VG_MIN(chars, length-1-j); k++ ){
-                  buf[ j++ ] = (u8)match[k];
-               }
-            }
-         }
-         else{
-            utf32_byte_ct = utf8_byte0_byte_count( ustr[i] )-1;
-            utf32_code = ustr[i] & (0x3F >> utf32_byte_ct);
-            utf32_code <<= utf32_byte_ct*6;
-         }
-      }
-      else{
-         utf32_byte_ct = 0x00;
-         buf[j ++] = str[i];
-      }
-
-      i++;
-   }
-
-   buf[j] = 0x00;
-   return j;
-}
-
-int steam_init(void)
-{
-   const char *username = "offline player";
-
-   vg_info( "Initializing steamworks\n" );
-
-   if( !SteamAPI_Init() ){
-      printf("\n");
-      vg_error( "Steamworks failed to initialize\n" );
-      return 1;
-   }
-
-   steam_ready = 1;
-
-   SteamAPI_ManualDispatch_Init();
-
-   /* Connect interfaces */
-   hSteamClientPipe = SteamAPI_GetHSteamPipe();
-   hSteamNetworkingSockets = SteamAPI_SteamNetworkingSockets_SteamAPI();
-   hSteamUser = SteamAPI_SteamUser();
-
-   ISteamUtils *utils = SteamAPI_SteamUtils();
-   SteamAPI_ISteamUtils_SetWarningMessageHook( utils, recv_steam_warning );
-
-   printf("\n");
-   vg_success( "\nSteamworks API running\n" );
-
-   ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
-   username = SteamAPI_ISteamFriends_GetPersonaName( hSteamFriends );
-
-   /*
-    * Request stats
-    * --------------------------------------------------------
-    */
-   hSteamUserStats = SteamAPI_SteamUserStats();
-   steam_register_callback( k_iUserStatsReceived,
-                            steam_on_recieve_current_stats );
-
-   if( !SteamAPI_ISteamUserStats_RequestCurrentStats( hSteamUserStats ) )
-      vg_warn( "No Steam Logon: Cannot request stats\n" );
-
-
-   vg_console_reg_cmd( "ach", steam_achievement_ccmd, NULL );
-
-   /* TODO: On username update callback */
-   str_utf8_collapse( username, steam_username_at_startup, 
-                        VG_ARRAY_LEN(steam_username_at_startup) );
-
-   return 1;
-}
-
-void steam_update(void)
-{
-   if( steam_ready ){
-      steamworks_event_loop( hSteamClientPipe );
-   }
-}
-
-void steam_end(void)
-{
-   if( steam_ready ){
-      vg_info( "Shutting down\n..." );
-      SteamAPI_Shutdown();
-   }
-}
diff --git a/steam.h b/steam.h
deleted file mode 100644 (file)
index e2ed982..0000000
--- a/steam.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- * All trademarks are property of their respective owners
- */
-#pragma once
-
-extern int steam_ready, steam_stats_ready;
-extern void *hSteamNetworkingSockets, *hSteamUser, *hSteamUserStats;
-extern char steam_username_at_startup[128];
-
-int steam_init(void);
-void steam_update(void);
-void steam_end(void);
-u32 str_utf8_collapse( const char *str, char *buf, u32 length );
-int steam_achievement_ccmd( int argc, char const *argv[] );
-void steam_print_all_achievements(void);
-void steam_clear_achievement( const char *name );
-void steam_set_achievement( const char *name );
-void steam_store_achievements(void);
diff --git a/traffic.h b/traffic.h
deleted file mode 100644 (file)
index 004c624..0000000
--- a/traffic.h
+++ /dev/null
@@ -1,222 +0,0 @@
-#ifndef TRAFFIC_H
-#define TRAFFIC_H
-
-#include "common.h"
-#include "model.h"
-#include "rigidbody.h"
-#include "world.h"
-
-typedef struct traffic_node traffic_node;
-typedef struct traffic_driver traffic_driver;
-
-struct traffic_node
-{
-   v3f co, h;
-
-   union
-   {
-      struct{ traffic_node *next, *next1; };
-      struct{ mdl_node *mn_next, *mn_next1; };
-   };
-};
-
-struct traffic_driver
-{
-   m4x3f transform;
-
-   traffic_node *current;
-   int option;
-   float t, speed;
-};
-
-static float eval_bezier_length( v3f p0, v3f p1, v3f h0, v3f h1, int res )
-{
-   float length = 0.0f, m = 1.0f/(float)res;
-   v3f l, p;
-   v3_copy( p0, l );
-
-   for( int i=0; i<res; i++ )
-   {
-      float t = (float)(i+1)*m;
-      eval_bezier_time(p0,p1,h0,h1,t,p);
-      length += v3_dist( p,l );
-      v3_copy( p, l );
-   }
-
-   return length;
-}
-
-static void traffic_finalize( traffic_node *system, int count )
-{
-   for( int i=0; i<count; i++ )
-   {
-      traffic_node *tn = &system[i];
-
-      if( tn->mn_next )
-         tn->next = &system[ tn->mn_next->sub_uid ];
-      if( tn->mn_next1 )
-         tn->next1 = &system[ tn->mn_next1->sub_uid ];
-   }
-}
-
-static void traffic_visualize_link( traffic_node *ta, traffic_node *tb )
-{
-   v3f p0, p1, h0, h1, p, l;
-
-   if( !tb ) return;
-
-   v3_copy( ta->co, p0 );
-   v3_muladds( ta->co, ta->h,  1.0f, h0 );
-   v3_copy( tb->co, p1 );
-   v3_muladds( tb->co, tb->h, -1.0f, h1 );
-   v3_copy( p0, l );
-
-   vg_line_pt3( h0, 0.2f, 0xff00ff00 );
-   vg_line_pt3( h1, 0.2f, 0xffff00ff );
-   vg_line( p0, h0, 0xff000000 );
-   vg_line( p1, h1, 0xff000000 );
-
-   for( int i=0; i<5; i++ )
-   {
-      float t = (float)(i+1)/5.0f;
-      eval_bezier_time( p0, p1, h0, h1, t, p );
-
-      vg_line( p, l, 0xffffffff );
-      v3_copy( p, l );
-   }
-}
-
-static void sample_wheel_floor( v3f pos )
-{
-   v3f ground;
-   v3_copy( pos, ground );
-   ground[1] += 4.0f;
-   
-   ray_hit hit;
-   hit.dist = 8.0f;
-
-   if( ray_world( ground, (v3f){0.0f,-1.0f,0.0f}, &hit ))
-   {
-      v3_copy( hit.pos, pos );
-   }
-}
-
-static void traffic_drive( traffic_driver *driver )
-{
-   traffic_node *next, *current = driver->current;
-
-   if( !current ) return;
-   next = driver->option==0? current->next: current->next1;
-   
-   if( driver->t > 1.0f )
-   {
-      driver->t = driver->t - floorf( driver->t );
-      driver->current = driver->option==0? current->next: current->next1;
-      driver->option = 0;
-      
-      current = driver->current;
-      if( !current )
-         return;
-
-      if( current->next && current->next1 )
-         if( vg_randf() > 0.5f )
-            driver->option = 1;
-   }
-
-   traffic_visualize_link( current, next );
-
-   /*
-    * Calculate the speed of the curve at the current point. On the reference
-    * curve the rate should come out to be exactly 1 ktimestep traveled.
-    * Dividing this distance by ktimestep gives us the modifier to use.
-    */
-   v3f p0,p1,h0,h1,pc,pn;
-   
-   v3_copy( current->co, p0 );
-   v3_muladds( current->co, current->h, 1.0f, h0 );
-   v3_copy( next->co, p1 );
-   v3_muladds( next->co, next->h, -1.0f, h1 );
-
-   eval_bezier_time( p0,p1,h0,h1, driver->t, pc );
-   eval_bezier_time( p0,p1,h0,h1, driver->t + vg.time_delta, pn );
-
-   float mod = vg.time_delta / v3_dist( pc, pn );
-   v3f dir,side,up;
-   v3_sub( pn, pc, dir );
-   v3_normalize(dir);
-   
-   /*
-    * Stick the car on the ground by casting rays where the wheels are
-    */
-   side[0] = -dir[2];
-   side[1] =  0.0f;
-   side[2] =  dir[0];
-   v3_normalize(side);
-
-   v3f fl, fr, bc;
-   v3_muladds( pc, dir, 2.0f, fr );
-   v3_muladds( pc, dir, 2.0f, fl );
-   v3_muladds( pc, dir, -2.0f, bc );
-   v3_muladds( fr, side, 1.0f, fr );
-   v3_muladds( fl, side, -1.0f, fl );
-
-   sample_wheel_floor( fl );
-   sample_wheel_floor( fr );
-   sample_wheel_floor( bc );
-
-   vg_line( fl, fr, 0xff00ffff );
-   vg_line( fr, bc, 0xff00ffff );
-   vg_line( bc, fl, 0xff00ffff );
-
-   v3f norm;
-   v3f v0, v1;
-   v3_sub( fr, fl, v0 );
-   v3_sub( bc, fl, v1 );
-   v3_cross( v1, v0, norm );
-   v3_normalize( norm );
-
-   /* 
-    * Jesus take the wheel
-    */
-   float steer_penalty = 1.0f-v3_dot( dir, driver->transform[0] );
-   steer_penalty /= vg.time_delta;
-   steer_penalty *= 30.0f;
-   
-   float target_speed = vg_maxf( 16.0f * (1.0f-steer_penalty), 0.1f ),
-         accel = target_speed - driver->speed;
-   driver->speed = stable_force( driver->speed, accel*vg.time_delta*2.0f );
-   driver->t += driver->speed*mod*vg.time_delta;
-
-   /* 
-    * Update transform
-    */
-   v3_cross( dir, norm, side );
-   v3_copy( dir, driver->transform[0] );
-   v3_copy( norm, driver->transform[1] );
-   v3_copy( side, driver->transform[2] );
-
-   v3_add( fl, fr, pc );
-   v3_add( bc, pc, pc );
-   v3_muls( pc, 1.0f/3.0f, pc );
-   v3_copy( pc, driver->transform[3] );
-}
-
-static void traffic_visualize( traffic_node *system, int count )
-{
-   for( int i=0; i<count; i++ )
-   {
-      traffic_node *tn = &system[i];
-
-      traffic_visualize_link( tn, tn->next );
-      traffic_visualize_link( tn, tn->next1 );
-   }
-}
-
-static void traffic_visualize_car( traffic_driver *driver )
-{
-   vg_line_boxf_transformed( driver->transform, 
-                                       (boxf){{-1.0f,0.0f,-0.5f},
-                                              { 1.0f,0.0f, 0.5f}}, 0xff00ff00 );
-}
-
-#endif /* TRAFFIC_H */
diff --git a/trail.c b/trail.c
deleted file mode 100644 (file)
index 376a009..0000000
--- a/trail.c
+++ /dev/null
@@ -1,187 +0,0 @@
-#pragma once
-#include "vg/vg_engine.h"
-#include "vg/vg_platform.h"
-#include "vg/vg_m.h"
-#include "vg/vg_lines.h"
-#include "vg/vg_async.h"
-#include "vg/vg_camera.h"
-#include "trail.h"
-#include "shaders/particle.h"
-#include "shaders/trail.h"
-
-static void trail_increment( trail_system *sys ){
-   sys->head ++;
-
-   if( sys->head == sys->max )
-      sys->head = 0;
-
-   /* undesirable effect: will remove active points if out of space! */
-   if( sys->count < sys->max )
-      sys->count ++;
-}
-
-void trail_system_update( trail_system *sys, f32 dt,
-                          v3f co, v3f normal, f32 alpha )
-{
-   /* update existing points and clip dead ones */
-   bool clip_allowed = 1;
-   for( i32 i=0; i<sys->count; i ++ ){
-      i32 i0 = sys->head - sys->count + i;
-      if( i0 < 0 ) i0 += sys->max;
-
-      trail_point *p0 = &sys->array[i0];
-      p0->alpha -= dt/sys->lifetime;
-
-      if( clip_allowed ){
-         if( p0->alpha <= 0.0f )
-            sys->count --;
-         else
-            clip_allowed = 0;
-      }
-   }
-
-   i32 icur  = sys->head -1,
-       iprev = sys->head -2,
-       ihead = sys->head;
-
-   if( icur  < 0 ) icur += sys->max;
-   if( iprev < 0 ) iprev += sys->max;
-
-   trail_point *pcur  = &sys->array[ icur ],
-               *pprev = &sys->array[ iprev ],
-               *phead = &sys->array[ ihead ],
-               *pdest = NULL;
-   v3f dir;
-
-   f32 k_min = 0.001f;
-
-   if( sys->count == 0 ){
-      trail_increment( sys );
-      v3_copy( (v3f){0,0,-1}, dir );
-      pdest = phead;
-   }
-   else if( sys->count == 1 ){
-      if( v3_dist2( pcur->co, co ) < k_min*k_min )
-         return;
-
-      trail_increment( sys );
-      pdest = phead;
-      v3_sub( co, pcur->co, dir );
-   }
-   else {
-      if( v3_dist2( pprev->co, co ) < k_min*k_min )
-         return;
-
-      if( v3_dist2( pprev->co, co ) > sys->min_dist*sys->min_dist ){
-         trail_increment( sys );
-         pdest = phead;
-      }
-      else
-         pdest = pcur;
-
-      v3_sub( co, pprev->co, dir );
-   }
-
-   v3_cross( dir, normal, pdest->right );
-   v3_normalize( pdest->right );
-   v3_copy( co, pdest->co );
-   v3_copy( normal, pdest->normal );
-   pdest->alpha = alpha;
-}
-
-void trail_system_debug( trail_system *sys )
-{
-   for( i32 i=0; i<sys->count; i ++ ){
-      i32 i0 = sys->head - sys->count + i;
-      if( i0 < 0 ) i0 += sys->max;
-
-      trail_point *p0 = &sys->array[i0];
-      vg_line_point( p0->co, 0.04f, 0xff000000 | (u32)(p0->alpha*255.0f) );
-      vg_line_arrow( p0->co, p0->right, 0.3f, VG__GREEN );
-
-      if( i == sys->count-1 ) break;
-
-      i32 i1 = i0+1;
-      if( i1 == sys->max ) i1 = 0;
-
-      trail_point *p1 = &sys->array[i1];
-      vg_line( p0->co, p1->co, VG__RED );
-   }
-}
-
-struct trail_init_args {
-   trail_system *sys;
-};
-
-void async_trail_init( void *payload, u32 size )
-{
-   struct trail_init_args *args = payload;
-   trail_system *sys = args->sys;
-
-   glGenVertexArrays( 1, &sys->vao );
-   glGenBuffers( 1, &sys->vbo );
-   glBindVertexArray( sys->vao );
-
-   size_t stride = sizeof(trail_vert);
-
-   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
-   glBufferData( GL_ARRAY_BUFFER, sys->max*stride*2, NULL, GL_DYNAMIC_DRAW );
-
-   /* 0: coordinates */
-   glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, stride, (void*)0 );
-   glEnableVertexAttribArray( 0 );
-}
-
-void trail_alloc( trail_system *sys, u32 max )
-{
-   size_t stride = sizeof(trail_vert);
-   sys->max = max;
-   sys->array = vg_linear_alloc( vg_mem.rtmemory, max*sizeof(trail_point) );
-   sys->vertices = vg_linear_alloc( vg_mem.rtmemory, max*stride*2 );
-
-   vg_async_item *call = vg_async_alloc( sizeof(struct trail_init_args) );
-
-   struct trail_init_args *init = call->payload;
-   init->sys = sys;
-   vg_async_dispatch( call, async_trail_init );
-}
-
-void trail_system_prerender( trail_system *sys )
-{
-   if( sys->count < 2 ) return;
-
-   for( i32 i=0; i<sys->count; i ++ ){
-      i32 i0 = sys->head - sys->count + i;
-      if( i0 < 0 ) i0 += sys->max;
-
-      trail_point *p0 = &sys->array[i0];
-      trail_vert *v0 = &sys->vertices[i*2+0],
-                 *v1 = &sys->vertices[i*2+1];
-
-      v3_muladds( p0->co, p0->right, -sys->width, v0->co );
-      v3_muladds( p0->co, p0->right,  sys->width, v1->co );
-      v0->co[3] = p0->alpha;
-      v1->co[3] = p0->alpha;
-   }
-
-   glBindVertexArray( sys->vao );
-
-   size_t stride = sizeof(trail_vert);
-   glBindBuffer( GL_ARRAY_BUFFER, sys->vbo );
-   glBufferSubData( GL_ARRAY_BUFFER, 0, sys->count*stride*2, sys->vertices );
-}
-
-void trail_system_render( trail_system *sys, vg_camera *cam )
-{
-   if( sys->count < 2 ) return;
-   glDisable( GL_CULL_FACE );
-   glEnable( GL_DEPTH_TEST );
-
-   shader_trail_use();
-   shader_trail_uPv( cam->mtx.pv );
-   shader_trail_uPvPrev( cam->mtx_prev.pv );
-   shader_trail_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
-
-       glBindVertexArray( sys->vao );
-   glDrawArrays( GL_TRIANGLE_STRIP, 0, sys->count*2 );
-}
diff --git a/trail.h b/trail.h
deleted file mode 100644 (file)
index 82c7d60..0000000
--- a/trail.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma once
-
-typedef struct trail_system trail_system;
-typedef struct trail_point trail_point;
-typedef struct trail_vert trail_vert;
-
-struct trail_system {
-   struct trail_point {
-      v3f co, normal, right;
-      f32 alpha;
-   }
-   *array;
-
-#pragma pack(push,1)
-   struct trail_vert {
-      v4f co; /* xyz: position, w: alpha */
-   }
-   *vertices;
-#pragma pack(pop)
-
-   i32 head, count, max;
-   GLuint vao, vbo;
-
-   /* render settings */
-   f32 width, lifetime, min_dist;
-};
-
-void trail_alloc( trail_system *sys, u32 max );
-void trail_system_update( trail_system *sys, f32 dt, v3f co, 
-                          v3f normal, f32 alpha );
-void trail_system_debug( trail_system *sys );
-void trail_system_prerender( trail_system *sys );
-void trail_system_render( trail_system *sys, vg_camera *cam );
diff --git a/vehicle.c b/vehicle.c
deleted file mode 100644 (file)
index c0a3376..0000000
--- a/vehicle.c
+++ /dev/null
@@ -1,279 +0,0 @@
-#include "skaterift.h"
-#include "vehicle.h"
-#include "scene_rigidbody.h"
-
-struct drivable_vehicle gzoomer =
-{
-   .rb.co = {-2000,-2000,-2000}
-};
-
-int spawn_car( int argc, const char *argv[] )
-{
-   v3f ra, rb, rx;
-   v3_copy( g_render.cam.pos, ra );
-   v3_muladds( ra, g_render.cam.transform[2], -10.0f, rb );
-
-   float t;
-   if( spherecast_world( world_current_instance(), 
-                         ra, rb, 1.0f, &t, rx, 0 ) != -1 )
-   {
-      v3_lerp( ra, rb, t, gzoomer.rb.co );
-      gzoomer.rb.co[1] += 4.0f;
-      q_axis_angle( gzoomer.rb.q, (v3f){1.0f,0.0f,0.0f}, 0.001f );
-      v3_zero( gzoomer.rb.v );
-      v3_zero( gzoomer.rb.w );
-      
-      rb_update_matrices( &gzoomer.rb );
-      gzoomer.alive = 1;
-
-      vg_success( "Spawned car\n" );
-   }
-   else{
-      vg_error( "Can't spawn here\n" );
-   }
-
-   return 0;
-}
-
-void vehicle_init(void)
-{
-   q_identity( gzoomer.rb.q );
-   v3_zero( gzoomer.rb.w );
-   v3_zero( gzoomer.rb.v );
-   v3_zero( gzoomer.rb.co );
-   rb_setbody_sphere( &gzoomer.rb, 1.0f, 8.0f, 1.0f );
-
-   VG_VAR_F32( k_car_spring,        flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_spring_damp,   flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_spring_length, flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_wheel_radius,  flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_friction_lat,  flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_friction_roll, flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_drive_force,   flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_air_resistance,flags=VG_VAR_PERSISTENT );
-   VG_VAR_F32( k_car_downforce,     flags=VG_VAR_PERSISTENT );
-
-   VG_VAR_I32( gzoomer.inside );
-
-   vg_console_reg_cmd( "spawn_car", spawn_car, NULL );
-
-   v3_copy((v3f){ -1.0f, -0.25f, -1.5f }, gzoomer.wheels_local[0] );
-   v3_copy((v3f){  1.0f, -0.25f, -1.5f }, gzoomer.wheels_local[1] );
-   v3_copy((v3f){ -1.0f, -0.25f,  1.5f }, gzoomer.wheels_local[2] );
-   v3_copy((v3f){  1.0f, -0.25f,  1.5f }, gzoomer.wheels_local[3] );
-}
-
-void vehicle_wheel_force( int index )
-{
-   v3f pa, pb, n;
-   m4x3_mulv( gzoomer.rb.to_world, gzoomer.wheels_local[index], pa );
-   v3_muladds( pa, gzoomer.rb.to_world[1], -k_car_spring_length, pb );
-
-
-#if 1
-   float t;
-   if( spherecast_world( world_current_instance(), pa, pb, 
-                         k_car_wheel_radius, &t, n, 0 ) == -1 )
-   { t = 1.0f;
-   }
-
-#else
-
-   v3f dir;
-   v3_muls( gzoomer.rb.up, -1.0f, dir );
-   
-   ray_hit hit;
-   hit.dist = k_car_spring_length;
-   ray_world( pa, dir, &hit );
-
-   float t = hit.dist / k_car_spring_length;
-
-#endif
-
-   v3f pc;
-   v3_lerp( pa, pb, t, pc );
-
-   m4x3f mtx;
-   m3x3_copy( gzoomer.rb.to_world, mtx );
-   v3_copy( pc, mtx[3] );
-   vg_line_sphere( mtx, k_car_wheel_radius, VG__BLACK );
-   vg_line( pa, pc, VG__WHITE );
-   v3_copy( pc, gzoomer.wheels[index] );
-
-   if( t < 1.0f ){
-      /* spring force */
-      float Fv = (1.0f-t) * k_car_spring*vg.time_fixed_delta;
-      
-      v3f delta;
-      v3_sub( pa, gzoomer.rb.co, delta );
-
-      v3f rv;
-      v3_cross( gzoomer.rb.w, delta, rv );
-      v3_add( gzoomer.rb.v, rv, rv );
-
-      Fv += v3_dot(rv, gzoomer.rb.to_world[1]) 
-               * -k_car_spring_damp*vg.time_fixed_delta;
-
-      /* scale by normal incident */
-      Fv *= v3_dot( n, gzoomer.rb.to_world[1] );
-
-      v3f F;
-      v3_muls( gzoomer.rb.to_world[1], Fv, F );
-      rb_linear_impulse( &gzoomer.rb, delta, F );
-
-      /* friction vectors
-       * -------------------------------------------------------------*/
-      v3f tx, ty;
-      
-      if( index <= 1 )
-         v3_cross( gzoomer.steerv, n, tx );
-      else
-         v3_cross( n, gzoomer.rb.to_world[2], tx );
-      v3_cross( tx, n, ty );
-
-      v3_copy( tx, gzoomer.tangent_vectors[ index ][0] );
-      v3_copy( ty, gzoomer.tangent_vectors[ index ][1] );
-
-      gzoomer.normal_forces[ index ] = Fv;
-      gzoomer.tangent_forces[ index ][0] = 0.0f;
-      gzoomer.tangent_forces[ index ][1] = 0.0f;
-
-      /* orient inverse inertia tensors */
-      v3f raW;
-      m3x3_mulv( gzoomer.rb.to_world, gzoomer.wheels_local[index], raW );
-
-      v3f raCtx, raCtxI, raCty, raCtyI;
-      v3_cross( tx, raW, raCtx );
-      v3_cross( ty, raW, raCty );
-      m3x3_mulv( gzoomer.rb.iIw, raCtx, raCtxI );
-      m3x3_mulv( gzoomer.rb.iIw, raCty, raCtyI );
-
-      gzoomer.tangent_mass[index][0] = gzoomer.rb.inv_mass;
-      gzoomer.tangent_mass[index][0] += v3_dot( raCtx, raCtxI );
-      gzoomer.tangent_mass[index][0] = 1.0f/gzoomer.tangent_mass[index][0];
-      
-      gzoomer.tangent_mass[index][1] = gzoomer.rb.inv_mass;
-      gzoomer.tangent_mass[index][1] += v3_dot( raCty, raCtyI );
-      gzoomer.tangent_mass[index][1] = 1.0f/gzoomer.tangent_mass[index][1];
-
-      /* apply drive force */
-      if( index >= 2 ){
-         v3_muls( ty, -gzoomer.drive * k_car_drive_force 
-                                     * vg.time_fixed_delta, F );
-         rb_linear_impulse( &gzoomer.rb, raW, F );
-      }
-   }
-   else{
-      gzoomer.normal_forces[ index ] = 0.0f;
-      gzoomer.tangent_forces[ index ][0] = 0.0f;
-      gzoomer.tangent_forces[ index ][1] = 0.0f;
-   }
-}
-
-void vehicle_solve_friction(void)
-{
-   rigidbody *rb = &gzoomer.rb;
-   for( int i=0; i<4; i++ ){
-      v3f raW;
-      m3x3_mulv( rb->to_world, gzoomer.wheels_local[i], raW );
-
-      v3f rv;
-      v3_cross( rb->w, raW, rv );
-      v3_add( rb->v, rv, rv );
-
-      float     fx = k_car_friction_lat * gzoomer.normal_forces[i],
-                fy = k_car_friction_roll * gzoomer.normal_forces[i],
-               vtx = v3_dot( rv, gzoomer.tangent_vectors[i][0] ),
-               vty = v3_dot( rv, gzoomer.tangent_vectors[i][1] ),
-           lambdax = gzoomer.tangent_mass[i][0] * -vtx,
-           lambday = gzoomer.tangent_mass[i][1] * -vty;
-      
-      float tempx = gzoomer.tangent_forces[i][0],
-            tempy = gzoomer.tangent_forces[i][1];
-      gzoomer.tangent_forces[i][0] = vg_clampf( tempx + lambdax, -fx, fx );
-      gzoomer.tangent_forces[i][1] = vg_clampf( tempy + lambday, -fy, fy );
-      lambdax = gzoomer.tangent_forces[i][0] - tempx;
-      lambday = gzoomer.tangent_forces[i][1] - tempy;
-
-      v3f impulsex, impulsey;
-      v3_muls( gzoomer.tangent_vectors[i][0], lambdax, impulsex );
-      v3_muls( gzoomer.tangent_vectors[i][1], lambday, impulsey );
-      rb_linear_impulse( rb, raW, impulsex );
-      rb_linear_impulse( rb, raW, impulsey );
-   }
-}
-
-void vehicle_update_fixed(void)
-{
-   if( !gzoomer.alive )
-      return;
-
-   rigidbody *rb = &gzoomer.rb;
-
-   v3_muls( rb->to_world[2], -cosf(gzoomer.steer), gzoomer.steerv );
-   v3_muladds( gzoomer.steerv, rb->to_world[0], 
-               sinf(gzoomer.steer), gzoomer.steerv );
-
-   /* apply air resistance */
-   v3f Fair, Fdown;
-
-   v3_muls( rb->v, -k_car_air_resistance, Fair );
-   v3_muls( rb->to_world[1], -fabsf(v3_dot( rb->v, rb->to_world[2] )) *
-                                 k_car_downforce, Fdown );
-
-   v3_muladds( rb->v, Fair,  vg.time_fixed_delta, rb->v );
-   v3_muladds( rb->v, Fdown, vg.time_fixed_delta, rb->v );
-   
-   for( int i=0; i<4; i++ )
-      vehicle_wheel_force( i );
-
-   rigidbody _null = {0};
-   _null.inv_mass = 0.0f;
-   m3x3_zero( _null.iI );
-
-   rb_ct manifold[64];
-   int len = rb_sphere__scene( rb->to_world, 1.0f, NULL, 
-                               world_current_instance()->geo_bh,
-                               manifold, 0 );
-   for( int j=0; j<len; j++ ){
-      manifold[j].rba = rb;
-      manifold[j].rbb = &_null;
-   }
-   rb_manifold_filter_coplanar( manifold, len, 0.05f );
-
-   if( len > 1 ){
-      rb_manifold_filter_backface( manifold, len );
-      rb_manifold_filter_joint_edges( manifold, len, 0.05f );
-      rb_manifold_filter_pairs( manifold, len, 0.05f );
-   }
-   len = rb_manifold_apply_filtered( manifold, len );
-
-   rb_presolve_contacts( manifold, vg.time_fixed_delta, len );
-   for( int i=0; i<8; i++ ){
-      rb_solve_contacts( manifold, len );
-      vehicle_solve_friction();
-   }
-
-   rb_iter( rb );
-   rb_update_matrices( rb );
-}
-
-void vehicle_update_post(void)
-{
-   if( !gzoomer.alive )
-      return;
-
-   vg_line_sphere( gzoomer.rb.to_world, 1.0f, VG__WHITE );
-
-   /* draw friction vectors */
-   v3f p0, px, py;
-
-   for( int i=0; i<4; i++ ){
-      v3_copy( gzoomer.wheels[i], p0 );
-      v3_muladds( p0, gzoomer.tangent_vectors[i][0], 0.5f, px );
-      v3_muladds( p0, gzoomer.tangent_vectors[i][1], 0.5f, py );
-
-      vg_line( p0, px, VG__RED );
-      vg_line( p0, py, VG__GREEN );
-   }
-}
diff --git a/vehicle.h b/vehicle.h
deleted file mode 100644 (file)
index 3e60bb7..0000000
--- a/vehicle.h
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-#include "vg/vg_rigidbody.h"
-#include "player.h"
-#include "world.h"
-#include "world_physics.h"
-
-static float k_car_spring = 1.0f,
-                k_car_spring_damp = 0.001f,
-                k_car_spring_length = 0.5f,
-                k_car_wheel_radius = 0.2f,
-                k_car_friction_lat = 0.6f,
-                k_car_friction_roll = 0.01f,
-                k_car_drive_force = 1.0f,
-                k_car_air_resistance = 0.1f,
-                k_car_downforce      = 0.5f;
-
-typedef struct drivable_vehicle drivable_vehicle;
-struct drivable_vehicle
-{
-   int alive, inside;
-   rigidbody rb;
-
-   v3f wheels[4];
-
-   float tangent_mass[4][2],
-         normal_forces[4],
-         tangent_forces[4][2];
-
-   float steer, drive;
-   v3f steerv;
-
-   v3f   tangent_vectors[4][2];
-   v3f   wheels_local[4];
-}
-extern gzoomer;
-
-int spawn_car( int argc, const char *argv[] );
-void vehicle_init(void);
-void vehicle_wheel_force( int index );
-void vehicle_solve_friction(void);
-void vehicle_update_fixed(void);
-void vehicle_update_post(void);
diff --git a/workshop.c b/workshop.c
deleted file mode 100644 (file)
index 7814b09..0000000
+++ /dev/null
@@ -1,1638 +0,0 @@
-#include "vg/vg_engine.h"
-#include "vg/vg_tex.h"
-#include "vg/vg_image.h"
-#include "vg/vg_msg.h"
-#include "vg/vg_binstr.h"
-#include "vg/vg_loader.h"
-#include "vg/vg_io.h"
-#include "ent_skateshop.h"
-
-#include "vg/vg_steam_auth.h"
-#include "vg/vg_steam_ugc.h"
-#include "vg/vg_steam_friends.h"
-#include "steam.h"
-#include "workshop.h"
-
-struct workshop_form workshop_form;
-
-static struct ui_enum_opt workshop_form_visibility_opts[] = {
- { k_ERemoteStoragePublishedFileVisibilityPublic,       "Public"       },
- { k_ERemoteStoragePublishedFileVisibilityUnlisted,     "Unlisted"     },
- { k_ERemoteStoragePublishedFileVisibilityFriendsOnly,  "Friends Only" },
- { k_ERemoteStoragePublishedFileVisibilityPrivate,      "Private"      },
-};
-
-static struct ui_enum_opt workshop_form_type_opts[] = {
- { k_addon_type_none,   "None"   },
- { k_addon_type_board,  "Board"  },
- { k_addon_type_world,  "World"  },
- { k_addon_type_player, "Player" },
-};
-
-/* 
- * Close the form and discard UGC query result
- */
-static void workshop_quit_form(void){
-   player_board_unload( &workshop_form.board_model );
-   workshop_form.file_intent = k_workshop_form_file_intent_none;
-
-   if( workshop_form.ugc_query.result == k_EResultOK ){
-      workshop_form.ugc_query.result = k_EResultNone;
-
-      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-      SteamAPI_ISteamUGC_ReleaseQueryUGCRequest( 
-            hSteamUGC, workshop_form.ugc_query.handle );
-   }
-
-   workshop_form.page = k_workshop_form_hidden;
-   workshop_form.op = k_workshop_op_none;
-}
-
-/*
- * Delete all information about the submission
- */
-static void workshop_reset_submission_data(void)
-{
-   workshop_form.submission.file_id = 0; /* assuming id of 0 is none/invalid */
-   workshop_form.submission.description[0] = '\0';
-   workshop_form.submission.title[0] = '\0';
-   workshop_form.submission.author[0] = '\0';
-   workshop_form.submission.submission_type_selection = 
-      k_addon_type_none;
-   workshop_form.submission.type = k_addon_type_none;
-
-   workshop_form.submission.visibility =
-      k_ERemoteStoragePublishedFileVisibilityPublic;
-
-   workshop_form.addon_folder[0] = '\0';
-   player_board_unload( &workshop_form.board_model );
-   workshop_form.file_intent = k_workshop_form_file_intent_none;
-}
-
-
-/*
- * Mostly copies of what it sais on the Steam API documentation
- */
-static const char *workshop_EResult_user_string( EResult result )
-{
-   switch( result ){
-    case k_EResultInsufficientPrivilege:
-     return "Your account is currently restricted from uploading content "
-             "due to a hub ban, account lock, or community ban. You need to "
-             "contact Steam Support to resolve the issue."; 
-    case k_EResultBanned:
-     return "You do not have permission to upload content to this hub "
-             "because you have an active VAC or Game ban.";
-    case k_EResultTimeout:
-     return "The operation took longer than expected, so it was discarded. "
-            "Please try again.";
-    case k_EResultNotLoggedOn:
-     return "You are currently not logged into Steam.";
-    case k_EResultServiceUnavailable:
-     return "The workshop server is having issues or is unavailable, "
-            "please try again.";
-    case k_EResultInvalidParam:
-     return "One of the submission fields contains something not being " 
-            "accepted by that field.";
-    case k_EResultAccessDenied:
-     return "There was a problem trying to save the title and description. "
-            "Access was denied.";
-    case k_EResultLimitExceeded:
-     return "You have exceeded your Steam Cloud quota. If you wish to "
-            "upload this file, you must remove some published items.";
-    case k_EResultFileNotFound:
-     return "The uploaded file could not be found.";
-    case k_EResultDuplicateRequest:
-     return "The file was already successfully uploaded.";
-    case k_EResultDuplicateName:
-     return "You already have a Steam Workshop item with that name.";
-    case k_EResultServiceReadOnly:
-     return "Due to a recent password or email change, you are not allowed "
-             "to upload new content. Usually this restriction will expire in"
-             " 5 days, but can last up to 30 days if the account has been "
-             "inactive recently.";
-     default: 
-      return "Operation failed for an error which has not been accounted for "
-             "by the programmer. Try again, sorry :)";
-   }
-}
-
-/*
- * op: k_workshop_form_op_publishing_update 
- * ----------------------------------------------------------------------------
- */
-
-/*
- * The endpoint of this operation
- */
-static void on_workshop_update_result( void *data, void *user )
-{
-   vg_info( "Recieved workshop update result\n" );
-   SubmitItemUpdateResult_t *result = data;
-
-   /* this seems to be set here, but my account definitely has accepted it */
-   if( result->m_bUserNeedsToAcceptWorkshopLegalAgreement ){
-      vg_warn( "Workshop agreement currently not accepted\n" );
-   }
-   
-   if( result->m_eResult == k_EResultOK ){
-      workshop_form.page = k_workshop_form_closing_good;
-      workshop_form.failure_or_success_string = "Uploaded workshop file!";
-      vg_success( "file uploaded\n" );
-   }
-   else{
-      workshop_form.page = k_workshop_form_closing_bad;
-      workshop_form.failure_or_success_string = 
-         workshop_EResult_user_string( result->m_eResult );
-
-      vg_error( "Error with the submitted file (%d)\n", result->m_eResult );
-   }
-   workshop_form.op = k_workshop_op_none;
-}
-
-static const char *workshop_filetype_folder(void){
-   enum addon_type type = workshop_form.submission.type;
-   if     ( type == k_addon_type_board )  return "boards/";
-   else if( type == k_addon_type_player ) return "playermodels/";
-   else if( type == k_addon_type_world )  return "maps/";
-
-   return "unknown_addon_type/";
-}
-
-/*
- * reciever on completion of packaging the files, it will then start the item
- * update with Steam API
- */
-static void workshop_form_upload_submission( PublishedFileId_t file_id,
-                                                char *metadata )
-{
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-   UGCUpdateHandle_t handle 
-      = SteamAPI_ISteamUGC_StartItemUpdate( hSteamUGC, SKATERIFT_APPID,
-                                            file_id );
-
-   /* TODO: Handle failure cases for these */
-
-   SteamAPI_ISteamUGC_SetItemMetadata( hSteamUGC, handle, metadata );
-
-   if( workshop_form.submission.submit_title ){
-      vg_info( "Setting title\n" );
-      SteamAPI_ISteamUGC_SetItemTitle( hSteamUGC, handle, 
-                                       workshop_form.submission.title );
-   }
-
-   if( workshop_form.submission.submit_description ){
-      vg_info( "Setting description\n" );
-      SteamAPI_ISteamUGC_SetItemDescription( hSteamUGC, handle, 
-                                       workshop_form.submission.description);
-   }
-
-   if( workshop_form.submission.submit_file_and_image ){
-      char path_buf[4096];
-      vg_str folder;
-      vg_strnull( &folder, path_buf, 4096 );
-      vg_strcat( &folder, vg.base_path );
-
-      vg_strcat( &folder, workshop_filetype_folder() );
-      vg_strcat( &folder, workshop_form.addon_folder );
-
-      vg_info( "Setting item content\n" );
-      SteamAPI_ISteamUGC_SetItemContent( hSteamUGC, handle, folder.buffer );
-      
-      vg_str preview = folder;
-      vg_strcat( &preview, "/preview.jpg" );
-
-      vg_info( "Setting preview image\n" );
-      SteamAPI_ISteamUGC_SetItemPreview( hSteamUGC, handle, preview.buffer );
-   }
-
-   vg_info( "Setting visibility\n" );
-   SteamAPI_ISteamUGC_SetItemVisibility( hSteamUGC, handle, 
-                                 workshop_form.submission.visibility );
-
-   vg_info( "Submitting updates\n" );
-   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
-   call->userdata = NULL;
-   call->p_handler = on_workshop_update_result;
-   call->id = SteamAPI_ISteamUGC_SubmitItemUpdate( hSteamUGC, handle, "" );
-}
-
-/*
- * Steam API call result for when we've created a new item on their network, or 
- * not, if it has failed
- */
-static void on_workshop_createitem( void *data, void *user )
-{
-   CreateItemResult_t *result = data;
-
-   if( result->m_eResult == k_EResultOK ){
-      vg_info( "Created workshop file with id: %lu\n", 
-                result->m_nPublishedFileId );
-
-      if( result->m_bUserNeedsToAcceptWorkshopLegalAgreement ){
-         vg_warn( "Workshop agreement currently not accepted\n" );
-      }
-      
-      workshop_form_upload_submission( result->m_nPublishedFileId, user );
-   }
-   else{
-      const char *errstr = workshop_EResult_user_string( result->m_eResult );
-      
-      if( errstr ){
-         vg_error( "ISteamUGC_CreateItem() failed(%d): '%s' \n", 
-                     result->m_eResult, errstr );
-      }
-
-      workshop_form.page = k_workshop_form_closing_bad;
-      workshop_form.failure_or_success_string = errstr;
-   }
-}
-
-/*
- * Starts the workshop upload process through Steam API
- */
-static void workshop_form_async_submit_begin( void *payload, u32 size )
-{
-
-   /* use existing file */
-   if( workshop_form.submission.file_id ){
-      workshop_form_upload_submission( workshop_form.submission.file_id,
-                                       payload );
-   }
-   else{
-      vg_steam_async_call *call = vg_alloc_async_steam_api_call();
-      call->userdata = payload;
-      call->p_handler = on_workshop_createitem;
-      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-      call->id = SteamAPI_ISteamUGC_CreateItem( hSteamUGC, SKATERIFT_APPID, 
-                                                k_EWorkshopFileTypeCommunity );
-   }
-}
-
-/*
- * Downloads the framebuffer into scratch memory
- */
-static void workshop_form_async_download_image( void *payload, u32 size )
-{
-   int w, h;
-   vg_framebuffer_get_res( g_render.fb_workshop_preview, &w, &h );
-   vg_linear_clear( vg_mem.scratch );
-   workshop_form.img_buffer = vg_linear_alloc( vg_mem.scratch, w*h*3 );
-
-   vg_info( "read framebuffer: glReadPixels( %dx%d )\n", w,h );
-
-   glBindFramebuffer( GL_READ_FRAMEBUFFER, g_render.fb_workshop_preview->id );
-   glReadBuffer( GL_COLOR_ATTACHMENT0 );
-   glReadPixels( 0,0, w,h, GL_RGB, GL_UNSIGNED_BYTE, workshop_form.img_buffer );
-
-   workshop_form.img_w = w;
-   workshop_form.img_h = h;
-}
-
-/*
- * Thread which kicks off the upload process
- */
-static void _workshop_form_submit_thread( void *data )
-{
-   vg_async_call( workshop_form_async_download_image, NULL, 0 );
-   vg_async_stall();
-
-   char path_buf[4096];
-   vg_str folder;
-   vg_strnull( &folder, path_buf, 4096 );
-
-   vg_strcat( &folder, workshop_filetype_folder() );
-   vg_strcat( &folder, workshop_form.addon_folder );
-
-   if( !vg_strgood(&folder) ){
-      vg_error( "addon folder path too long\n" );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   /* 
-    * Create the metadata file
-    * -----------------------------------------------------------------------*/
-   u8 descriptor_buf[ 512 ];
-   vg_msg descriptor;
-   vg_msg_init( &descriptor, descriptor_buf, sizeof(descriptor_buf) );
-   vg_linear_clear( vg_mem.scratch );
-
-   /* short description */
-   vg_msg_frame( &descriptor, "workshop" );
-      vg_msg_wkvstr( &descriptor, "title", workshop_form.submission.title );
-      //vg_msg_wkvstr( &descriptor, "author", "unknown" );
-      vg_msg_wkvnum( &descriptor, "type", k_vg_msg_u32, 1,
-                     &workshop_form.submission.type );
-      vg_msg_wkvstr( &descriptor, "folder", workshop_form.addon_folder );
-   vg_msg_end_frame( &descriptor );
-   //vg_msg_wkvstr( &descriptor, "location", "USA" );
-
-   char *short_descriptor_str = 
-      vg_linear_alloc( vg_mem.scratch, vg_align8(descriptor.cur.co*2+1));
-   vg_bin_str( descriptor_buf, short_descriptor_str, descriptor.cur.co );
-   short_descriptor_str[descriptor.cur.co*2] = '\0';
-   vg_info( "binstr: %s\n", short_descriptor_str );
-
-   vg_dir dir;
-   if( !vg_dir_open( &dir, folder.buffer ) )
-   {
-      vg_error( "could not open addon folder '%s'\n", folder.buffer );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   while( vg_dir_next_entry(&dir) )
-   {
-      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file )
-      {
-         const char *d_name = vg_dir_entry_name(&dir);
-         if( d_name[0] == '.' ) continue;
-
-         vg_str file = folder;
-         vg_strcat( &file, "/" );
-         vg_strcat( &file, d_name );
-         if( !vg_strgood( &file ) ) continue;
-
-         char *ext = vg_strch( &file, '.' );
-         if( !ext ) continue;
-         if( strcmp(ext,".mdl") ) continue;
-
-         vg_msg_wkvstr( &descriptor, "content", d_name );
-         break;
-      }
-   }
-   vg_dir_close(&dir);
-
-   vg_str descriptor_file = folder;
-   vg_strcat( &descriptor_file, "/addon.inf" );
-   if( !vg_strgood(&descriptor_file) ){
-      vg_error( "Addon info path too long\n" );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-   
-   FILE *fp = fopen( descriptor_file.buffer, "wb" );
-   if( !fp ){
-      vg_error( "Could not open addon info file '%s'\n", 
-                descriptor_file.buffer );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-   fwrite( descriptor_buf, descriptor.cur.co, 1, fp );
-   fclose( fp );
-
-   /* Save the preview 
-    * -----------------------------------------------------------------------*/
-   vg_str preview = folder;
-   vg_strcat( &preview, "/preview.jpg" );
-
-   if( !vg_strgood(&preview) ){
-      vg_error( "preview image path too long\n" );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   int w = workshop_form.img_w,
-       h = workshop_form.img_h;
-
-   vg_info( "writing: %s (%dx%d @90%%)\n", preview.buffer, w,h );
-   stbi_flip_vertically_on_write(1);
-   stbi_write_jpg( preview.buffer, w,h, 3, workshop_form.img_buffer, 90 );
-
-   vg_async_call( workshop_form_async_submit_begin, short_descriptor_str, 0 );
-}
-
-/*
- * Entry point for the publishing submission operation
- */
-static void workshop_op_submit( ui_context *ctx )
-{
-   /* TODO: Show these errors to the user */
-   if( workshop_form.submission.submit_title )
-   {
-      if( !workshop_form.submission.title[0] )
-      {
-         ui_start_modal( ctx, "Cannot submit because a title is required\n",
-                         UI_MODAL_WARN );
-         workshop_form.op = k_workshop_op_none;
-         return;
-      }
-   }
-
-   if( workshop_form.submission.submit_description )
-   {
-      if( !workshop_form.submission.description[0] )
-      {
-         ui_start_modal( ctx, 
-                         "Cannot submit because a description is required\n",
-                         UI_MODAL_WARN );
-         workshop_form.op = k_workshop_op_none;
-         return;
-      }
-   }
-
-   if( workshop_form.submission.submit_file_and_image )
-   {
-      if( workshop_form.file_intent == k_workshop_form_file_intent_none )
-      {
-         ui_start_modal( ctx, "Cannot submit because the file is "
-                         "empty or unspecified\n", UI_MODAL_WARN );
-         workshop_form.op = k_workshop_op_none;
-         return;
-      }
-   }
-
-   player_board_unload( &workshop_form.board_model );
-   workshop_form.file_intent = k_workshop_form_file_intent_none;
-   workshop_form.op = k_workshop_op_publishing_update;
-
-   vg_loader_start( _workshop_form_submit_thread, NULL );
-}
-
-/*
- *  op: k_workshop_form_op_loading_model
- * -----------------------------------------------------------------------------
- */
-
-/*
- * Reciever for completion of the model file load
- */
-static void workshop_form_loadmodel_async_complete( void *payload, u32 size )
-{
-   v2_zero( workshop_form.view_angles );
-   v3_zero( workshop_form.view_offset );
-   workshop_form.view_dist = 1.0f;
-   workshop_form.view_changed = 1;
-   workshop_form.file_intent = k_workshop_form_file_intent_new;
-   
-   vg_success( "workshop async load complete\n" );
-   workshop_form.op = k_workshop_op_none;
-}
-
-/*
- * Reciever for failure to load
- */
-static void workshop_form_loadmodel_async_error( void *payload, u32 size ){
-}
-
-/*
- * Thread which loads the model from the disk 
- */
-static void _workshop_form_load_thread( void *data )
-{
-   char path_buf[4096];
-   vg_str folder;
-   vg_strnull( &folder, path_buf, 4096 );
-
-   vg_strcat( &folder, workshop_filetype_folder() );
-   vg_strcat( &folder, workshop_form.addon_folder );
-
-   if( !vg_strgood(&folder) ){
-      vg_error( "workshop async load failed: path too long\n" );
-      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   vg_dir dir;
-   if( !vg_dir_open( &dir, folder.buffer ) ){
-      vg_error( "workshop async load failed: could not open folder\n" );
-      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   vg_info( "Searching %s for model files\n", folder.buffer );
-
-   int found_mdl = 0;
-   while( vg_dir_next_entry(&dir) ){
-      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file ){
-         const char *d_name = vg_dir_entry_name(&dir);
-         if( d_name[0] == '.' ) continue;
-
-         vg_str file = folder;
-         vg_strcat( &file, "/" );
-         vg_strcat( &file, d_name );
-         if( !vg_strgood( &file ) ) continue;
-
-         char *ext = vg_strch( &file, '.' );
-         if( !ext ) continue;
-         if( strcmp(ext,".mdl") ) continue;
-         found_mdl = 1;
-         break;
-      }
-   }
-   vg_dir_close(&dir);
-
-   if( !found_mdl ){
-      vg_error( "workshop async load failed: no model files found\n" );
-      vg_async_call( workshop_form_loadmodel_async_error, NULL, 0 );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   if( workshop_form.submission.type == k_addon_type_board )
-      player_board_load( &workshop_form.board_model, path_buf );
-   else if( workshop_form.submission.type == k_addon_type_player )
-      player_model_load( &workshop_form.player_model, path_buf );
-
-   vg_async_call( workshop_form_loadmodel_async_complete, NULL, 0 );
-}
-
-/*
- * Entry point for load model operation
- */
-static void workshop_op_load_model( ui_context *ctx )
-{
-   world_instance *world = world_current_instance();
-   workshop_form.view_world = world;
-
-   if( workshop_form.submission.type == k_addon_type_board )
-   {
-      if( mdl_arrcount( &world->ent_swspreview ) )
-      {
-         workshop_form.ptr_ent = mdl_arritm( &world->ent_swspreview, 0 );
-      }
-      else
-      {
-         ui_start_modal( ctx, "There is no ent_swspreview in the level. \n"
-                         "Cannot publish here\n", UI_MODAL_BAD );
-         workshop_form.op = k_workshop_op_none;
-         return;
-      }
-   }
-   else if( workshop_form.submission.type == k_addon_type_player ){}
-   else 
-   {
-      ui_start_modal( ctx, "Don't know how to prepare for this item type. \n"
-                      "Please contact the developers.\n", UI_MODAL_BAD );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   workshop_form.op = k_workshop_op_loading_model;
-   vg_loader_start( _workshop_form_load_thread, NULL );
-}
-
-/*
- * op: k_workshop_form_op_downloading_submission 
- * -----------------------------------------------------------------------------
- */
-
-/*
- * The image has been decoded and is ready to slap into the framebuffer
- */
-static void workshop_form_async_imageload( void *data, u32 len )
-{
-   if( data )
-   {
-      vg_framebuffer_attachment *a = 
-         &g_render.fb_workshop_preview->attachments[0];
-
-      glBindTexture( GL_TEXTURE_2D, a->id );
-      glTexSubImage2D( GL_TEXTURE_2D, 0,0,0,
-                        WORKSHOP_PREVIEW_WIDTH, WORKSHOP_PREVIEW_HEIGHT, 
-                        a->format, a->type, data );
-      stbi_image_free( data );
-      vg_success( "Loaded workshop preview image\n" );
-   }
-   else
-   {
-      snprintf( workshop_form.error_msg, sizeof(workshop_form.error_msg),
-               "Preview image could not be loaded. Reason: %s\n",
-                stbi_failure_reason() );
-      ui_start_modal( &vg_ui.ctx, workshop_form.error_msg, UI_MODAL_BAD );
-   }
-   workshop_form.op = k_workshop_op_none;
-}
-
-/*
- * Load the image located at ./workshop_preview.jpg into our framebuffer
- */
-static void _workshop_load_preview_thread( void *data ){
-   char path_buf[ 4096 ];
-   vg_str path;
-   vg_strnull( &path, path_buf, 4096 );
-   vg_strcat( &path, workshop_filetype_folder() );
-   vg_strcat( &path, workshop_form.addon_folder );
-   vg_strcat( &path, "/preview.jpg" );
-
-   if( vg_strgood( &path ) )
-   {
-      stbi_set_flip_vertically_on_load(1);
-      int x, y, nc;
-      u8 *rgb = stbi_load( path.buffer, &x, &y, &nc, 3 );
-
-      if( rgb )
-      {
-         if( (x == WORKSHOP_PREVIEW_WIDTH) && (y == WORKSHOP_PREVIEW_HEIGHT) )
-         {
-            vg_async_call( workshop_form_async_imageload, rgb, x*y*3 );
-         }
-         else
-         {
-            vg_error( "Resolution does not match framebuffer, so we can't"
-                      " show it\n" );
-            stbi_image_free( rgb );
-            vg_async_call( workshop_form_async_imageload, NULL, 0 );
-         }
-      }
-      else
-      {
-         vg_async_call( workshop_form_async_imageload, NULL, 0 );
-      }
-   }
-   else
-   {
-      vg_async_call( workshop_form_async_imageload, NULL, 0 );
-   }
-}
-
-/*
- * Entry point to view operation
- */
-static void workshop_op_download_and_view_submission( int result_index )
-{
-   workshop_form.op = k_workshop_op_downloading_submission;
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-   ISteamRemoteStorage *hSteamRemoteStorage = SteamAPI_SteamRemoteStorage();
-   SteamUGCDetails_t details;
-   if( SteamAPI_ISteamUGC_GetQueryUGCResult( hSteamUGC, 
-                                             workshop_form.ugc_query.handle,
-                                             result_index,
-                                             &details ) )
-   {
-      workshop_reset_submission_data();
-      workshop_form.submission.submit_description = 0;
-      workshop_form.submission.submit_file_and_image = 0;
-      workshop_form.submission.submit_title = 0;
-
-      u8 metadata_buf[512];
-      char metadata_str[1024+1];
-      int have_meta = SteamAPI_ISteamUGC_GetQueryUGCMetadata( hSteamUGC, 
-                                              workshop_form.ugc_query.handle,
-                                              result_index, metadata_str, 
-                                              1024+1 );
-
-      vg_strncpy( details.m_rgchDescription, 
-                  workshop_form.submission.description, 
-                  VG_ARRAY_LEN( workshop_form.submission.description ),
-                  k_strncpy_always_add_null );
-
-      vg_strncpy( details.m_rgchTitle,
-                  workshop_form.submission.title,
-                  VG_ARRAY_LEN( workshop_form.submission.title ),
-                  k_strncpy_always_add_null );
-
-      snprintf( workshop_form.addon_folder, 
-                 VG_ARRAY_LEN( workshop_form.addon_folder ),
-                 "Steam Cloud ("PRINTF_U64")", details.m_nPublishedFileId );
-
-      workshop_form.submission.file_id = details.m_nPublishedFileId;
-      workshop_form.file_intent = k_workshop_form_file_intent_keep_old;
-      workshop_form.page = k_workshop_form_edit;
-      workshop_form.submission.visibility = details.m_eVisibility;
-      workshop_form.submission.type = k_addon_type_none;
-      workshop_form.submission.submission_type_selection = k_addon_type_none;
-
-      if( have_meta )
-      {
-         u32 len = strlen(metadata_str);
-         vg_info( "Metadata: %s\n", metadata_str );
-         vg_str_bin( metadata_str, metadata_buf, len );
-         vg_msg msg;
-         vg_msg_init( &msg, metadata_buf, len/2 );
-         
-         if( vg_msg_seekframe( &msg, "workshop" ))
-         {
-            u32 type;
-            vg_msg_getkvintg( &msg, "type", k_vg_msg_u32, &type, NULL );
-            workshop_form.submission.type = type;
-            workshop_form.submission.submission_type_selection = type;
-
-            const char *kv_folder = vg_msg_getkvstr( &msg, "folder" );
-            if( kv_folder )
-            {
-               vg_strncpy( kv_folder, workshop_form.addon_folder,
-                           sizeof(workshop_form.addon_folder),
-                           k_strncpy_always_add_null );
-            }
-         }
-      }
-      else
-      {
-         vg_error( "No metadata was returned with this item.\n" );
-      }
-
-      vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
-      glClearColor( 0.2f, 0.0f, 0.0f, 1.0f );
-      glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-      glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-      glViewport( 0,0, vg.window_x, vg.window_y );
-
-      vg_loader_start( _workshop_load_preview_thread, NULL );
-   }
-   else
-   {
-      vg_error( "GetQueryUGCResult: Index out of range\n" );
-      workshop_form.op = k_workshop_op_none;
-   }
-}
-
-/*
- * Regular stuff
- * -----------------------------------------------------------------------------
- */
-
-/*
- * View a page of results on the sidebar
- */
-static void workshop_view_page( int req )
-{
-   if( workshop_form.ugc_query.result != k_EResultOK )
-   {
-      vg_error( "Tried to change page without complete data\n" );
-      workshop_form.op = k_workshop_op_none;
-      return;
-   }
-
-   int page = VG_MAX(VG_MIN(req, workshop_form.view_published_page_count-1),0),
-       start = page * WORKSHOP_VIEW_PER_PAGE,
-       end   = VG_MIN( (page+1) * WORKSHOP_VIEW_PER_PAGE, 
-                       workshop_form.ugc_query.returned_item_count ),
-       count = end-start;
-
-   vg_info( "View page %d\n", page );
-
-   workshop_form.view_published_page_id = page;
-   workshop_form.published_files_list_length = count;
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-
-   for( int i=0; i<count; i ++ )
-   {
-      struct published_file *pfile = &workshop_form.published_files_list[i];
-
-      SteamUGCDetails_t details;
-      if( SteamAPI_ISteamUGC_GetQueryUGCResult( hSteamUGC, 
-                                                workshop_form.ugc_query.handle,
-                                                start+i,
-                                                &details ) )
-      {
-         if( details.m_eResult != k_EResultOK )
-         {
-            snprintf( pfile->title, 80, "Error (%d)", details.m_eResult );
-         }
-         else
-         {
-            vg_strncpy( details.m_rgchTitle, pfile->title, 80,
-                        k_strncpy_always_add_null );
-         }
-
-         pfile->result = details.m_eResult;
-         pfile->result_index = start+i;
-      }
-      else
-      {
-         pfile->result = k_EResultValueOutOfRange;
-         pfile->result_index = -1;
-         snprintf( pfile->title, 80, "Error (invalid index)" );
-      }
-   }
-}
-
-/*
- * Steam API result for when we recieve submitted UGC information about the user
- */
-static void on_workshop_UGCQueryComplete( void *data, void *userdata )
-{
-   SteamUGCQueryCompleted_t *query = data;
-   workshop_form.ugc_query.result = query->m_eResult;
-
-   if( query->m_eResult == k_EResultOK )
-   {
-      if( query->m_unTotalMatchingResults > 50 )
-      {
-         vg_warn( "You have %d items submitted, "
-                  "we can only view the last 50\n" );
-      }
-
-      workshop_form.ugc_query.all_item_count = query->m_unTotalMatchingResults;
-      workshop_form.ugc_query.returned_item_count =
-                                             query->m_unNumResultsReturned;
-
-      workshop_form.ugc_query.handle = query->m_handle;
-      workshop_form.view_published_page_count = 
-         (query->m_unNumResultsReturned+WORKSHOP_VIEW_PER_PAGE-1)/
-            WORKSHOP_VIEW_PER_PAGE;
-      workshop_form.view_published_page_id = 0;
-      workshop_form.published_files_list_length = 0;
-
-      workshop_view_page( 0 );
-   }
-   else
-   {
-      vg_error( "Steam UGCQuery failed (%d)\n", query->m_eResult );
-      workshop_form.view_published_page_count = 0;
-      workshop_form.view_published_page_id = 0;
-      workshop_form.published_files_list_length = 0;
-
-      ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-      SteamAPI_ISteamUGC_ReleaseQueryUGCRequest( hSteamUGC, query->m_handle );
-   }
-}
-
-/*
- * Console command to open the workshop publisher
- */
-int workshop_submit_command( int argc, const char *argv[] )
-{
-   if( !steam_ready )
-   {
-      ui_start_modal( &vg_ui.ctx,
-                      "Steam API is not initialized\n", UI_MODAL_BAD );
-      return 0;
-   }
-
-   workshop_form.page = k_workshop_form_open;
-   workshop_form.view_published_page_count = 0;
-   workshop_form.view_published_page_id = 0;
-   workshop_form.published_files_list_length = 0;
-   workshop_form.ugc_query.result = k_EResultNone;
-
-   vg_steam_async_call *call = vg_alloc_async_steam_api_call();
-
-   ISteamUser *hSteamUser = SteamAPI_SteamUser();
-   CSteamID steamid;
-   steamid.m_unAll64Bits = SteamAPI_ISteamUser_GetSteamID( hSteamUser );
-
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-   call->p_handler = on_workshop_UGCQueryComplete;
-   call->userdata = NULL;
-   UGCQueryHandle_t handle = SteamAPI_ISteamUGC_CreateQueryUserUGCRequest
-              ( 
-                hSteamUGC,
-                steamid.m_comp.m_unAccountID,
-                k_EUserUGCList_Published, 
-                k_EUGCMatchingUGCType_Items,
-                k_EUserUGCListSortOrder_CreationOrderDesc,
-                SKATERIFT_APPID, SKATERIFT_APPID,
-                1 );
-   SteamAPI_ISteamUGC_SetReturnMetadata( hSteamUGC, handle, 1 );
-   call->id = SteamAPI_ISteamUGC_SendQueryUGCRequest( hSteamUGC, handle );
-   return 0;
-}
-
-void workshop_init(void)
-{
-   vg_console_reg_cmd( "workshop_submit", workshop_submit_command, NULL );
-}
-
-static void workshop_render_world_preview(void)
-{
-   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
-
-   glClearColor( 0.0f, 0.0f, 0.3f, 1.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-   glEnable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-
-   render_world( world_current_instance(), &g_render.cam, 0, 0, 1, 1 );
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   glViewport( 0,0, vg.window_x, vg.window_y );
-}
-
-/*
- * Redraw the playermodel into the workshop framebuffer 
- */
-static void workshop_render_player_preview(void)
-{
-   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
-   glClearColor( 0.16f, 0.15f, 0.15f, 1.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-   glEnable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-
-   struct skeleton *sk = &localplayer.skeleton;
-
-   player_pose res;
-   res.type = k_player_pose_type_ik;
-
-   struct skeleton_anim *anim = skeleton_get_anim( sk, "idle_cycle+y" );
-   skeleton_sample_anim( sk, anim, vg.time*0.1f, res.keyframes );
-   q_axis_angle( res.root_q, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
-   v3_zero( res.root_co );
-   res.root_co[1] = 200.0f;
-
-   m4x3f transform;
-   q_m3x3( res.root_q, transform );
-   v3_copy( res.root_co, transform[3] );
-
-   /* TODO: Function. */
-   skeleton_apply_pose( sk, res.keyframes, k_anim_apply_defer_ik, 
-                        localplayer.final_mtx );
-   skeleton_apply_ik_pass( sk, localplayer.final_mtx );
-   skeleton_apply_pose( sk, res.keyframes, k_anim_apply_deffered_only,
-                        localplayer.final_mtx );
-   skeleton_apply_inverses( sk, localplayer.final_mtx );
-   skeleton_apply_transform( sk, transform, localplayer.final_mtx );
-
-   vg_camera cam;
-   v3_copy( (v3f){ 0.0f, 201.7f, 1.2f }, cam.pos );
-   
-   cam.nearz = 0.01f;
-   cam.farz = 100.0f;
-   cam.fov = 57.0f;
-   v3_zero( cam.angles );
-   
-   vg_camera_update_transform( &cam );
-   vg_camera_update_view( &cam );
-   vg_camera_update_projection( &cam );
-   vg_camera_finalize( &cam );
-
-   render_playermodel( &cam, world_current_instance(), 0, 
-                       &workshop_form.player_model, sk, localplayer.final_mtx );
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   glViewport( 0,0, vg.window_x, vg.window_y );
-}
-
-/*
- * Redraw the model file into the workshop framebuffer
- */
-static void workshop_render_board_preview(void)
-{
-   if( !workshop_form.ptr_ent )
-   {
-      return;
-   }
-
-   vg_framebuffer_bind( g_render.fb_workshop_preview, 1.0f );
-
-   glClearColor( 0.0f, 0.0f, 0.3f, 1.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-   glEnable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-
-   ent_swspreview *swsprev = workshop_form.ptr_ent;
-   world_instance *world = workshop_form.view_world;
-
-   ent_camera *ref = mdl_arritm( &world->ent_camera, 
-                                 mdl_entity_id_id(swsprev->id_camera) );
-   ent_marker *display = mdl_arritm( &world->ent_marker,
-                                 mdl_entity_id_id(swsprev->id_display) ),
-              *display1= mdl_arritm( &world->ent_marker,
-                                 mdl_entity_id_id(swsprev->id_display1) );
-
-   v3f baseco;
-   v3_add( display->transform.co, display1->transform.co, baseco );
-   v3_muls( baseco, 0.5f, baseco );
-
-   vg_camera cam;
-   v3f basevector;
-   v3_sub( display->transform.co, ref->transform.co, basevector );
-   float dist = v3_length( basevector );
-
-   v3f baseangles;
-   v3_angles( basevector, baseangles );
-
-   v2_add( workshop_form.view_angles, baseangles, cam.angles );
-   cam.angles[2] = 0.0f;
-
-   float sX = sinf( cam.angles[0] ),
-         cX = cosf( cam.angles[0] ),
-         sY = sinf( cam.angles[1] ),
-         cY = cosf( cam.angles[1] );
-
-   v3f offset = { -sX * cY, sY, cX * cY };
-
-   v3_muladds( display->transform.co, offset, 
-               dist*workshop_form.view_dist, cam.pos );
-
-   cam.pos[0] += -sX*workshop_form.view_offset[2];
-   cam.pos[2] +=  cX*workshop_form.view_offset[2];
-   cam.pos[0] +=  cX*workshop_form.view_offset[0];
-   cam.pos[2] +=  sX*workshop_form.view_offset[0];
-   cam.pos[1] += workshop_form.view_offset[1];
-
-   cam.nearz = 0.01f;
-   cam.farz  = 100.0f;
-   cam.fov   = ref->fov;
-   
-   vg_camera_update_transform( &cam );
-   vg_camera_update_view( &cam );
-   vg_camera_update_projection( &cam );
-   vg_camera_finalize( &cam );
-
-   m4x3f mmdl, mmdl1;
-   mdl_transform_m4x3( &display->transform, mmdl );
-   mdl_transform_m4x3( &display1->transform, mmdl1 );
-
-   /* force update this for nice shadows. its usually set in the world
-    * pre-render step, but that includes timer stuff 
-    */
-   struct player_board *board = &workshop_form.board_model;
-   struct ub_world_lighting *ubo = &world->ub_lighting;
-   v3f vp0, vp1;
-   v3_copy((v3f){0.0f,0.1f, board->truck_positions[0][2]}, vp0 );
-   v3_copy((v3f){0.0f,0.1f, board->truck_positions[1][2]}, vp1 );
-   m4x3_mulv( mmdl1, vp0, ubo->g_board_0 );
-   m4x3_mulv( mmdl1, vp1, ubo->g_board_1 );
-   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
-   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
-                    sizeof(struct ub_world_lighting), &world->ub_lighting );
-
-   render_world( world, &cam, 0, 0, 0, 0 );
-   struct player_board_pose pose = {0};
-   render_board( &cam, world, board, mmdl,  &pose, k_board_shader_entity );
-   render_board( &cam, world, board, mmdl1, &pose, k_board_shader_entity );
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-   glViewport( 0,0, vg.window_x, vg.window_y );
-}
-
-/*
- * ImGUI section for workshop form
- * -----------------------------------------------------------------------------
- */
-
-static void workshop_changed_model_path( ui_context *ctx, char *buf, u32 len )
-{
-   workshop_form.submission.submit_file_and_image = 1;
-}
-
-static void workshop_changed_title( ui_context *ctx, char *buf, u32 len )
-{
-   workshop_form.submission.submit_title = 1;
-}
-
-static void workshop_changed_description( ui_context *ctx, char *buf, u32 len )
-{
-   workshop_form.submission.submit_description = 1;
-}
-
-static void workshop_form_gui_page_undecided( ui_context *ctx, ui_rect content )
-{
-   ui_rect box;
-   rect_copy( content, box );
-   box[3] = 128;
-   box[2] = (box[2]*2)/3;
-   ui_rect_center( content, box );
-
-   ui_rect row;
-   ui_split( box, k_ui_axis_h, 28, 0, row, box );
-   ui_text( ctx, row, 
-            "Select the type of item\n", 1, k_ui_align_middle_center,0);
-   ui_split( box, k_ui_axis_h, 28, 0, row, box );
-   ui_enum( ctx, row, "Type:", workshop_form_type_opts,
-               4, &workshop_form.submission.submission_type_selection );
-   ui_split( box, k_ui_axis_h, 8, 0, row, box );
-   ui_split( box, k_ui_axis_h, 28, 0, row, box );
-
-   ui_rect button_l, button_r;
-   rect_copy( row, button_l );
-   button_l[2] = 128*2;
-   ui_rect_center( row, button_l );
-   ui_split_ratio( button_l, k_ui_axis_v, 0.5f, 2, button_l, button_r );
-
-   enum addon_type type = workshop_form.submission.submission_type_selection;
-   if( type != k_addon_type_none)
-   {
-      if( ui_button_text( ctx, button_l, "OK", 1 ) == 1 )
-      {
-         workshop_form.submission.type = type;
-
-         if( type == k_addon_type_world ){
-            workshop_form.view_changed = 1;
-            workshop_form.file_intent = k_workshop_form_file_intent_new;
-         }
-      }
-   }
-   else
-   {
-      ui_fill( ctx, button_l, ui_colour(ctx,k_ui_bg) );
-      ui_text( ctx, button_l, "OK", 1, k_ui_align_middle_center, 
-               ui_colour(ctx, k_ui_bg+4) );
-   }
-   
-   if( ui_button_text( ctx, button_r, "Cancel", 1 ) == 1 )
-   {
-      workshop_form.page = k_workshop_form_open;
-      workshop_form.file_intent = k_workshop_form_file_intent_none;
-   }
-}
-
-static void workshop_form_gui_draw_preview( ui_context *ctx, ui_rect img_box )
-{
-   enum addon_type type = workshop_form.submission.type;
-   if( workshop_form.file_intent == k_workshop_form_file_intent_keep_old )
-   {
-      ui_image( ctx, 
-                img_box, &g_render.fb_workshop_preview->attachments[0].id );
-   }
-   else if( workshop_form.file_intent == k_workshop_form_file_intent_new )
-   {
-      ui_image( ctx, 
-                img_box, &g_render.fb_workshop_preview->attachments[0].id );
-
-      if( type == k_addon_type_world )
-      {
-         return;
-      }
-
-      int hover  = ui_inside_rect( img_box, ctx->mouse ),
-          target = ui_inside_rect( img_box, ctx->mouse_click );
-      
-      if( ui_click_down(ctx,UI_MOUSE_MIDDLE) && target )
-      {
-         v3_copy( workshop_form.view_offset,
-                  workshop_form.view_offset_begin );
-      }
-      else if( ui_click_down(ctx,UI_MOUSE_LEFT) && target )
-      {
-         v2_copy( workshop_form.view_angles, 
-                  workshop_form.view_angles_begin );
-      }
-
-      if( ui_clicking(ctx,UI_MOUSE_MIDDLE) && target )
-      {
-         v2f delta = { ctx->mouse[0]-ctx->mouse_click[0],
-                       ctx->mouse[1]-ctx->mouse_click[1] };
-
-         float *begin  = workshop_form.view_offset_begin,
-               *offset = workshop_form.view_offset;
-         offset[0] = vg_clampf( begin[0]-delta[0]*0.002f, -1.0f, 1.0f );
-         offset[2] = vg_clampf( begin[2]-delta[1]*0.002f, -1.0f, 1.0f );
-         workshop_form.view_changed = 1;
-      }
-      else if( ui_clicking(ctx,UI_MOUSE_LEFT) && target )
-      {
-         v2f delta = { ctx->mouse[0]-ctx->mouse_click[0],
-                       ctx->mouse[1]-ctx->mouse_click[1] };
-
-         v2f angles;
-         v2_muladds( workshop_form.view_angles_begin, delta, 0.002f, angles);
-
-         float limit = VG_PIf*0.2f;
-
-         angles[0] = vg_clampf( angles[0], -limit, limit );
-         angles[1] = vg_clampf( angles[1], -limit, limit );
-
-         v2_copy( angles, workshop_form.view_angles );
-         workshop_form.view_changed = 1;
-      }
-
-      if( !ui_clicking(ctx,UI_MOUSE_LEFT) && hover )
-      {
-         float zoom = workshop_form.view_dist;
-         zoom += vg.mouse_wheel[1] * -0.07f;
-         zoom = vg_clampf( zoom, 0.4f, 2.0f );
-         
-         if( zoom != workshop_form.view_dist )
-         {
-            workshop_form.view_changed = 1;
-            workshop_form.view_dist = zoom;
-         }
-      }
-   }
-   else
-   {
-      ui_text( ctx, img_box, "No image", 1, k_ui_align_middle_center,
-               ui_colour( ctx, k_ui_orange ) );
-   }
-}
-
-static void workshop_form_gui_edit_page( ui_context *ctx, ui_rect content )
-{
-   enum addon_type type = workshop_form.submission.type;
-
-   if( type == k_addon_type_none )
-   {
-      workshop_form_gui_page_undecided( ctx, content );
-      return;
-   }
-
-   ui_rect image_plane;
-   ui_split( content, k_ui_axis_h, 300, 0, image_plane, content );
-   ui_fill( ctx, image_plane, ui_colour( ctx, k_ui_bg+0 ) );
-
-   ui_rect img_box;
-   ui_fit_item( image_plane, (ui_px[2]){ 3, 2 }, img_box );
-   workshop_form_gui_draw_preview( ctx, img_box );
-
-   /* file path */
-   ui_rect file_button, file_label;
-
-   char buf[128];
-   snprintf( buf, 128, 
-             "Addon folder: skaterift/%s", workshop_filetype_folder() );
-
-   if( type == k_addon_type_world )
-   {
-      struct ui_textbox_callbacks callbacks = 
-      {
-         .change = workshop_changed_model_path
-      };
-      ui_textbox( ctx, content, buf, workshop_form.addon_folder,
-                  VG_ARRAY_LEN(workshop_form.addon_folder), 1, 0, &callbacks );
-   }
-   else
-   {
-      ui_rect file_entry;
-      ui_standard_widget( ctx, content, file_entry, 1 );
-      ui_split( file_entry, k_ui_axis_v, -128, 0, file_entry, file_button );
-
-      if( workshop_form.file_intent != k_workshop_form_file_intent_none )
-      {
-         ui_text( ctx, file_entry, workshop_form.addon_folder, 1, 
-                  k_ui_align_middle_left, ui_colour( ctx, k_ui_fg+4 ) );
-
-         if( ui_button_text( ctx, file_button, "Remove", 1 ) == 1 )
-         {
-            if( type == k_addon_type_board )
-               player_board_unload( &workshop_form.board_model );
-            else if( type == k_addon_type_player )
-               player_model_unload( &workshop_form.player_model );
-
-            workshop_form.file_intent = k_workshop_form_file_intent_none;
-            workshop_form.addon_folder[0] = '\0';
-         }
-      }
-      else
-      {
-         struct ui_textbox_callbacks callbacks = 
-         {
-            .change = workshop_changed_model_path
-         };
-
-         ui_textbox( ctx, file_entry, buf, workshop_form.addon_folder,
-                     VG_ARRAY_LEN(workshop_form.addon_folder), 1,
-                     0, &callbacks );
-
-         if( ui_button_text( ctx, file_button, "Load", 1 ) == 1 )
-         {
-            workshop_op_load_model( ctx );
-         }
-      }
-   }
-
-   const char *str_title = "Title:", *str_desc = "Description:";
-
-   /* title box */
-   {
-      struct ui_textbox_callbacks callbacks = {
-         .change = workshop_changed_title
-      };
-      ui_textbox( ctx, content, str_title, workshop_form.submission.title, 
-                  VG_ARRAY_LEN(workshop_form.submission.title), 1,
-                  0, &callbacks );
-   }
-
-   /* visibility option */
-   {
-      ui_enum( ctx, content, "Visibility:", workshop_form_visibility_opts,
-               4, &workshop_form.submission.visibility );
-   }
-
-   /* description box */
-   {
-      struct ui_textbox_callbacks callbacks = 
-      {
-         .change = workshop_changed_description
-      };
-      ui_textbox( ctx, content, str_desc, workshop_form.submission.description,
-                  VG_ARRAY_LEN(workshop_form.submission.description), 4,
-                  UI_TEXTBOX_MULTILINE|UI_TEXTBOX_WRAP, &callbacks );
-   }
-
-   /* submissionable */
-   ui_rect final_row;
-   ui_split( content, k_ui_axis_h, content[3]-32-8, 0, content, final_row );
-
-   ui_rect submission_center;
-   rect_copy( final_row, submission_center );
-   submission_center[2] = 256;
-   ui_rect_center( final_row, submission_center );
-
-   ui_rect btn_left, btn_right;
-   ui_split_ratio( submission_center, k_ui_axis_v, 0.5f, 8, 
-                   btn_left, btn_right );
-
-   if( ui_button_text( ctx, btn_left, "Publish", 1 ) == 1 )
-   {
-      workshop_op_submit( ctx );
-   }
-   if( ui_button_text( ctx, btn_right, "Cancel", 1 ) == 1 )
-   {
-      workshop_form.page = k_workshop_form_open;
-      player_board_unload( &workshop_form.board_model );
-      workshop_form.file_intent = k_workshop_form_file_intent_none;
-   }
-
-   /* disclaimer */
-   const char *disclaimer_text = 
-      "By submitting this item, you agree to the workshop terms of service";
-
-   ui_rect disclaimer_row, inner, link;
-   ui_split( content, k_ui_axis_h, content[3]-32, 0, content, disclaimer_row );
-
-   ui_px btn_width = 32;
-
-   rect_copy( disclaimer_row, inner );
-   inner[2] = ui_text_line_width( ctx, disclaimer_text ) + btn_width+8;
-
-   ui_rect label;
-   ui_rect_center( disclaimer_row, inner );
-   ui_split( inner, k_ui_axis_v, inner[2]-btn_width, 0, label, btn_right);
-   ui_rect_pad( btn_right, (ui_px[2]){2,2} );
-
-   if( ui_button_text( ctx, btn_right, "\xb2", 2 ) == 1 )
-   {
-      ISteamFriends *hSteamFriends = SteamAPI_SteamFriends();
-      SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage( hSteamFriends,
-            "https://steamcommunity.com/sharedfiles/workshoplegalagreement",
-            k_EActivateGameOverlayToWebPageMode_Default );
-   }
-
-   ui_text( ctx, label, disclaimer_text, 1, k_ui_align_middle_left,
-            ui_colour( ctx, k_ui_fg+4 ) );
-}
-
-static void workshop_form_gui_sidebar( ui_context *ctx, ui_rect sidebar )
-{
-   ui_fill( ctx, sidebar, ui_colour( ctx, k_ui_bg+2 ) );
-
-   ui_rect title;
-   ui_split( sidebar, k_ui_axis_h, 28, 0, title, sidebar );
-
-   if( workshop_form.page == k_workshop_form_edit )
-   {
-      ui_text( ctx, title, "Editing", 1, k_ui_align_middle_center, 0 );
-      ui_split( sidebar, k_ui_axis_h, 28, 0, title, sidebar );
-
-      if( workshop_form.submission.type != k_addon_type_none )
-      {
-         char buf[512];
-         vg_str str;
-         vg_strnull( &str, buf, 512 );
-
-         if( workshop_form.submission.file_id )
-            vg_strcat( &str, "Editing an existing " );
-         else
-            vg_strcat( &str, "Creating a new " );
-
-         if( workshop_form.submission.type == k_addon_type_board )
-            vg_strcat( &str, "skateboard." );
-         else if( workshop_form.submission.type == k_addon_type_world )
-            vg_strcat( &str, "world." );
-         else if( workshop_form.submission.type == k_addon_type_player )
-            vg_strcat( &str, "playermodel." );
-         else
-            vg_strcat( &str, "???." );
-
-         ui_text( ctx, title, buf, 1, k_ui_align_middle_center, 
-                  ui_colour(ctx, k_ui_fg+4) );
-      }
-      return;
-   }
-
-   /* 
-    * sidebar existing entries panel
-    */
-   ui_text( ctx, title, "Your submissions", 1, k_ui_align_middle_center, 0 );
-
-   ui_rect controls, btn_create_new;
-   ui_split( sidebar, k_ui_axis_h,  32, 0, controls, sidebar );
-   ui_split( sidebar, k_ui_axis_h, -32, 0, sidebar, btn_create_new );
-   ui_fill( ctx, controls, ui_colour( ctx, k_ui_bg+1 ) );
-
-   char buf[32];
-   vg_str str;
-   vg_strnull( &str, buf, sizeof(buf) );
-   vg_strcat( &str, "page " );
-   vg_strcati32( &str,  workshop_form.view_published_page_id+1 );
-   vg_strcatch( &str, '/' );
-   vg_strcati32( &str, workshop_form.view_published_page_count );
-
-   ui_rect_pad( controls, (ui_px[2]){0,4} );
-   ui_rect info;
-   ui_split_ratio( controls, k_ui_axis_v, 0.25f, 0, info, controls );
-   ui_text( ctx, info, buf, 1, k_ui_align_middle_center, 0 );
-
-   ui_rect btn_left, btn_right;
-   ui_split_ratio( controls, k_ui_axis_v, 0.5f, 2, btn_left, btn_right );
-            
-   if( ui_button_text( ctx, btn_left, "newer", 1 ) == 1 )
-   {
-      workshop_view_page( workshop_form.view_published_page_id-1 );
-   }
-
-   if( ui_button_text( ctx, btn_right, "older", 1 ) == 1 )
-   {
-      workshop_view_page( workshop_form.view_published_page_count+1 );
-   }
-
-   if( ui_button_text( ctx, btn_create_new, "Create New Item", 1 ) == 1 )
-   {
-      workshop_reset_submission_data();
-      workshop_form.submission.submit_title = 1;
-      workshop_form.submission.submit_description = 1;
-      workshop_form.submission.submit_file_and_image = 1;
-      workshop_form.page = k_workshop_form_edit;
-   }
-
-   for( int i=0; i<workshop_form.published_files_list_length; i++ )
-   {
-      ui_rect item;
-      ui_split( sidebar, k_ui_axis_h, 28, 0, item, sidebar );
-      ui_rect_pad( item, (ui_px[2]){4,4} );
-      
-      struct published_file *pfile = &workshop_form.published_files_list[i];
-      if( ui_button_text( ctx, item, pfile->title, 1 ) == 1 )
-      {
-         if( pfile->result == k_EResultOK )
-         {
-            vg_info( "Select index: %d\n", pfile->result_index );
-            workshop_op_download_and_view_submission( pfile->result_index );
-         }
-         else
-         {
-            vg_warn( "Cannot select that item, result not OK\n" );
-         }
-      }
-   }
-}
-
-void workshop_form_gui( ui_context *ctx )
-{
-   enum workshop_form_page stable_page = workshop_form.page;
-   if( stable_page == k_workshop_form_hidden ) return;
-
-   ui_rect null;
-   ui_rect screen = { 0, 0, vg.window_x, vg.window_y };
-   ui_rect window = { 0, 0, 1000, 700 };
-   ui_rect_center( screen, window );
-   ctx->wants_mouse = 1;
-
-   ui_fill( ctx, window, ui_colour( ctx, k_ui_bg+1 ) );
-   ui_outline( ctx, window, 1, ui_colour( ctx, k_ui_bg+7 ), 0 );
-
-   ui_rect title, panel;
-   ui_split( window, k_ui_axis_h, 28, 0, title, panel );
-   ui_fill( ctx, title, ui_colour( ctx, k_ui_bg+7 ) );
-   ui_text( ctx, title, "Workshop tool", 1, k_ui_align_middle_center, 
-            ui_colourcont( ctx, k_ui_bg+7 ) );
-
-   ui_rect quit_button;
-   ui_split( title, k_ui_axis_v, title[2]-title[3], 2, title, quit_button );
-
-   if( vg_loader_availible() )
-   {
-      if( ui_button_text( ctx, quit_button, "X", 1 ) == 1 )
-      {
-         workshop_quit_form();
-         return;
-      }
-   }
-
-   /* 
-    * temporary operation blinders, we don't yet have a nice way to show the
-    * user that we're doing something uninterruptable, so the code just 
-    * escapes here and we show them a basic string
-    */
-
-   if( workshop_form.op != k_workshop_op_none )
-   {
-      const char *op_string = "The programmer has not bothered to describe "
-                              "the current operation that is running.";
-
-      switch( workshop_form.op )
-      {
-         case k_workshop_op_loading_model:
-            op_string = "Operation in progress: Loading model file.";
-         break;
-         case k_workshop_op_publishing_update:
-            op_string = "Operation in progress: publishing submission update "
-                        "to steam.";
-         break;
-         case k_workshop_op_downloading_submission:
-            op_string = "Operation in progress: downloading existing submission"
-                        " from Steam services.";
-         break;
-         default: break;
-      }
-
-      ui_text( ctx, panel, op_string, 1, k_ui_align_middle_center, 0 );
-      return;
-   }
-
-   /* re draw board preview if need to */
-   if( (stable_page == k_workshop_form_edit) && 
-       workshop_form.view_changed && 
-       workshop_form.file_intent == k_workshop_form_file_intent_new )
-   {
-      enum addon_type type = workshop_form.submission.type;
-      if( type == k_addon_type_board ){
-         workshop_render_board_preview();
-      }
-      else if( type == k_addon_type_world ){
-         vg_success( "Renders world preview\n" );
-         workshop_render_world_preview();
-      }
-      else if( type == k_addon_type_player ){
-         workshop_render_player_preview();
-      }
-
-      workshop_form.view_changed = 0;
-   }
-
-   struct workshop_form *form = &workshop_form;
-
-   ui_rect sidebar, content;
-   ui_split_ratio( panel, k_ui_axis_v, 0.3f, 1, sidebar, content );
-
-   /* content page */
-   ui_rect_pad( content, (ui_px[2]){8,8} );
-
-   if( stable_page == k_workshop_form_edit )
-   {
-      workshop_form_gui_edit_page( ctx, content );
-   }
-   else if( stable_page == k_workshop_form_open )
-   {
-      ui_text( ctx, content, "Nothing selected.", 1, k_ui_align_middle_center,
-               ui_colour( ctx, k_ui_fg+4 ) );
-   }
-   else if( stable_page >= k_workshop_form_cclosing )
-   {
-      ui_rect submission_row;
-      ui_split( content, k_ui_axis_h, content[3]-32-8, 0, content, 
-                   submission_row );
-
-      u32 colour;
-
-      if( stable_page == k_workshop_form_closing_bad )
-         colour = ui_colour( ctx, k_ui_red+k_ui_brighter );
-      else
-         colour = ui_colour( ctx, k_ui_green+k_ui_brighter );
-
-      ui_text( ctx, content, workshop_form.failure_or_success_string, 1, 
-               k_ui_align_middle_center, colour );
-
-      ui_rect submission_center;
-      rect_copy( submission_row, submission_center );
-      submission_center[2] = 128;
-      ui_rect_center( submission_row, submission_center );
-      ui_rect_pad( submission_center, (ui_px[2]){8,8} );
-
-      if( ui_button_text( ctx, submission_center, "OK", 1 ) == 1 )
-      {
-         workshop_form.page = k_workshop_form_open;
-      }
-   }
-
-   workshop_form_gui_sidebar( ctx, sidebar );
-}
-
-/*
- * Some async api stuff
- * -----------------------------------------------------------------------------
- */
-
-void async_workshop_get_filepath( void *data, u32 len )
-{
-   struct async_workshop_filepath_info *info = data;
-
-   u64 _size;
-   u32 _ts;
-
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-   if( !SteamAPI_ISteamUGC_GetItemInstallInfo( hSteamUGC, info->id, &_size,
-                                               info->buf, info->len, &_ts ))
-   {
-      vg_error( "GetItemInstallInfo failed\n" );
-      info->buf[0] = '\0';
-   }
-}
-
-void async_workshop_get_installed_files( void *data, u32 len )
-{
-   struct async_workshop_installed_files_info *info = data;
-
-   ISteamUGC *hSteamUGC = SteamAPI_SteamUGC();
-   u32 count = SteamAPI_ISteamUGC_GetSubscribedItems( hSteamUGC, info->buffer,
-                                                      *info->len );
-
-   vg_info( "Found %u subscribed items\n", count );
-
-   u32 j=0;
-   for( u32 i=0; i<count; i++ ){
-      u32 state = SteamAPI_ISteamUGC_GetItemState( hSteamUGC, info->buffer[i] );
-      if( state & k_EItemStateInstalled ){
-         info->buffer[j ++] = info->buffer[i];
-      }
-   }
-
-   *info->len = j;
-}
diff --git a/workshop.h b/workshop.h
deleted file mode 100644 (file)
index 08776df..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-#pragma once
-#include "addon_types.h"
-#include "vg/vg_steam_remote_storage.h"
-#include "skaterift.h"
-#include "vg/vg_steam_auth.h"
-#include "vg/vg_steam_ugc.h"
-#include "vg/vg_steam_friends.h"
-#include "steam.h"
-#include "ent_skateshop.h"
-
-struct async_workshop_filepath_info{
-   PublishedFileId_t id;
-   char *buf;
-   u32 len;
-};
-
-struct async_workshop_installed_files_info{
-   PublishedFileId_t *buffer;
-   u32 *len; /* inout */
-};
-
-struct async_workshop_metadata_info{
-   struct workshop_file_info *info;
-   const char *path;
-};
-
-
-#define WORKSHOP_VIEW_PER_PAGE 15
-
-struct workshop_form{
-   enum workshop_op {
-      k_workshop_op_none,
-      k_workshop_op_downloading_submission,
-      k_workshop_op_publishing_update,
-      k_workshop_op_loading_model
-   }
-   op;
-
-   struct {
-      char title[80];
-      char description[512];
-      char author[32];
-      i32 submission_type_selection;
-      enum addon_type type;
-
-      PublishedFileId_t file_id; /* 0 if not published yet */
-
-      i32 visibility;
-      int submit_title,       /* set if the respective controls are touched */
-          submit_description,
-          submit_file_and_image;
-   } 
-   submission;
-
-   enum workshop_form_page{
-      k_workshop_form_hidden, 
-      k_workshop_form_open,      /* open but not looking at anything */
-      k_workshop_form_edit,      /* editing a submission */
-      k_workshop_form_cclosing,
-      k_workshop_form_closing_good, /* post upload screen */
-      k_workshop_form_closing_bad,  
-   }
-   page;
-
-   /* model viewer 
-    * -----------------------------
-    */
-
-   char addon_folder[128];
-   struct player_board board_model;
-   struct player_model player_model;
-
-   /* what does the user want to do with the image preview? */
-   enum workshop_form_file_intent{
-      k_workshop_form_file_intent_none,         /* loading probably */
-      k_workshop_form_file_intent_new,          /* board_model is valid */
-      k_workshop_form_file_intent_keep_old      /* just browsing */
-   }
-   file_intent;
-
-   world_instance *view_world;
-   ent_swspreview *ptr_ent;
-   v2f view_angles,
-       view_angles_begin;
-   v3f view_offset,
-       view_offset_begin;
-
-   float view_dist;
-   int view_changed;
-
-   /*
-    * published UGC request
-    * ------------------------------
-    */
-
-   struct {
-      UGCQueryHandle_t handle;
-      EResult result;
-
-      int all_item_count,
-          returned_item_count;
-   }
-   ugc_query;
-
-   /* 
-    * UI information
-    * ------------------------------------------
-    */
-
-   const char *failure_or_success_string;
-   char error_msg[256];
-
-   int img_w, img_h;
-   u8 *img_buffer;
-
-   int view_published_page_count,
-       view_published_page_id;
-
-   struct published_file{
-      EResult result;
-      int result_index;
-      char title[80];
-   }
-   published_files_list[WORKSHOP_VIEW_PER_PAGE];
-   int published_files_list_length;
-}
-extern workshop_form;
-
-void workshop_init(void);
-int workshop_submit_command( int argc, const char *argv[] );
-void async_workshop_get_filepath( void *data, u32 len );
-void async_workshop_get_installed_files( void *data, u32 len );
-void workshop_load_metadata( const char *path,struct workshop_file_info *info );
-void workshop_form_gui( ui_context *ctx );
diff --git a/world.c b/world.c
deleted file mode 100644 (file)
index e6d6c31..0000000
--- a/world.c
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#include "skaterift.h"
-#include "world.h"
-#include "network.h"
-#include "vg/vg_loader.h"
-#include "vg/vg_mem.h"
-#include "save.h"
-#include "player.h"
-#include "ent_traffic.h"
-
-struct world_static world_static;
-
-world_instance *world_current_instance(void)
-{
-   return &world_static.instances[ world_static.active_instance ];
-}
-
-static int skaterift_switch_instance_cmd( int argc, const char *argv[] );
-
-void world_init(void)
-{
-   vg_loader_step( world_render_init, NULL );
-   vg_loader_step( world_sfd_init, NULL );
-   vg_loader_step( world_water_init, NULL );
-   vg_loader_step( world_gates_init, NULL );
-   vg_loader_step( world_routes_init, NULL );
-
-   /* Allocate dynamic world memory arena */
-   u32 max_size = 76*1024*1024;
-   world_static.heap = vg_create_linear_allocator( vg_mem.rtmemory, max_size,
-                                                   VG_MEMORY_SYSTEM );
-
-   vg_console_reg_cmd( "switch_active_instance", 
-                        skaterift_switch_instance_cmd, NULL );
-}
-
-void world_switch_instance( u32 index )
-{
-   localplayer.subsystem = k_player_subsystem_walk;
-
-   if( index >= VG_ARRAY_LEN(world_static.instances) ){
-      vg_error( "Instance ID out of range (%u)\n", index );
-      return;
-   }
-
-   world_instance *new = &world_static.instances[ index ];
-
-   if( new->status != k_world_status_loaded ){
-      vg_error( "Instance is not loaded (%u)\n", index );
-      return;
-   }
-
-   if( skaterift.demo_mode ){
-      if( world_static.instance_addons[index]->flags & ADDON_REG_PREMIUM ){
-         vg_error( "Can't switch to a premium world in the demo version\n" );
-         return;
-      }
-   }
-
-   world_instance *current = 
-      &world_static.instances[ world_static.active_instance ];
-
-   if( index != world_static.active_instance ){
-      v3_copy( localplayer.rb.co, current->player_co );
-      skaterift_autosave(1);
-   }
-
-   v3_copy( new->player_co, localplayer.rb.co );
-
-   world_static.active_instance = index;
-   player__reset();
-}
-
-static int skaterift_switch_instance_cmd( int argc, const char *argv[] )
-{
-   if( argc )
-      world_switch_instance( atoi(argv[0]) );
-   else 
-      vg_info( "switch_active_instance <id>\n" );
-   return 0;
-}
-
-void skaterift_world_get_save_path( enum world_purpose which, char buf[128] )
-{
-   addon_reg *reg = world_static.instance_addons[ which ];
-
-   if( !reg )
-      vg_fatal_error( "Looking up addon for world without one\n" );
-
-   char id[76];
-   addon_alias_uid( &reg->alias, id );
-   snprintf( buf, 128, "savedata/%s.bkv", id );
-}
-
-void world_update( world_instance *world, v3f pos )
-{
-   world_render.sky_time += world_render.sky_rate * vg.time_delta;
-   world_render.sky_rate = vg_lerp( world_render.sky_rate, 
-                                    world_render.sky_target_rate, 
-                                    vg.time_delta * 5.0 );
-
-   world_routes_update_timer_texts( world );
-   world_routes_update( world );
-   ent_traffic_update( world, pos );
-   world_sfd_update( world, pos );
-   world_volumes_update( world, pos );
-}
diff --git a/world.h b/world.h
deleted file mode 100644 (file)
index 3a067db..0000000
--- a/world.h
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-#include "render.h"
-#include "network_msg.h"
-#include "addon.h"
-#include "scene.h"
-
-/* types
- */
-
-enum world_geo_type{
-   k_world_geo_type_solid = 0,
-   k_world_geo_type_nonsolid = 1,
-   k_world_geo_type_water = 2
-};
-
-enum world_purpose{
-   k_world_purpose_invalid = -1,
-   k_world_purpose_hub = 0,
-   k_world_purpose_client = 1,
-   k_world_max
-};
-
-struct leaderboard_cache {
-   enum request_status status;
-   f64 cache_time;
-   u8 *data;
-   u32 data_len;
-};
-
-typedef struct world_instance world_instance;
-
-void skaterift_world_get_save_path( enum world_purpose which, char buf[128] );
-
-/* submodule headers */
-#include "world_entity.h"
-#include "world_gate.h"
-#include "world_gen.h"
-#include "world_info.h"
-#include "world_physics.h"
-#include "world_render.h"
-#include "world_sfd.h"
-#include "world_volumes.h"
-#include "world_water.h"
-#include "world_audio.h"
-#include "world_routes.h"
-#include "world_routes_ui.h"
-
-/* console variables */
-
-static f32   k_day_length            = 30.0f; /* minutes */
-static i32   k_debug_light_indices   = 0,
-             k_debug_light_complexity= 0,
-             k_light_preview         = 0,
-             k_light_editor          = 0;
-
-#define WORLD_SURFACE_HAS_TRAFFIC 0x1
-#define WORLD_SURFACE_HAS_PROPS   0x2
-
-struct world_instance {
-   /* Fixed items
-    * -------------------------------------------------------
-    */
-
-   v4f player_co;
-
-   void *heap;
-   enum world_status{
-      k_world_status_unloaded = 0,
-      k_world_status_loading = 1,
-      k_world_status_loaded = 2,
-      k_world_status_unloading = 3  /* dont spawn sounds and stuff */
-   }
-   status;
-
-   struct{
-      boxf depthbounds;
-      int depth_computed;
-
-      float height;
-      int enabled;
-      v4f plane;
-   }
-   water;
-
-   f64 time;
-   f32 tar_min, tar_max;
-
-   /* STD140 */
-   struct ub_world_lighting{
-      v4f g_cube_min,
-          g_cube_inv_range;
-
-      v4f g_water_plane,
-          g_depth_bounds;
-
-      v4f g_daysky_colour;
-      v4f g_nightsky_colour;
-      v4f g_sunset_colour;
-      v4f g_ambient_colour;
-      v4f g_sunset_ambient;
-      v4f g_sun_colour;
-      v4f g_sun_dir;
-      v4f g_board_0;
-      v4f g_board_1;
-
-      float g_water_fog;
-      float g_time;
-      float g_realtime;
-      float g_shadow_length;
-      float g_shadow_spread;
-
-      float g_time_of_day;
-      float g_day_phase;
-      float g_sunset_phase;
-
-      int g_light_preview;
-      int g_shadow_samples;
-
-      int g_debug_indices;
-      int g_debug_complexity;
-   }
-   ub_lighting;
-   GLuint ubo_lighting;
-   int    ubo_bind_point;
-
-   GLuint tbo_light_entities,
-          tex_light_entities,
-          tex_light_cubes;
-
-   float probabilities[3];
-
-   v3i light_cubes;
-   vg_framebuffer *heightmap;
-
-   /*
-    * Dynamically allocated when world_load is called.
-    *
-    *                  the following arrays index somewhere into this linear 
-    *                  allocator
-    * --------------------------------------------------------------------------
-    */
-
-   /*
-    * Main world .mdl 
-    */
-   mdl_context meta;
-
-   GLuint *textures;
-   u32 texture_count;
-
-   struct world_surface{
-      mdl_material info;
-      mdl_submesh sm_geo,
-                  sm_no_collide;
-      u32 flags;
-      u32 alpha_tex;
-   }
-   * surfaces;
-   u32 surface_count;
-
-   ent_worldinfo info;
-   mdl_array_ptr ent_spawn,
-                 ent_gate,
-                 ent_light,
-                 ent_route_node,
-                 ent_path_index,
-                 ent_checkpoint,
-                 ent_route,
-                 ent_water,
-
-                 ent_audio_clip,
-                 ent_audio,
-                 ent_volume,
-                 ent_traffic,
-                 ent_skateshop,
-                 ent_marker,
-                 ent_camera,
-                 ent_swspreview,
-                 ent_ccmd,
-                 ent_objective,
-                 ent_challenge,
-                 ent_relay,
-                 ent_cubemap,
-                 ent_miniworld,
-                 ent_prop,
-                 ent_region,
-                 ent_glider,
-                 ent_npc;
-
-   enum skybox {
-      k_skybox_default,
-      k_skybox_space
-   } skybox;
-
-   ent_gate *rendering_gate;
-
-   /* logic 
-    * ----------------------------------------------------
-    */
-
-   /* world geometry */
-   scene_context scene_geo,
-                 scene_no_collide,
-                 scene_lines;
-
-   /* spacial mappings */
-   bh_tree *geo_bh,
-           *entity_bh;
-   u32 *entity_list;
-
-   /* graphics */
-   glmesh mesh_route_lines;
-   glmesh mesh_geo, 
-          mesh_no_collide;
-   u32 cubemap_cooldown, cubemap_side;
-
-   /* leaderboards */
-   struct leaderboard_cache *leaderboard_cache;
-
-   /* ui */
-   struct route_ui *routes_ui;
-};
-
-struct world_static {
-   /*
-    * Allocated as system memory
-    * --------------------------------------------------------------------------
-    */
-   void *heap;
-
-   u32 current_run_version;
-   double time, rewind_from, rewind_to, last_use;
-
-   u32 active_trigger_volumes[8];
-   u32 active_trigger_volume_count;
-
-   addon_reg *instance_addons[ k_world_max ];
-   world_instance instances[ k_world_max ];
-
-   enum world_purpose active_instance;
-   u32            focused_entity; /* like skateshop, challenge.. */
-   f32            focus_strength;
-   vg_camera      focus_cam;
-
-   /* challenges */
-   ent_objective *challenge_target;
-   f32 challenge_timer;
-
-   enum world_loader_state{
-      k_world_loader_none,
-      k_world_loader_preload,
-      k_world_loader_load
-   }
-   load_state;
-
-   bool clear_async_op_when_done;
-}
-extern world_static;
-
-struct world_load_args 
-{
-   enum world_purpose purpose;
-   addon_reg *reg;
-};
-
-void world_init(void);
-world_instance *world_current_instance(void);
-void world_switch_instance( u32 index );
-void skaterift_world_load_thread( void *_args );
-void world_update( world_instance *world, v3f pos );
diff --git a/world_audio.c b/world_audio.c
deleted file mode 100644 (file)
index b33d62f..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-#include "audio.h"
-#include "world_audio.h"
-
-/* finds any active playing in world and fades them out, we can only do this 
- * while unloading */
-void world_fadeout_audio( world_instance *world )
-{
-   if( world->status != k_world_status_unloading ){
-      vg_fatal_error( "World status must be set to 'unloading', to fadeout"
-                      " audio.\n" );
-   }
-
-   u8 world_id = (world - world_static.instances) + 1;
-
-   audio_lock();
-   for( u32 i=0; i<AUDIO_CHANNELS; i++ ){
-      audio_channel *ch = &vg_audio.channels[i];
-
-      if( ch->allocated && (ch->world_id == world_id) ){
-         ch = audio_channel_fadeout( ch, 1.0f );
-      }
-   }
-   audio_unlock();
-}
-
-/*
- * Trace out a random point, near the player to try and determine water areas
- */
-enum audio_sprite_type world_audio_sample_sprite_random(v3f origin, v3f output)
-{
-   v3f chance = { (vg_randf64(&vg.rand)-0.5f) * 30.0f, 
-                  8,
-                  (vg_randf64(&vg.rand)-0.5f) * 30.0f };
-   
-   v3f pos;
-   v3_add( chance, origin, pos );
-
-   ray_hit contact;
-   contact.dist = vg_minf( 16.0f, pos[1] );
-
-   world_instance *world = world_current_instance();
-   
-   if( ray_world( world, pos, (v3f){0.0f,-1.0f,0.0f}, &contact, 
-            k_material_flag_ghosts ) ){
-      struct world_surface *mat = ray_hit_surface( world, &contact );
-
-      if( mat->info.surface_prop == k_surface_prop_grass){
-         v3_copy( contact.pos, output );
-         return k_audio_sprite_type_grass;
-      }
-      else{
-         return k_audio_sprite_type_none;
-      }
-   }
-
-   output[0] = pos[0];
-   output[1] = 0.0f;
-   output[2] = pos[2];
-
-   float dist = fabsf(output[1] - origin[1]);
-   
-   if( world->water.enabled && dist<=40.0f && !(world->info.flags&0x2) )
-      return k_audio_sprite_type_water;
-   else
-      return k_audio_sprite_type_none;
-}
-
-void world_audio_sample_distances( v3f co, int *index, float *value )
-{
-   float inr3 = 0.57735027,
-         inr2 = 0.70710678118;
-
-   v3f sample_directions[] = {
-      { -1.0f,  0.0f,  0.0f },
-      {  1.0f,  0.0f,  0.0f },
-      {  0.0f,  0.0f,  1.0f },
-      {  0.0f,  0.0f, -1.0f },
-      {  0.0f,  1.0f,  0.0f },
-      {  0.0f, -1.0f,  0.0f },
-      { -inr3,  inr3,  inr3 },
-      {  inr3,  inr3,  inr3 },
-      { -inr3,  inr3, -inr3 },
-      {  inr3,  inr3, -inr3 },
-      { -inr2,  0.0f,  inr2 },
-      {  inr2,  0.0f,  inr2 },
-      { -inr2,  0.0f, -inr2 },
-      {  inr2,  0.0f, -inr2 },
-   };
-
-   static int si = 0;
-   static float distances[16];
-
-   ray_hit ray;
-   ray.dist = 5.0f;
-
-   v3f rc, rd, ro;
-   v3_copy( sample_directions[ si ], rd );
-   v3_add( co, (v3f){0.0f,1.5f,0.0f}, ro );
-   v3_copy( ro, rc );
-
-   float dist = 200.0f;
-
-   for( int i=0; i<10; i++ ){
-      if( ray_world( world_current_instance(), rc, rd, &ray, 
-                     k_material_flag_ghosts ) ){
-         dist = (float)i*5.0f + ray.dist;
-         break;
-      }
-      else{
-         v3_muladds( rc, rd, ray.dist, rc );
-      }
-   }
-
-   distances[si] = dist;
-
-   if( vg_audio.debug_ui && vg_lines.enabled ){
-      for( int i=0; i<14; i++ ){
-         if( distances[i] != 200.0f ){
-            u32 colours[] = { VG__RED, VG__BLUE, VG__GREEN,
-                              VG__CYAN, VG__YELOW, VG__PINK,
-                              VG__WHITE };
-
-            u32 colour = colours[i%7];
-
-            v3f p1;
-            v3_muladds( ro, sample_directions[i], distances[i], p1 );
-            vg_line( ro, p1, colour );
-            vg_line_point( p1, 0.1f, colour );
-         }
-      }
-   }
-
-   *index = si;
-   *value = dist;
-
-   si ++;
-   if( si >= 14 )
-      si = 0;
-}
diff --git a/world_audio.h b/world_audio.h
deleted file mode 100644 (file)
index 07d66d1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#pragma once
-#include "world.h"
-
-void world_fadeout_audio( world_instance *world );
-void world_audio_sample_distances( v3f co, int *index, float *value );
-enum audio_sprite_type 
-world_audio_sample_sprite_random( v3f origin, v3f output );
diff --git a/world_entity.c b/world_entity.c
deleted file mode 100644 (file)
index b85e6fa..0000000
+++ /dev/null
@@ -1,876 +0,0 @@
-#include "vg/vg_steam.h"
-#include "vg/vg_steam_user_stats.h"
-#include "model.h"
-#include "entity.h"
-#include "world.h"
-#include "world_load.h"
-#include "save.h"
-#include "vg/vg_msg.h"
-#include "menu.h"
-#include "ent_challenge.h"
-#include "ent_skateshop.h"
-#include "ent_route.h"
-#include "ent_traffic.h"
-#include "ent_glider.h"
-#include "ent_region.h"
-#include "ent_npc.h"
-#include "ent_camera.h"
-#include "input.h"
-#include "player_walk.h"
-
-bh_system bh_system_entity_list = 
-{
-   .expand_bound = entity_bh_expand_bound,
-   .item_centroid = entity_bh_centroid,
-   .item_closest = entity_bh_closest,
-   .item_swap = entity_bh_swap,
-   .item_debug = entity_bh_debug,
-   .cast_ray = NULL
-};
-
-void world_entity_set_focus( u32 entity_id )
-{
-   if( world_static.focused_entity )
-   {
-      vg_warn( "Entity %u#%u tried to take focus from %u#%u\n",
-                  mdl_entity_id_type( entity_id ),
-                  mdl_entity_id_id( entity_id ),
-                  mdl_entity_id_type( world_static.focused_entity ),
-                  mdl_entity_id_id( world_static.focused_entity ) );
-      return;
-   }
-
-   world_static.focused_entity = entity_id;
-}
-
-void world_entity_focus_modal(void)
-{
-   localplayer.immobile = 1;
-   menu.disable_open = 1;
-   srinput.state = k_input_state_resume;
-
-   v3_zero( localplayer.rb.v );
-   v3_zero( localplayer.rb.w );
-   player_walk.move_speed = 0.0f;
-   skaterift.activity = k_skaterift_ent_focus;
-}
-
-void world_entity_exit_modal(void)
-{
-   if( skaterift.activity != k_skaterift_ent_focus )
-   {
-      vg_warn( "Entity %u#%u tried to exit modal when we weren't in one\n",
-                  mdl_entity_id_type( world_static.focused_entity ),
-                  mdl_entity_id_id( world_static.focused_entity ) );
-      return;
-   }
-
-   localplayer.immobile = 0;
-   menu.disable_open = 0;
-   srinput.state = k_input_state_resume;
-   skaterift.activity = k_skaterift_default;
-}
-
-void world_entity_clear_focus(void)
-{
-   if( skaterift.activity == k_skaterift_ent_focus )
-   {
-      vg_warn( "Entity %u#%u tried to clear focus before exiting modal\n",
-                  mdl_entity_id_type( world_static.focused_entity ),
-                  mdl_entity_id_id( world_static.focused_entity ) );
-      return;
-   }
-
-   world_static.focused_entity = 0;
-}
-
-void world_entity_focus_camera( world_instance *world, u32 uid )
-{
-   if( mdl_entity_id_type( uid ) == k_ent_camera )
-   {
-      u32 index = mdl_entity_id_id( uid );
-      ent_camera *cam = mdl_arritm( &world->ent_camera, index );
-      ent_camera_unpack( cam, &world_static.focus_cam );
-   }
-   else 
-   {
-      vg_camera_copy( &localplayer.cam, &world_static.focus_cam );
-
-      /* TODO ? */
-      world_static.focus_cam.nearz = localplayer.cam.nearz;
-      world_static.focus_cam.farz = localplayer.cam.farz;
-   }
-}
-
-/* logic preupdate */
-void world_entity_focus_preupdate(void)
-{
-   f32 rate = vg_minf( 1.0f, vg.time_frame_delta * 2.0f );
-   int active = 0;
-   if( skaterift.activity == k_skaterift_ent_focus )
-      active = 1;
-
-   vg_slewf( &world_static.focus_strength, active, 
-             vg.time_frame_delta * (1.0f/0.5f) );
-
-   if( world_static.focused_entity == 0 )
-      return;
-
-   u32 type = mdl_entity_id_type( world_static.focused_entity ),
-       index = mdl_entity_id_id( world_static.focused_entity );
-
-   world_instance *world = world_current_instance();
-
-   static void (*table[])( ent_focus_context *ctx ) =
-   {
-      [ k_ent_skateshop ] = ent_skateshop_preupdate,
-      [ k_ent_challenge ] = ent_challenge_preupdate,
-      [ k_ent_route ] = ent_route_preupdate,
-      [ k_ent_npc ] = ent_npc_preupdate,
-   };
-
-   if( (type > VG_ARRAY_LEN(table)) || (table[type] == NULL) )
-   {
-      vg_fatal_error( "No pre-update method set for entity (%u#%u)\n",
-                      type, index );
-   }
-
-   table[type]( &(ent_focus_context){
-                  .world = world,
-                  .index = index,
-                  .active = active } );
-}
-
-/* additional renderings like text etc.. */
-void world_entity_focus_render(void)
-{
-   world_instance *world = world_current_instance();
-   if( skaterift.activity != k_skaterift_ent_focus ){
-      skateshop_render_nonfocused( world, &g_render.cam );
-      return;
-   }
-
-   u32 type = mdl_entity_id_type( world_static.focused_entity ),
-       index = mdl_entity_id_id( world_static.focused_entity );
-
-   if( type == k_ent_skateshop ){
-      ent_skateshop *skateshop = mdl_arritm( &world->ent_skateshop, index );
-      skateshop_render( skateshop );
-   }
-   else if( type == k_ent_challenge ){}
-   else if( type == k_ent_route ){}
-   else if( type == k_ent_miniworld ){}
-   else if( type == k_ent_npc ){}
-   else {
-      vg_fatal_error( "Programming error\n" );
-   }
-}
-
-void world_gen_entities_init( world_instance *world )
-{
-   /* lights */
-   for( u32 j=0; j<mdl_arrcount(&world->ent_light); j ++ ){
-      ent_light *light = mdl_arritm( &world->ent_light, j );
-
-      m4x3f to_world;
-      q_m3x3( light->transform.q, to_world );
-      v3_copy( light->transform.co, to_world[3] );
-      m4x3_invert_affine( to_world, light->inverse_world );
-
-      light->angle_sin_cos[0] = sinf( light->angle * 0.5f );
-      light->angle_sin_cos[1] = cosf( light->angle * 0.5f );
-   }
-
-   vg_async_call( world_link_gates_async, world, 0 );
-   vg_async_stall();
-
-   /* water */
-   for( u32 j=0; j<mdl_arrcount(&world->ent_water); j++ ){
-      ent_water *water = mdl_arritm( &world->ent_water, j );
-      if( world->water.enabled ){
-         vg_warn( "Multiple water surfaces in level!\n" );
-         break;
-      }
-
-      world->water.enabled = 1;
-      water_set_surface( world, water->transform.co[1] );
-   }
-   
-   /* volumes */
-   for( u32 j=0; j<mdl_arrcount(&world->ent_volume); j++ ){
-      ent_volume *volume = mdl_arritm( &world->ent_volume, j );
-      mdl_transform_m4x3( &volume->transform, volume->to_world );
-      m4x3_invert_full( volume->to_world, volume->to_local );
-   }
-
-   /* audio packs */
-   for( u32 j=0; j<mdl_arrcount(&world->ent_audio); j++ ){
-      ent_audio *audio = mdl_arritm( &world->ent_audio, j );
-
-      for( u32 k=0; k<audio->clip_count; k++ ){
-         ent_audio_clip *clip = mdl_arritm( &world->ent_audio_clip,  
-                                             audio->clip_start+k );
-
-         if( clip->_.file.pack_size ){
-            u32 size = clip->_.file.pack_size,
-                offset = clip->_.file.pack_offset;
-
-            /* embedded files are fine to clear the scratch buffer, only
-             * external audio uses it */
-
-            vg_linear_clear( vg_mem.scratch );
-            void *data = vg_linear_alloc( vg_mem.scratch, 
-                                          clip->_.file.pack_size );
-
-            mdl_fread_pack_file( &world->meta, &clip->_.file, data );
-
-            clip->_.clip.path = NULL;
-            clip->_.clip.flags = audio->flags;
-            clip->_.clip.data = data;
-            clip->_.clip.size = size;
-         }
-         else{
-            clip->_.clip.path = mdl_pstr(&world->meta,clip->_.file.pstr_path);
-            clip->_.clip.flags = audio->flags;
-            clip->_.clip.data = NULL;
-            clip->_.clip.size = 0;
-         }
-
-         audio_clip_load( &clip->_.clip, world->heap );
-      }
-   }
-
-   /* create generic entity hierachy for those who need it */
-   u32 indexed_count = 0;
-   struct {
-      u32 type;
-      mdl_array_ptr *array;
-   }
-   indexables[] = {
-      { k_ent_gate, &world->ent_gate },
-      { k_ent_objective, &world->ent_objective },
-      { k_ent_volume, &world->ent_volume },
-      { k_ent_challenge, &world->ent_challenge },
-      { k_ent_glider, &world->ent_glider },
-      { k_ent_npc, &world->ent_npc }
-   };
-
-   for( u32 i=0; i<VG_ARRAY_LEN(indexables); i++ )
-      indexed_count += mdl_arrcount( indexables[i].array );
-   vg_info( "indexing %u entities\n", indexed_count );
-
-   world->entity_list = vg_linear_alloc( world->heap, 
-                                         vg_align8(indexed_count*sizeof(u32)));
-
-   u32 index=0;
-   for( u32 i=0; i<VG_ARRAY_LEN(indexables); i++ ){
-      u32 type  = indexables[i].type,
-          count = mdl_arrcount( indexables[i].array );
-      
-      for( u32 j=0; j<count; j ++ )
-         world->entity_list[index ++] = mdl_entity_id( type, j );
-   }
-
-   world->entity_bh = bh_create( world->heap, &bh_system_entity_list, world,
-                                 indexed_count, 2 );
-
-   world->tar_min = world->entity_bh->nodes[0].bbx[0][1];
-   world->tar_max = world->entity_bh->nodes[0].bbx[1][1] + 20.0f;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_marker); i++ ){
-      ent_marker *marker = mdl_arritm( &world->ent_marker, i );
-
-      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tar_min" ) )
-         world->tar_min = marker->transform.co[1];
-
-      if( MDL_CONST_PSTREQ( &world->meta, marker->pstr_alias, "tar_max" ) )
-         world->tar_max = marker->transform.co[1];
-   }
-}
-
-ent_spawn *world_find_closest_spawn( world_instance *world, v3f position )
-{
-   ent_spawn *rp = NULL, *r;
-   float min_dist = INFINITY;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ ){
-      r = mdl_arritm( &world->ent_spawn, i );
-      float d = v3_dist2( r->transform.co, position );
-      
-      if( d < min_dist ){
-         min_dist = d;
-         rp = r;
-      }
-   }
-
-   if( !rp ){
-      if( mdl_arrcount(&world->ent_spawn) ){
-         vg_warn( "Invalid distances to spawns.. defaulting to first one.\n" );
-         return mdl_arritm( &world->ent_spawn, 0 );
-      }
-      else{
-         vg_error( "There are no spawns in the level!\n" );
-      }
-   }
-
-   return rp;
-}
-
-ent_spawn *world_find_spawn_by_name( world_instance *world, const char *name )
-{
-   ent_spawn *rp = NULL, *r;
-   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ ){
-      r = mdl_arritm( &world->ent_spawn, i );
-      if( !strcmp( mdl_pstr(&world->meta, r->pstr_name), name ) ){
-         rp = r;
-         break;
-      }
-   }
-
-   if( !rp )
-      vg_warn( "No spawn named '%s'\n", name );
-
-   return rp;
-}
-
-void world_default_spawn_pos( world_instance *world, v3f pos )
-{
-   ent_spawn *rp = world_find_spawn_by_name( world, "start" );
-   if( !rp ) rp = world_find_closest_spawn( world, (v3f){0,0,0} );
-   if( rp )
-      v3_copy( rp->transform.co, pos );
-   else
-   {
-      vg_error( "There are no valid spawns in the world\n" );
-      v3_zero( pos );
-   }
-}
-
-entity_call_result ent_volume_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-
-   if( !volume->target ) 
-      return k_entity_call_result_OK;
-
-   if( call->function == k_ent_function_trigger )
-   {
-      call->id = volume->target;
-
-      if( volume->flags & k_ent_volume_flag_particles )
-      {
-         float *co = alloca( sizeof(float)*3 );
-         co[0] = vg_randf64(&vg.rand)*2.0f-1.0f;
-         co[1] = vg_randf64(&vg.rand)*2.0f-1.0f;
-         co[2] = vg_randf64(&vg.rand)*2.0f-1.0f;
-         m4x3_mulv( volume->to_world, co, co );
-
-         call->function = k_ent_function_particle_spawn;
-         call->data = co;
-         entity_call( world, call );
-      }
-      else
-      {
-         call->function = volume->trigger.event;
-         entity_call( world, call );
-      }
-
-      return k_entity_call_result_OK;
-   }
-   else if( call->function == k_ent_function_trigger_leave )
-   {
-      call->id = volume->target;
-
-      if( volume->flags & k_ent_volume_flag_particles )
-      {
-         vg_warn( "Invalid condition; calling leave on particle volume.\n" );
-      }
-      else
-      {
-         call->function = volume->trigger.event_leave;
-         entity_call( world, call );
-      }
-
-      return k_entity_call_result_OK;
-   }
-
-   return k_entity_call_result_unhandled;
-}
-
-entity_call_result ent_audio_call( world_instance *world, ent_call *call )
-{
-   if( world->status == k_world_status_unloading )
-   {
-      vg_warn( "cannot modify audio while unloading world\n" );
-      return k_entity_call_result_invalid;
-   }
-
-   u8 world_id = (world - world_static.instances) + 1;
-   u32 index = mdl_entity_id_id( call->id );
-   ent_audio *audio = mdl_arritm( &world->ent_audio, index );
-
-   v3f sound_co;
-
-   if( call->function == k_ent_function_particle_spawn )
-   {
-      v3_copy( call->data, sound_co );
-   }
-   else if( call->function == k_ent_function_trigger )
-   {
-      v3_copy( audio->transform.co, sound_co );
-   }
-   else
-      return k_entity_call_result_unhandled;
-
-   float chance = vg_randf64(&vg.rand)*100.0f,
-         bar = 0.0f;
-
-   for( u32 i=0; i<audio->clip_count; i++ ){
-      ent_audio_clip *clip = mdl_arritm( &world->ent_audio_clip, 
-                                          audio->clip_start+i );
-
-      float mod = world->probabilities[ audio->probability_curve ],
-            p   = clip->probability * mod;
-
-      bar += p;
-      if( chance < bar )
-      {
-         audio_lock();
-
-         if( audio->behaviour == k_channel_behaviour_unlimited )
-         {
-            audio_oneshot_3d( &clip->_.clip, sound_co,
-                              audio->transform.s[0],
-                              audio->volume );
-         }
-         else if( audio->behaviour == k_channel_behaviour_discard_if_full )
-         {
-            audio_channel *ch = 
-               audio_get_group_idle_channel( audio->group, 
-                                             audio->max_channels );
-
-            if( ch )
-            {
-               audio_channel_init( ch, &clip->_.clip, audio->flags );
-               audio_channel_group( ch, audio->group );
-               audio_channel_world( ch, world_id );
-               audio_channel_set_spacial( ch, sound_co, audio->transform.s[0] );
-               audio_channel_edit_volume( ch, audio->volume, 1 );
-               ch = audio_relinquish_channel( ch );
-            }
-         }
-         else if( audio->behaviour == k_channel_behaviour_crossfade_if_full)
-         {
-            audio_channel *ch =
-               audio_get_group_idle_channel( audio->group,
-                                             audio->max_channels );
-
-            /* group is full */
-            if( !ch ){
-               audio_channel *existing = 
-                  audio_get_group_first_active_channel( audio->group );
-
-               if( existing ){
-                  if( existing->source == &clip->_.clip ){
-                     audio_unlock();
-                     return k_entity_call_result_OK;
-                  }
-                 
-                  existing->group = 0;
-                  existing = audio_channel_fadeout(existing, audio->crossfade);
-               }
-
-               ch = audio_get_first_idle_channel();
-            }
-
-            if( ch )
-            {
-               audio_channel_init( ch, &clip->_.clip, audio->flags );
-               audio_channel_group( ch, audio->group );
-               audio_channel_world( ch, world_id );
-               audio_channel_fadein( ch, audio->crossfade );
-               ch = audio_relinquish_channel( ch );
-            }
-         }
-
-         audio_unlock();
-         return k_entity_call_result_OK;
-      }
-   }
-   return k_entity_call_result_OK;
-}
-
-
-entity_call_result ent_ccmd_call( world_instance *world, ent_call *call )
-{
-   if( call->function == k_ent_function_trigger )
-   {
-      u32 index = mdl_entity_id_id( call->id );
-      ent_ccmd *ccmd = mdl_arritm( &world->ent_ccmd, index );
-      vg_execute_console_input( mdl_pstr(&world->meta, ccmd->pstr_command), 0 );
-      return k_entity_call_result_OK;
-   }
-   else
-      return k_entity_call_result_unhandled;
-}
-
-/*
- * BVH implementation
- * ----------------------------------------------------------------------------
- */
-
-void entity_bh_expand_bound( void *user, boxf bound, u32 item_index )
-{
-   world_instance *world = user;
-
-   u32 id    = world->entity_list[ item_index ],
-       type  = mdl_entity_id_type( id ),
-       index = mdl_entity_id_id( id );
-
-   if( type == k_ent_gate ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
-      boxf box = {{ -gate->dimensions[0], -gate->dimensions[1], -0.1f },
-                  {  gate->dimensions[0],  gate->dimensions[1],  0.1f }};
-
-      m4x3_expand_aabb_aabb( gate->to_world, bound, box );
-   }
-   else if( type == k_ent_objective ){
-      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-      
-      /* TODO: This might be more work than necessary. could maybe just get
-       *       away with representing them as points */
-
-      boxf box;
-      box_init_inf( box );
-
-      for( u32 i=0; i<objective->submesh_count; i++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs,
-                                       objective->submesh_start+i );
-         box_concat( box, sm->bbx );
-      }
-
-      m4x3f transform;
-      mdl_transform_m4x3( &objective->transform, transform );
-      m4x3_expand_aabb_aabb( transform, bound, box );
-   }
-   else if( type == k_ent_volume ){
-      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-      m4x3_expand_aabb_aabb( volume->to_world, bound,
-                              (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}} );
-   }
-   else if( type == k_ent_challenge ){
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-
-      boxf box = {{-1.2f*0.5f,-0.72f*0.5f,-0.01f*0.5f},
-                  { 1.2f*0.5f, 0.72f*0.5f, 0.01f*0.5f}};
-      m4x3f transform;
-      mdl_transform_m4x3( &challenge->transform, transform );
-      m4x3_expand_aabb_aabb( transform, bound, box );
-   }
-   else if( type == k_ent_glider ){
-      ent_glider *glider = mdl_arritm( &world->ent_glider, index );
-      m4x3f transform;
-      mdl_transform_m4x3( &glider->transform, transform );
-      m4x3_expand_aabb_aabb( transform, bound,
-                            (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}} );
-   }
-   else if( type == k_ent_npc )
-   {
-      ent_npc *npc = mdl_arritm( &world->ent_npc, index );
-      box_addpt( bound, npc->transform.co );
-   }
-   else{
-      vg_fatal_error( "Programming error\n" );
-   }
-}
-
-float entity_bh_centroid( void *user, u32 item_index, int axis )
-{
-   world_instance *world = user;
-
-   u32 id    = world->entity_list[ item_index ],
-       type  = mdl_entity_id_type( id ),
-       index = mdl_entity_id_id( id );
-
-   if( type == k_ent_gate ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
-      return gate->to_world[3][axis];
-   }
-   else if( type == k_ent_objective ){
-      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-      return objective->transform.co[axis];
-   }
-   else if( type == k_ent_volume ){
-      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-      return volume->transform.co[axis];
-   }
-   else if( type == k_ent_challenge )
-   {
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-      return challenge->transform.co[axis];
-   }
-   else if( type == k_ent_glider )
-   {
-      ent_glider *glider = mdl_arritm( &world->ent_glider, index );
-      return glider->transform.co[axis];
-   }
-   else if( type == k_ent_npc )
-   {
-      ent_npc *npc = mdl_arritm( &world->ent_npc, index );
-      return npc->transform.co[axis];
-   }
-   else 
-   {
-      vg_fatal_error( "Programming error\n" );
-      return INFINITY;
-   }
-}
-
-void entity_bh_swap( void *user, u32 ia, u32 ib )
-{
-   world_instance *world = user;
-
-   u32 a = world->entity_list[ ia ],
-       b = world->entity_list[ ib ];
-
-   world->entity_list[ ia ] = b;
-   world->entity_list[ ib ] = a;
-}
-
-void entity_bh_debug( void *user, u32 item_index ){
-   world_instance *world = user;
-
-   u32 id    = world->entity_list[ item_index ],
-       type  = mdl_entity_id_type( id ),
-       index = mdl_entity_id_id( id );
-
-   if( type == k_ent_gate ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
-      boxf box = {{ -gate->dimensions[0], -gate->dimensions[1], -0.1f },
-                  {  gate->dimensions[0],  gate->dimensions[1],  0.1f }};
-      vg_line_boxf_transformed( gate->to_world, box, 0xf000ff00 );
-   }
-   else if( type == k_ent_objective ){
-      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-      boxf box;
-      box_init_inf( box );
-
-      for( u32 i=0; i<objective->submesh_count; i++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs,
-                                       objective->submesh_start+i );
-         box_concat( box, sm->bbx );
-      }
-
-      m4x3f transform;
-      mdl_transform_m4x3( &objective->transform, transform );
-      vg_line_boxf_transformed( transform, box, 0xf000ff00 );
-   }
-   else if( type == k_ent_volume ){
-      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-      vg_line_boxf_transformed( volume->to_world,
-                                (boxf){{-1.0f,-1.0f,-1.0f},{ 1.0f, 1.0f, 1.0f}},
-                                0xf000ff00 );
-   }
-   else if( type == k_ent_challenge ){
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-
-      boxf box = {{-1.2f*0.5f,-0.72f*0.5f,-0.01f*0.5f},
-                  { 1.2f*0.5f, 0.72f*0.5f, 0.01f*0.5f}};
-      m4x3f transform;
-      mdl_transform_m4x3( &challenge->transform, transform );
-      vg_line_boxf_transformed( transform, box, 0xf0ff0000 );
-   }
-   else{
-      vg_fatal_error( "Programming error\n" );
-   }
-}
-
-void update_ach_models(void)
-{
-   world_instance *hub = &world_static.instances[k_world_purpose_hub];
-   if( hub->status != k_world_status_loaded ) return;
-
-   for( u32 i=0; i<mdl_arrcount( &hub->ent_prop ); i ++ ){
-      ent_prop *prop = mdl_arritm( &hub->ent_prop, i );
-      if( prop->flags & 0x2 ){
-         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "MARC" ) )
-            if( skaterift.achievements & 0x1 )
-               prop->flags &= ~0x1;
-         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "ALBERT" ) )
-            if( skaterift.achievements & 0x2 )
-               prop->flags &= ~0x1;
-         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "JANET" ) )
-            if( skaterift.achievements & 0x4 )
-               prop->flags &= ~0x1;
-         if( MDL_CONST_PSTREQ( &hub->meta, prop->pstr_alias, "BERNADETTA" ) )
-            if( skaterift.achievements & 0x8 )
-               prop->flags &= ~0x1;
-      }
-   }
-}
-
-void entity_bh_closest( void *user, u32 item_index, v3f point, v3f closest )
-{
-   world_instance *world = user;
-
-   u32 id    = world->entity_list[ item_index ],
-       type  = mdl_entity_id_type( id ),
-       index = mdl_entity_id_id( id );
-
-   if( type == k_ent_gate ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, index );
-      v3_copy( gate->to_world[3], closest );
-   }
-   else if( type == k_ent_objective ){
-      ent_objective *challenge = mdl_arritm( &world->ent_objective, index );
-      v3_copy( challenge->transform.co, closest );
-   }
-   else if( type == k_ent_volume ){
-      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-      v3_copy( volume->to_world[3], closest );
-   }
-   else if( type == k_ent_challenge ){
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-      v3_copy( challenge->transform.co, closest );
-   }
-   else{
-      vg_fatal_error( "Programming error\n" );
-   }
-}
-
-void world_entity_start( world_instance *world, vg_msg *sav )
-{
-   vg_info( "Start instance %p\n", world );
-
-   world->probabilities[ k_probability_curve_constant ] = 1.0f;
-   for( u32 i=0; i<mdl_arrcount(&world->ent_audio); i++ )
-   {
-      ent_audio *audio = mdl_arritm(&world->ent_audio,i);
-      if( audio->flags & AUDIO_FLAG_AUTO_START )
-      {
-         ent_call call;
-         call.data = NULL;
-         call.function = k_ent_function_trigger;
-         call.id = mdl_entity_id( k_ent_audio, i );
-         entity_call( world, &call );
-      }
-   }
-
-   /* read savedata 
-    * ----------------------------------------------------------------------- */
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ ){
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
-      const char *alias = mdl_pstr( &world->meta, challenge->pstr_alias );
-
-      u32 result;
-      vg_msg_getkvintg( sav, alias, k_vg_msg_u32, &result, NULL );
-
-      if( result ){
-         ent_call call;
-         call.data = NULL;
-         call.function = 0;
-         call.id = mdl_entity_id( k_ent_challenge, i );
-         entity_call( world, &call );
-      }
-   }
-
-   vg_msg routes_block = *sav;
-   if( vg_msg_seekframe( &routes_block, "routes" ) ){
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-
-         vg_msg route_info = routes_block;
-         if( vg_msg_seekframe( &route_info, 
-                               mdl_pstr(&world->meta,route->pstr_name) ) ){
-
-            u32 flags;
-            vg_msg_getkvintg( &route_info, "flags", k_vg_msg_u32,    
-                              &flags, NULL );
-            route->flags |= flags;
-
-            vg_msg_getkvintg( &route_info, "best_laptime", k_vg_msg_f64,
-                              &route->best_laptime, NULL );
-
-            f32 sections[ route->checkpoints_count ];
-            vg_msg_cmd cmd;
-            if( vg_msg_getkvcmd( &route_info, "sections", &cmd ) ){
-               vg_msg_cast( cmd.value, cmd.code, sections,
-                            k_vg_msg_f32 |
-                            vg_msg_count_bits(route->checkpoints_count) );
-            }
-            else{
-               for( u32 j=0; j<route->checkpoints_count; j ++ )
-                  sections[j] = 0.0f;
-            }
-
-            for( u32 j=0; j<route->checkpoints_count; j ++ ){
-               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint,
-                     route->checkpoints_start + j );
-
-               cp->best_time = sections[j];
-            }
-
-            /* LEGACY: check if steam achievements can give us a medal */
-            if( steam_ready && steam_stats_ready ){
-               for( u32 j=0; j<VG_ARRAY_LEN(track_infos); j ++ ){
-                  struct track_info *inf = &track_infos[j];
-                  if( !strcmp(inf->name,
-                              mdl_pstr(&world->meta,route->pstr_name))){
-                     
-                     steamapi_bool set = 0;
-                     if( SteamAPI_ISteamUserStats_GetAchievement( 
-                              hSteamUserStats, inf->achievement_id, &set ) )
-                     {
-                        if( set ){
-                           route->flags |= k_ent_route_flag_achieve_silver;
-                        }
-                     }
-                  }
-               }
-            }
-         }
-      }
-   }
-
-   ent_region_re_eval( world );
-}
-
-void world_entity_serialize( world_instance *world, vg_msg *sav )
-{
-   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ ){
-      ent_challenge *challenge = mdl_arritm(&world->ent_challenge,i);
-
-      const char *alias = mdl_pstr(&world->meta,challenge->pstr_alias);
-      vg_msg_wkvnum( sav, alias, k_vg_msg_u32, 1, &challenge->status );
-   }
-   
-   if( mdl_arrcount(&world->ent_route) ){
-      vg_msg_frame( sav, "routes" );
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-         
-         vg_msg_frame( sav, mdl_pstr( &world->meta, route->pstr_name ) );
-         {
-            vg_msg_wkvnum( sav, "flags", k_vg_msg_u32, 1, &route->flags );
-            vg_msg_wkvnum( sav, "best_laptime", 
-                           k_vg_msg_f64, 1, &route->best_laptime );
-
-            f32 sections[ route->checkpoints_count ];
-
-            for( u32 j=0; j<route->checkpoints_count; j ++ ){
-               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint,
-                     route->checkpoints_start + j );
-
-               sections[j] = cp->best_time;
-            }
-
-            vg_msg_wkvnum( sav, "sections", k_vg_msg_f32, 
-                           route->checkpoints_count, sections );
-         }
-         vg_msg_end_frame( sav );
-      }
-      vg_msg_end_frame( sav );
-   }
-}
diff --git a/world_entity.h b/world_entity.h
deleted file mode 100644 (file)
index c954052..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#pragma once
-#include "world.h"
-#include "entity.h"
-#include "vg/vg_bvh.h"
-#include "vg/vg_msg.h"
-
-typedef struct ent_focus_context ent_focus_context;
-struct ent_focus_context
-{
-   world_instance *world;
-   u32 index; /* Array index of the focused entity */
-   bool active;
-};
-
-void world_gen_entities_init( world_instance *world );
-ent_spawn *world_find_spawn_by_name( world_instance *world, 
-                                        const char *name );
-ent_spawn *world_find_closest_spawn( world_instance *world, 
-                                        v3f position );
-void world_default_spawn_pos( world_instance *world, v3f pos );
-void world_entity_start( world_instance *world, vg_msg *sav );
-void world_entity_serialize( world_instance *world, vg_msg *sav );
-
-entity_call_result ent_volume_call( world_instance *world, ent_call *call );
-entity_call_result ent_audio_call( world_instance *world, ent_call *call );
-entity_call_result ent_ccmd_call( world_instance *world, ent_call *call );
-
-void entity_bh_expand_bound( void *user, boxf bound, u32 item_index );
-float entity_bh_centroid( void *user, u32 item_index, int axis );
-void entity_bh_swap( void *user, u32 ia, u32 ib );
-void entity_bh_debug( void *user, u32 item_index );
-void entity_bh_closest( void *user, u32 item_index, v3f point,
-                           v3f closest );
-
-void world_entity_set_focus( u32 entity_id );
-void world_entity_focus_modal(void);
-
-void world_entity_exit_modal(void);
-void world_entity_clear_focus(void);
-
-void world_entity_focus_preupdate(void);
-void world_entity_focus_render(void);
-void world_entity_focus_camera( world_instance *world, u32 uid );
-void update_ach_models(void);
-
-extern bh_system bh_system_entity_list;
diff --git a/world_gate.c b/world_gate.c
deleted file mode 100644 (file)
index 3e6fdbb..0000000
+++ /dev/null
@@ -1,410 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#ifndef WORLD_GATE_C
-#define WORLD_GATE_C
-
-#include "world.h"
-#include "world_gate.h"
-
-#include "skaterift.h"
-#include "common.h"
-#include "model.h"
-#include "entity.h"
-#include "render.h"
-
-#include "world_water.h"
-#include "player_remote.h"
-#include "shaders/model_gate_unlinked.h"
-#include <string.h>
-
-struct world_gates world_gates;
-
-/*
- * Update the transform matrices for gate
- */
-void gate_transform_update( ent_gate *gate )
-{
-   if( gate->flags & k_ent_gate_flip )
-   {
-      v4f qflip;
-      q_axis_angle( qflip, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
-      q_mul( gate->q[1], qflip, gate->q[1] );
-      q_normalize( gate->q[1] );
-   }
-
-   m4x3f to_local, recv_to_world;
-
-   q_m3x3( gate->q[0], gate->to_world );
-   v3_copy( gate->co[0], gate->to_world[3] );
-   m4x3_invert_affine( gate->to_world, to_local );
-
-   q_m3x3( gate->q[1], recv_to_world );
-   v3_copy( gate->co[1], recv_to_world[3] );
-
-   m4x3_mul( recv_to_world, to_local, gate->transport );
-}
-
-void world_gates_init(void)
-{
-   vg_info( "world_gates_init\n" );
-   vg_linear_clear( vg_mem.scratch );
-
-   mdl_context mgate;
-   mdl_open( &mgate, "models/rs_gate.mdl", vg_mem.scratch );
-   mdl_load_metadata_block( &mgate, vg_mem.scratch );
-
-   mdl_mesh *surface = mdl_find_mesh( &mgate, "rs_gate" );
-   mdl_submesh *sm = mdl_arritm(&mgate.submeshs,surface->submesh_start);
-   world_gates.sm_surface = *sm;
-
-   const char *names[] = { "rs_gate_marker", "rs_gate_marker.001", 
-                           "rs_gate_marker.002", "rs_gate_marker.003" };
-
-   for( int i=0; i<4; i++ ){
-      mdl_mesh *marker = mdl_find_mesh( &mgate, names[i] );
-      sm = mdl_arritm( &mgate.submeshs, marker->submesh_start );
-      world_gates.sm_marker[i] = *sm;
-   }
-
-   mdl_async_load_glmesh( &mgate, &world_gates.mesh, NULL );
-   mdl_close( &mgate );
-}
-
-void ent_gate_get_mdl_mtx( ent_gate *gate, m4x3f mmdl )
-{
-   m4x3_copy( gate->to_world, mmdl );
-   
-   if( !(gate->flags & k_ent_gate_custom_mesh) ){
-      m3x3_scale( mmdl, (v3f){ gate->dimensions[0], 
-                               gate->dimensions[1], 1.0f } );
-   }
-}
-
-static void render_gate_mesh( world_instance *world, ent_gate *gate )
-{
-   if( gate->flags & k_ent_gate_custom_mesh ){
-      mesh_bind( &world->mesh_no_collide );
-      for( u32 i=0; i<gate->submesh_count; i++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       gate->submesh_start+i );
-         mdl_draw_submesh( sm );
-      }
-   }
-   else {
-      mesh_bind( &world_gates.mesh );
-      mdl_draw_submesh( &world_gates.sm_surface );
-   }
-}
-
-/*
- * Render the view through a gate
- */
-int render_gate( world_instance *world, world_instance *world_inside,
-                 ent_gate *gate, vg_camera *cam )
-{
-   v3f viewdir, gatedir;
-   m3x3_mulv( cam->transform, (v3f){0.0f,0.0f,-1.0f}, viewdir );
-   q_mulv( gate->q[0], (v3f){0.0f,0.0f,-1.0f}, gatedir );
-
-   v3f v0;
-   v3_sub( cam->pos, gate->co[0], v0 );
-
-   float dist = v3_dot(v0, gatedir);
-
-   /* Hard cutoff */
-   if( dist > 3.0f )
-      return 0;
-
-   if( v3_dist( cam->pos, gate->co[0] ) > 100.0f )
-      return 0;
-
-   {
-      f32 w = gate->dimensions[0],
-          h = gate->dimensions[1];
-
-      v3f a,b,c,d;
-      m4x3_mulv( gate->to_world, (v3f){-w,-h,0.0f}, a );
-      m4x3_mulv( gate->to_world, (v3f){ w,-h,0.0f}, b );
-      m4x3_mulv( gate->to_world, (v3f){ w, h,0.0f}, c );
-      m4x3_mulv( gate->to_world, (v3f){-w, h,0.0f}, d );
-
-      vg_line( a,b, 0xffffa000 );
-      vg_line( b,c, 0xffffa000 );
-      vg_line( c,d, 0xffffa000 );
-      vg_line( d,a, 0xffffa000 );
-      vg_line( gate->co[0], gate->co[1], 0xff0000ff );
-   }
-
-   /* update gate camera */
-   world_gates.cam.fov = cam->fov;
-   world_gates.cam.nearz = 0.1f;
-   world_gates.cam.farz  = 2000.0f;
-
-   m4x3_mul( gate->transport, cam->transform, world_gates.cam.transform );
-   vg_camera_update_view( &world_gates.cam );
-   vg_camera_update_projection( &world_gates.cam );
-
-   /* Add special clipping plane to projection */
-   v4f surface;
-   q_mulv( gate->q[1], (v3f){0.0f,0.0f,-1.0f}, surface );
-   surface[3] = v3_dot( surface, gate->co[1] );
-   
-   m4x3_mulp( world_gates.cam.transform_inverse, surface, surface );
-   surface[3] = -fabsf(surface[3]);
-
-   if( dist < -0.5f )
-      m4x4_clip_projection( world_gates.cam.mtx.p, surface );
-
-   /* Ready to draw with new camrea */
-   vg_camera_finalize( &world_gates.cam );
-
-   vg_line_point( world_gates.cam.transform[3], 0.3f, 0xff00ff00 );
-
-   shader_model_gate_use();
-   shader_model_gate_uPv( cam->mtx.pv );
-   shader_model_gate_uCam( cam->pos );
-   shader_model_gate_uColour( (v4f){0.0f,1.0f,0.0f,0.0f} );
-   shader_model_gate_uTime( vg.time*0.25f );
-   shader_model_gate_uInvRes( (v2f){
-         1.0f / (float)vg.window_x,
-         1.0f / (float)vg.window_y });
-
-   glEnable( GL_STENCIL_TEST );
-   glStencilOp( GL_KEEP, GL_KEEP, GL_REPLACE );  
-   glStencilFunc( GL_ALWAYS, 1, 0xFF ); 
-   glStencilMask( 0xFF );
-   glEnable( GL_CULL_FACE );
-
-   m4x3f mmdl;
-   ent_gate_get_mdl_mtx( gate, mmdl );
-   shader_model_gate_uMdl( mmdl );
-   render_gate_mesh( world, gate );
-
-   render_world( world_inside, &world_gates.cam, 
-                 1, !localplayer.gate_waiting, 1, 1 );
-
-   return 1;
-}
-
-void render_gate_unlinked( world_instance *world, 
-                           ent_gate *gate, vg_camera *cam )
-{
-   m4x3f mmdl; m4x4f m4mdl;
-   ent_gate_get_mdl_mtx( gate, mmdl );
-   m4x3_expand( mmdl, m4mdl );
-   m4x4_mul( cam->mtx_prev.pv, m4mdl, m4mdl );
-
-   shader_model_gate_unlinked_use();
-   shader_model_gate_unlinked_uPv( cam->mtx.pv );
-   shader_model_gate_unlinked_uPvmPrev( m4mdl );
-   shader_model_gate_unlinked_uCam( cam->pos );
-   shader_model_gate_unlinked_uColour( (v4f){0.0f,1.0f,0.0f,0.0f} );
-   shader_model_gate_unlinked_uTime( vg.time*0.25f );
-   shader_model_gate_unlinked_uMdl( mmdl );
-
-   vg_line_point( gate->co[0], 0.1f, 0xffffff00 );
-
-   render_gate_mesh( world, gate );
-}
-
-/*
- * Intersect the plane of a gate with a line segment, plane coordinate result 
- * stored in 'where'
- */
-static int gate_intersect_plane( ent_gate *gate, 
-                                    v3f pos, v3f last, v2f where )
-{
-   v4f surface;
-   q_mulv( gate->q[0], (v3f){0.0f,0.0f,-1.0f}, surface );
-   surface[3] = v3_dot( surface, gate->co[0] );
-
-   v3f v0, c, delta, p0;
-   v3_sub( pos, last, v0 );
-   float l = v3_length( v0 );
-
-   if( l == 0.0f )
-      return 0;
-
-   v3_divs( v0, l, v0 );
-
-   v3_muls( surface, surface[3], c );
-   v3_sub( c, last, delta );
-
-   float d = v3_dot( surface, v0 );
-
-   if( d > 0.00001f ){
-      float t = v3_dot(delta, surface) / d;
-      if( t >= 0.0f && t <= l ){
-         v3f local, rel;
-         v3_muladds( last, v0, t, local );
-         v3_sub( gate->co[0], local, rel );
-
-         where[0] = v3_dot( rel, gate->to_world[0] );
-         where[1] = v3_dot( rel, gate->to_world[1] );
-
-         where[0] /= v3_dot( gate->to_world[0], gate->to_world[0] );
-         where[1] /= v3_dot( gate->to_world[1], gate->to_world[1] );
-
-         return 1;
-      }
-   }
-
-   return 0;
-}
-
-/*
- * Intersect specific gate
- */
-int gate_intersect( ent_gate *gate, v3f pos, v3f last )
-{
-   v2f xy;
-
-   if( gate_intersect_plane( gate, pos, last, xy ) ){
-      if( (fabsf(xy[0]) <= gate->dimensions[0]) && 
-          (fabsf(xy[1]) <= gate->dimensions[1]) ){
-         return 1;
-      }
-   }
-
-   return 0;
-}
-
-/* 
- * Intersect all gates in the world
- */
-u32 world_intersect_gates( world_instance *world, v3f pos, v3f last )
-{
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-
-      if( !(gate->flags & k_ent_gate_linked) ) continue;
-      if( gate->flags & k_ent_gate_locked ) continue;
-
-      if( gate->flags & k_ent_gate_nonlocal ){
-         if( world_static.instances[gate->target].status 
-               != k_world_status_loaded )
-            continue;
-      }
-
-      if( gate_intersect( gate, pos, last ) )
-         return mdl_entity_id( k_ent_gate, i );
-   }
-
-   return 0;
-}
-
-entity_call_result ent_gate_call( world_instance *world, ent_call *call )
-{
-   u32 index = mdl_entity_id_id( call->id );
-   ent_gate *gate = mdl_arritm( &world->ent_gate, index );
-
-   if( call->function == 0 ) /* unlock() */
-   { 
-      gate->flags &= ~k_ent_gate_locked;
-      return k_entity_call_result_OK;
-   }
-   else 
-   {
-      return k_entity_call_result_unhandled;
-   }
-}
-
-
-/* 
- * detatches any nonlocal gates 
- */
-void world_unlink_nonlocal( world_instance *world )
-{
-   for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ )
-   {
-      ent_gate *gate = mdl_arritm( &world->ent_gate, j );
-
-      if( gate->flags & k_ent_gate_nonlocal )
-      {
-         gate->flags &= ~k_ent_gate_linked;
-      }
-   }
-}
-
-/*
- * This has to be synchronous because main thread looks at gate data for 
- * rendering, and we modify gates that the main thread has ownership of.
- */
-void world_link_gates_async( void *payload, u32 size )
-{
-   VG_ASSERT( vg_thread_purpose() == k_thread_purpose_main );
-
-   world_instance *world = payload;
-   u32 world_id = world - world_static.instances;
-
-   for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ )
-   {
-      ent_gate *gate = mdl_arritm( &world->ent_gate, j );
-      gate_transform_update( gate );
-
-      if( skaterift.demo_mode )
-         if( world_static.instance_addons[world_id]->flags & ADDON_REG_PREMIUM )
-            continue;
-
-      if( !(gate->flags & k_ent_gate_nonlocal) ) continue;
-      if( gate->flags & k_ent_gate_linked ) continue;
-
-      const char *key = mdl_pstr( &world->meta, gate->key );
-      vg_info( "key: %s\n", key );
-
-      for( u32 i=0; i<VG_ARRAY_LEN(world_static.instances); i++ ){
-         world_instance *other = &world_static.instances[i];
-         if( other == world ) continue;
-         if( other->status != k_world_status_loaded ) continue;
-         vg_info( "Checking world %u for key matches\n", i );
-
-         for( u32 k=0; k<mdl_arrcount( &other->ent_gate ); k++ ){
-            ent_gate *gate2 = mdl_arritm( &other->ent_gate, k );
-
-            if( !(gate2->flags & k_ent_gate_nonlocal) ) continue;
-            if( gate2->flags & k_ent_gate_linked ) continue;
-
-            const char *key2 = mdl_pstr( &other->meta, gate2->key );
-            vg_info( " key2: %s\n", key2 );
-
-            if( strcmp( key, key2 ) ) continue;
-
-            vg_success( "Non-local matching pair '%s' found. (%u:%u)\n",
-                         key, world_id, i );
-
-            gate->flags |= k_ent_gate_linked;
-            gate2->flags |= k_ent_gate_linked;
-            gate->target = i;
-            gate2->target = world_id;
-
-            v3_copy( gate->co[0], gate2->co[1] );
-            v3_copy( gate2->co[0], gate->co[1] );
-            v4_copy( gate->q[0], gate2->q[1] );
-            v4_copy( gate2->q[0], gate->q[1] );
-
-            if( world->meta.info.version < 102 ){
-               /* LEGACY BEHAVIOUR: v101
-                *   this would flip both the client worlds portal's entrance and
-                *   exit. effectively the clients portal would be the opposite 
-                *   to the hub worlds one. new behaviour is to just flip the 
-                *   destinations so the rules are consistent in each world.
-                */
-               v4f qflip;
-               q_axis_angle( qflip, (v3f){0.0f,1.0f,0.0f}, VG_PIf );
-               q_mul( gate->q[0], qflip, gate->q[0] );
-               q_mul( gate->q[1], qflip, gate->q[1] );
-               q_mul( gate2->q[1], qflip, gate2->q[1] );
-            }
-
-            gate_transform_update( gate );
-            gate_transform_update( gate2 );
-
-            goto matched;
-         }
-      } matched:;
-   }
-}
-
-#endif /* WORLD_GATE_C */
diff --git a/world_gate.h b/world_gate.h
deleted file mode 100644 (file)
index a071e4b..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-
-#include "vg/vg_camera.h"
-#include "world.h"
-#include "shaders/model_gate.h"
-#include "entity.h"
-
-struct world_gates
-{
-   glmesh mesh;
-   mdl_submesh sm_surface, sm_marker[4];
-   vg_camera cam;
-
-   v3f userportal_co;
-}
-extern world_gates;
-
-void world_gates_init(void);
-void gate_transform_update( ent_gate *gate );
-int render_gate( world_instance *world, world_instance *world_inside,
-                    ent_gate *gate, vg_camera *cam );
-
-int gate_intersect( ent_gate *gate, v3f pos, v3f last );
-u32 world_intersect_gates( world_instance *world, v3f pos, v3f last );
-
-entity_call_result ent_gate_call( world_instance *world, ent_call *call );
-void ent_gate_get_mdl_mtx( ent_gate *gate, m4x3f mmdl );
-
-void world_link_gates_async( void *payload, u32 size );
-void world_unlink_nonlocal( world_instance *world );
-void render_gate_unlinked( world_instance *world,
-                           ent_gate *gate, vg_camera *cam );
diff --git a/world_gen.c b/world_gen.c
deleted file mode 100644 (file)
index e51836d..0000000
+++ /dev/null
@@ -1,776 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- *
- * World generation/population. Different to regular loading, since it needs to
- * create geometry, apply procedural stuff and save that image to files etc.
- */
-#include "world.h"
-#include "world_gen.h"
-#include "world_load.h"
-#include "world_volumes.h"
-#include "world_gate.h"
-#include <string.h>
-
-/*
- * Add all triangles from the model, which match the material ID
- * applies affine transform to the model
- */
-static void world_add_all_if_material( m4x3f transform, scene_context *scene, 
-                                          mdl_context *mdl, u32 id )
-{
-   for( u32 i=0; i<mdl_arrcount(&mdl->meshs); i++ ){
-      mdl_mesh *mesh = mdl_arritm( &mdl->meshs, i );
-
-      for( u32 j=0; j<mesh->submesh_count; j++ ){
-         mdl_submesh *sm = mdl_arritm( &mdl->submeshs, mesh->submesh_start+j );
-         if( sm->material_id == id ){
-            m4x3f transform2;
-            mdl_transform_m4x3( &mesh->transform, transform2 );
-            m4x3_mul( transform, transform2, transform2 );
-
-            scene_add_mdl_submesh( scene, mdl, sm, transform2 );
-         }
-      }
-   }
-}
-
-/*
- * Adds a small blob shape to the world at a raycast location. This is for the
- * grass sprites
- *
- *   /''''\
- *  /      \
- * |        |
- * |________|
- */
-static void world_gen_add_blob( vg_rand *rand, world_instance *world,
-                                   scene_context *scene, ray_hit *hit )
-{
-   m4x3f transform;
-   v4f qsurface, qrandom;
-   v3f axis;
-
-   v3_cross( (v3f){0.0f,1.0f,0.0f}, hit->normal, axis );
-
-   float angle = v3_dot(hit->normal,(v3f){0.0f,1.0f,0.0f});
-   q_axis_angle( qsurface, axis, angle );
-   q_axis_angle( qrandom, (v3f){0.0f,1.0f,0.0f}, vg_randf64(rand)*VG_TAUf );
-   q_mul( qsurface, qrandom, qsurface );
-   q_m3x3( qsurface, transform );
-   v3_copy( hit->pos, transform[3] );
-
-   scene_vert verts[] = 
-   {
-      { .co = { -1.00f, 0.0f, 0.0f } },
-      { .co = {  1.00f, 0.0f, 0.0f } },
-      { .co = { -1.00f, 1.2f, 0.0f } },
-      { .co = {  1.00f, 1.2f, 0.0f } },
-      { .co = { -0.25f, 2.0f, 0.0f } },
-      { .co = {  0.25f, 2.0f, 0.0f } }
-   };
-
-   const u32 indices[] = { 0,1,3, 0,3,2, 2,3,5, 2,5,4 };
-
-   if( scene->vertex_count + VG_ARRAY_LEN(verts) > scene->max_vertices )
-      vg_fatal_error( "Scene vertex buffer overflow" );
-
-   if( scene->indice_count + VG_ARRAY_LEN(indices) > scene->max_indices )
-      vg_fatal_error( "Scene index buffer overflow" );
-
-   scene_vert *dst_verts = &scene->arrvertices[ scene->vertex_count ];
-   u32 *dst_indices      = &scene->arrindices [ scene->indice_count ];
-
-   scene_vert *ref       = &world->scene_geo.arrvertices[ hit->tri[0] ];
-
-   for( u32 i=0; i<VG_ARRAY_LEN(verts); i++ ){
-      scene_vert *pvert = &dst_verts[ i ],
-                 *src   = &verts[ i ];
-
-      m4x3_mulv( transform, src->co, pvert->co );
-      scene_vert_pack_norm( pvert, transform[1], 0.0f );
-
-      v2_copy( ref->uv, pvert->uv );
-   }
-
-   for( u32 i=0; i<VG_ARRAY_LEN(indices); i++ )
-      dst_indices[i] = indices[i] + scene->vertex_count;
-
-   scene->vertex_count += VG_ARRAY_LEN(verts);
-   scene->indice_count += VG_ARRAY_LEN(indices);
-}
-
-/* 
- * Sprinkle foliage models over the map on terrain material 
- */
-static void world_apply_procedural_foliage( world_instance *world,
-                                               scene_context *scene,
-                                               struct world_surface *mat )
-{
-   if( (vg.quality_profile == k_quality_profile_low) ||
-       (vg.quality_profile == k_quality_profile_min) )
-      return;
-
-   vg_info( "Applying foliage (%u)\n", mat->info.pstr_name );
-
-   v3f volume;
-   v3_sub( world->scene_geo.bbx[1], world->scene_geo.bbx[0], volume );
-   volume[1] = 1.0f;
-
-   int count = 0;
-
-   float area = volume[0]*volume[2];
-   u32 particles = 0.08f * area;
-
-   vg_info( "Map area: %f. Max particles: %u\n", area, particles );
-
-   u64 t0 = SDL_GetPerformanceCounter();
-#if 0
-   for( u32 i=0; i<particles; i++ ){
-      v3f pos;
-      v3_mul( volume, (v3f){ vg_randf64(), 1000.0f, vg_randf64() }, pos );
-      pos[1] = 1000.0f;
-      v3_add( pos, world->scene_geo.bbx[0], pos );
-      
-      ray_hit hit;
-      hit.dist = INFINITY;
-
-      if( ray_world( world, pos, (v3f){0.0f,-1.0f,0.0f}, &hit, 
-                     k_material_flag_ghosts )){
-         struct world_surface *m1 = ray_hit_surface( world, &hit );
-         if((hit.normal[1] > 0.8f) && (m1 == mat) && (hit.pos[1] > 0.0f+10.0f)){
-            world_gen_add_blob( world, scene, &hit );
-            count ++;
-         }
-      }
-   }
-#else
-
-   vg_rand rand;
-   vg_rand_seed( &rand, 3030 );
-   
-   const f32 tile_scale = 16.0f;
-   v2i tiles = { volume[0]/tile_scale, volume[2]/tile_scale };
-
-   u32 per_tile = particles/(tiles[0]*tiles[1]);
-
-   for( i32 x=0; x<tiles[0]; x ++ ){
-      for( i32 z=0; z<tiles[1]; z ++ ){
-         for( u32 i=0; i<per_tile; i ++ ){
-            v3f co = { (f32)x+vg_randf64(&rand), 0, (f32)z+vg_randf64(&rand) };
-            v3_muls( co, tile_scale, co );
-            co[1] = 1000.0f;
-            v3_add( co, world->scene_geo.bbx[0], co );
-
-            ray_hit hit;
-            hit.dist = INFINITY;
-
-            if( ray_world( world, co, (v3f){0.0f,-1.0f,0.0f}, &hit, 
-                           k_material_flag_ghosts )){
-               struct world_surface *m1 = ray_hit_surface( world, &hit );
-               if((hit.normal[1] > 0.8f) && (m1 == mat) &&
-                  (hit.pos[1] > 0.0f+10.0f)){
-                  world_gen_add_blob( &rand, world, scene, &hit );
-                  count ++;
-               }
-            }
-
-         }
-      }
-   }
-
-#endif
-
-
-
-   u64 t1 = SDL_GetPerformanceCounter(),
-       utime_blobs = t1-t0,
-       ufreq = SDL_GetPerformanceFrequency();
-   f64 ftime_blobs = ((f64)utime_blobs / (f64)ufreq)*1000.0;
-
-   vg_info( "%d foliage models added. %f%% (%fms)\n", count, 
-            100.0*((f64)count/(f64)particles), ftime_blobs);
-}
-
-static 
-void world_unpack_submesh_dynamic( world_instance *world,
-                                   scene_context *scene, mdl_submesh *sm ){
-   if( sm->flags & k_submesh_flag_consumed ) return;
-
-   m4x3f identity;
-   m4x3_identity( identity );
-   scene_add_mdl_submesh( scene, &world->meta, sm, identity );
-
-   scene_copy_slice( scene, sm );
-   sm->flags |= k_submesh_flag_consumed;
-}
-
-/*
- * Create the main meshes for the world
- */
-void world_gen_generate_meshes( world_instance *world )
-{
-   /* 
-    * Compile meshes into the world scenes
-    */
-   scene_init( &world->scene_geo, 320000, 1200000 );
-   u32 buf_size = scene_mem_required( &world->scene_geo );
-   u8 *buffer = vg_linear_alloc( world->heap, buf_size );
-   scene_supply_buffer( &world->scene_geo, buffer );
-
-   m4x3f midentity;
-   m4x3_identity( midentity );
-
-   /*
-    * Generate scene: collidable geometry
-    * ----------------------------------------------------------------
-    */
-
-   vg_info( "Generating collidable geometry\n" );
-   
-   for( u32 i=0; i<world->surface_count; i++ ){
-      struct world_surface *surf = &world->surfaces[ i ];
-
-      if( surf->info.flags & k_material_flag_collision )
-         world_add_all_if_material( midentity, &world->scene_geo, 
-                                    &world->meta, i );
-
-      scene_copy_slice( &world->scene_geo, &surf->sm_geo );
-      scene_set_vertex_flags( &world->scene_geo, 
-                              surf->sm_geo.vertex_start,
-                              surf->sm_geo.vertex_count, 
-                              (u16)(surf->info.flags & 0xffff) );
-   }
-
-   /* compress that bad boy */
-   u32 new_vert_max = world->scene_geo.vertex_count,
-       new_vert_size = vg_align8(new_vert_max*sizeof(scene_vert)),
-       new_indice_len = world->scene_geo.indice_count*sizeof(u32);
-
-   u32 *src_indices = world->scene_geo.arrindices,
-       *dst_indices = (u32 *)(buffer + new_vert_size);
-
-   memmove( dst_indices, src_indices, new_indice_len );
-
-   world->scene_geo.max_indices = world->scene_geo.indice_count;
-   world->scene_geo.max_vertices = world->scene_geo.vertex_count;
-   buf_size = scene_mem_required( &world->scene_geo );
-
-   buffer = vg_linear_resize( world->heap, buffer, buf_size );
-
-   world->scene_geo.arrvertices = (scene_vert *)(buffer);
-   world->scene_geo.arrindices = (u32 *)(buffer + new_vert_size);
-
-   scene_upload_async( &world->scene_geo, &world->mesh_geo );
-
-   /* need send off the memory to the gpu before we can create the bvh. */
-   vg_async_stall();
-   vg_info( "creating bvh\n" );
-   world->geo_bh = scene_bh_create( world->heap, &world->scene_geo );
-
-   /*
-    * Generate scene: non-collidable geometry
-    * ----------------------------------------------------------------
-    */
-   vg_info( "Generating non-collidable geometry\n" );
-
-   vg_async_item *call = scene_alloc_async( &world->scene_no_collide,
-                                            &world->mesh_no_collide,
-                                            250000, 500000 );
-
-   for( u32 i=0; i<world->surface_count; i++ ){
-      struct world_surface *surf = &world->surfaces[ i ];
-
-      if( !(surf->info.flags & k_material_flag_collision) ){
-         world_add_all_if_material( midentity, 
-                                    &world->scene_no_collide, &world->meta, i );
-      }
-
-      if( surf->info.flags & k_material_flag_grow_grass ){
-         world_apply_procedural_foliage( world, &world->scene_no_collide, 
-                                         surf );
-      }
-
-      scene_copy_slice( &world->scene_no_collide, &surf->sm_no_collide );
-   }
-
-   /* unpack traffic models.. TODO: should we just put all these submeshes in a
-    * dynamic models list? and then the actual entitities point to the 
-    * models. we only have 2 types at the moment which need dynamic models but
-    * would make sense to do this when/if we have more.
-    */
-   for( u32 i=0; i<mdl_arrcount( &world->ent_traffic ); i++ ){
-      ent_traffic *vehc = mdl_arritm( &world->ent_traffic, i );
-
-      for( u32 j=0; j<vehc->submesh_count; j++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       vehc->submesh_start+j );
-         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
-         world->surfaces[ sm->material_id ].flags |= WORLD_SURFACE_HAS_TRAFFIC;
-      }
-   }
-
-   /* unpack challenge models */
-   for( u32 i=0; i<mdl_arrcount( &world->ent_objective ); i++ ){
-      ent_objective *objective = mdl_arritm( &world->ent_objective, i );
-
-      for( u32 j=0; j<objective->submesh_count; j ++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       objective->submesh_start+j );
-         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
-      }
-   }
-
-   /* unpack region models */
-   for( u32 i=0; i<mdl_arrcount( &world->ent_region ); i++ ){
-      ent_region *region = mdl_arritm( &world->ent_region, i );
-
-      for( u32 j=0; j<region->submesh_count; j ++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       region->submesh_start+j );
-         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
-      }
-   }
-
-   /* unpack gate models */
-   for( u32 i=0; i<mdl_arrcount( &world->ent_gate ); i++ ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-
-      if( !(gate->flags & k_ent_gate_custom_mesh) ) continue;
-
-      for( u32 j=0; j<gate->submesh_count; j ++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       gate->submesh_start+j );
-         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
-      }
-   }
-
-   /* unpack prop models */
-   for( u32 i=0; i<mdl_arrcount( &world->ent_prop ); i++ ){
-      ent_prop *prop = mdl_arritm( &world->ent_prop, i );
-
-      for( u32 j=0; j<prop->submesh_count; j ++ ){
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       prop->submesh_start+j );
-         world->surfaces[ sm->material_id ].flags |= WORLD_SURFACE_HAS_PROPS;
-         world_unpack_submesh_dynamic( world, &world->scene_no_collide, sm );
-      }
-   }
-
-   vg_async_dispatch( call, async_scene_upload );
-}
-
-/* signed distance function for cone */
-static f32 fsd_cone_infinite( v3f p, v2f c ){
-   v2f q = { v2_length( (v2f){ p[0], p[2] } ), -p[1] };
-   float s = vg_maxf( 0.0f, v2_dot( q, c ) );
-
-   v2f v0;
-   v2_muls( c, s, v0 );
-   v2_sub( q, v0, v0 );
-
-   float d = v2_length( v0 );
-   return d * ((q[0]*c[1]-q[1]*c[0]<0.0f)?-1.0f:1.0f);
-}
-
-struct light_indices_upload_info{
-   world_instance *world;
-   v3i count;
-
-   void *data;
-};
-
-/*
- * Async reciever to buffer light index data 
- */
-static void async_upload_light_indices( void *payload, u32 size ){
-   struct light_indices_upload_info *info = payload;
-
-   glGenTextures( 1, &info->world->tex_light_cubes );
-   glBindTexture( GL_TEXTURE_3D, info->world->tex_light_cubes );
-   glTexImage3D( GL_TEXTURE_3D, 0, GL_RG32UI,
-                 info->count[0], info->count[1], info->count[2],
-                 0, GL_RG_INTEGER, GL_UNSIGNED_INT, info->data );
-   glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
-   glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
-}
-
-/*
- * Computes light indices for world
- */
-void world_gen_compute_light_indices( world_instance *world )
-{
-   /* light cubes */
-   v3f cubes_min, cubes_max;
-   v3_muls( world->scene_geo.bbx[0], 1.0f/k_world_light_cube_size, cubes_min );
-   v3_muls( world->scene_geo.bbx[1], 1.0f/k_world_light_cube_size, cubes_max );
-
-   v3_sub( cubes_min, (v3f){ 0.5f, 0.5f, 0.5f }, cubes_min );
-   v3_add( cubes_max, (v3f){ 0.5f, 0.5f, 0.5f }, cubes_max );
-
-   v3_floor( cubes_min, cubes_min );
-   v3_floor( cubes_max, cubes_max );
-
-   v3i icubes_min, icubes_max;
-
-   for( int i=0; i<3; i++ ){
-      icubes_min[i] = cubes_min[i];
-      icubes_max[i] = cubes_max[i];
-   }
-
-   v3f cube_size;
-
-   v3i icubes_count;
-   v3i_sub( icubes_max, icubes_min, icubes_count );
-   
-   for( int i=0; i<3; i++ ){
-      int clamped_count = VG_MIN( 128, icubes_count[i]+1 );
-      float clamped_max = icubes_min[i] + clamped_count,
-                    max = icubes_min[i] + icubes_count[i]+1;
-
-      icubes_count[i] = clamped_count;
-      cube_size[i] = (max / clamped_max) * k_world_light_cube_size;
-      cubes_max[i] = clamped_max;
-   }
-
-   v3_mul( cubes_min, cube_size, cubes_min );
-   v3_mul( cubes_max, cube_size, cubes_max );
-
-   for( int i=0; i<3; i++ ){
-      float range = cubes_max[i]-cubes_min[i];
-      world->ub_lighting.g_cube_inv_range[i] = 1.0f / range;
-      world->ub_lighting.g_cube_inv_range[i] *= (float)icubes_count[i];
-
-      vg_info( "cubes[%d]: %d\n", i, icubes_count[i] );
-   }
-
-   int total_cubes = icubes_count[0]*icubes_count[1]*icubes_count[2];
-
-   u32 data_size = vg_align8(total_cubes*sizeof(u32)*2),
-       hdr_size  = vg_align8(sizeof(struct light_indices_upload_info));
-
-   vg_async_item *call = vg_async_alloc( data_size + hdr_size );
-   struct light_indices_upload_info *info = call->payload;
-   info->data = ((u8*)call->payload) + hdr_size;
-   info->world = world;
-   u32 *cubes_index = info->data;
-
-   for( int i=0; i<3; i++ )
-      info->count[i] = icubes_count[i];
-                                       
-   vg_info( "Computing light cubes (%d) [%f %f %f] -> [%f %f %f]\n", 
-             total_cubes, cubes_min[0], -cubes_min[2], cubes_min[1],
-                          cubes_max[0], -cubes_max[2], cubes_max[1] );
-   v3_copy( cubes_min, world->ub_lighting.g_cube_min );
-
-   float bound_radius = v3_length( cube_size );
-
-   for( int iz = 0; iz<icubes_count[2]; iz ++ ){
-      for( int iy = 0; iy<icubes_count[1]; iy++ ){
-         for( int ix = 0; ix<icubes_count[0]; ix++ ){
-            boxf bbx;
-            v3_div( (v3f){ ix, iy, iz }, world->ub_lighting.g_cube_inv_range, 
-                  bbx[0] );
-            v3_div( (v3f){ ix+1, iy+1, iz+1 }, 
-                  world->ub_lighting.g_cube_inv_range, 
-                  bbx[1] );
-
-            v3_add( bbx[0], world->ub_lighting.g_cube_min, bbx[0] );
-            v3_add( bbx[1], world->ub_lighting.g_cube_min, bbx[1] );
-
-            v3f center;
-            v3_add( bbx[0], bbx[1], center );
-            v3_muls( center, 0.5f, center );
-            
-            u32 indices[6] = { 0, 0, 0, 0, 0, 0 };
-            u32 count = 0;
-
-            float influences[6] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
-            const int N = VG_ARRAY_LEN( influences );
-
-            for( u32 j=0; j<mdl_arrcount(&world->ent_light); j ++ ){
-               ent_light *light = mdl_arritm( &world->ent_light, j );
-               v3f closest;
-               closest_point_aabb( light->transform.co, bbx, closest );
-
-               f32 dist2 = v3_dist2( closest, light->transform.co );
-
-               if( dist2 > light->range*light->range )
-                  continue;
-
-               f32 dist = sqrtf(dist2),
-                   influence = 1.0f/(dist+1.0f);
-
-               if( light->type == k_light_type_spot){
-                  v3f local;
-                  m4x3_mulv( light->inverse_world, center, local );
-
-                  float r = fsd_cone_infinite( local, light->angle_sin_cos );
-
-                  if( r > bound_radius )
-                     continue;
-               }
-
-               int best_pos = N;
-               for( int k=best_pos-1; k>=0; k -- )
-                  if( influence > influences[k] )
-                     best_pos = k;
-
-               if( best_pos < N ){
-                  for( int k=N-1; k>best_pos; k -- ){
-                     influences[k] = influences[k-1];
-                     indices[k] = indices[k-1];
-                  }
-
-                  influences[best_pos] = influence;
-                  indices[best_pos] = j;
-               }
-            }
-
-            for( int j=0; j<N; j++ )
-               if( influences[j] > 0.0f )
-                  count ++;
-
-            int base_index = iz * (icubes_count[0]*icubes_count[1]) +
-                             iy * (icubes_count[0]) +
-                             ix;
-
-            int lower_count = VG_MIN( 3, count );
-            u32 packed_index_lower = lower_count;
-            packed_index_lower |= indices[0]<<2;
-            packed_index_lower |= indices[1]<<12;
-            packed_index_lower |= indices[2]<<22;
-
-            int upper_count = VG_MAX( 0, count - lower_count );
-            u32 packed_index_upper = upper_count;
-            packed_index_upper |= indices[3]<<2;
-            packed_index_upper |= indices[4]<<12;
-            packed_index_upper |= indices[5]<<22;
-
-            cubes_index[ base_index*2 + 0 ] = packed_index_lower;
-            cubes_index[ base_index*2 + 1 ] = packed_index_upper;
-         }
-      }
-   }
-
-   vg_async_dispatch( call, async_upload_light_indices );
-}
-
-/*
- * Rendering pass needed to complete the world
- */
-void async_world_postprocess( void *payload, u32 _size )
-{
-   /* create scene lighting buffer */
-   world_instance *world = payload;
-
-   u32 size = VG_MAX(mdl_arrcount(&world->ent_light),1) * sizeof(float)*12;
-   vg_info( "Upload %ubytes (lighting)\n", size );
-
-   glGenBuffers( 1, &world->tbo_light_entities );
-   glBindBuffer( GL_TEXTURE_BUFFER, world->tbo_light_entities );
-   glBufferData( GL_TEXTURE_BUFFER, size, NULL, GL_DYNAMIC_DRAW );
-   
-   /* buffer layout
-    *  
-    *  colour               position                direction (spots)
-    * | .   .   .   .     | .   .   .   .         | .   .   .   .  |
-    * | Re  Ge  Be  Night | Xco Yco Zco Range     | Dx  Dy  Dz  Da |
-    *
-    */
-
-   v4f *light_dst = glMapBuffer( GL_TEXTURE_BUFFER, GL_WRITE_ONLY );
-   for( u32 i=0; i<mdl_arrcount(&world->ent_light); i++ ){
-      ent_light *light = mdl_arritm( &world->ent_light, i );
-
-      /* colour  + night */
-      v3_muls( light->colour, light->colour[3] * 2.0f, light_dst[i*3+0] );
-      light_dst[i*3+0][3] = 2.0f;
-
-      if( !light->daytime ){
-         u32 hash = (i * 29986577u) & 0xffu;
-         float switch_on = hash;
-               switch_on *= (1.0f/255.0f);
-
-         light_dst[i*3+0][3] = 0.44f + switch_on * 0.015f;
-      }
-      
-      /* position + 1/range^2 */
-      v3_copy( light->transform.co, light_dst[i*3+1] );
-      light_dst[i*3+1][3] = 1.0f/(light->range*light->range);
-
-      /* direction + angle */
-      q_mulv( light->transform.q, (v3f){0.0f,-1.0f,0.0f}, light_dst[i*3+2]);
-      light_dst[i*3+2][3] = cosf( light->angle );
-   }
-
-   glUnmapBuffer( GL_TEXTURE_BUFFER );
-
-   glGenTextures( 1, &world->tex_light_entities );
-   glBindTexture( GL_TEXTURE_BUFFER, world->tex_light_entities );
-   glTexBuffer( GL_TEXTURE_BUFFER, GL_RGBA32F, world->tbo_light_entities );
-
-   /* Upload lighting uniform buffer */
-   if( world->water.enabled )
-      v4_copy( world->water.plane, world->ub_lighting.g_water_plane );
-
-   v4f info_vec;
-   v3f *bounds = world->scene_geo.bbx;
-
-   info_vec[0] = bounds[0][0];
-   info_vec[1] = bounds[0][2];
-   info_vec[2] = 1.0f/ (bounds[1][0]-bounds[0][0]);
-   info_vec[3] = 1.0f/ (bounds[1][2]-bounds[0][2]);
-   v4_copy( info_vec, world->ub_lighting.g_depth_bounds );
-
-   /* 
-    * Rendering the depth map
-    */
-   vg_camera ortho;
-
-   v3f extent;
-   v3_sub( world->scene_geo.bbx[1], world->scene_geo.bbx[0], extent );
-
-   float fl = world->scene_geo.bbx[0][0],
-         fr = world->scene_geo.bbx[1][0],
-         fb = world->scene_geo.bbx[0][2],
-         ft = world->scene_geo.bbx[1][2],
-         rl = 1.0f / (fr-fl),
-         tb = 1.0f / (ft-fb);
-
-   m4x4_zero( ortho.mtx.p );
-   ortho.mtx.p[0][0] = 2.0f * rl;
-   ortho.mtx.p[2][1] = 2.0f * tb;
-   ortho.mtx.p[3][0] = (fr + fl) * -rl;
-   ortho.mtx.p[3][1] = (ft + fb) * -tb;
-   ortho.mtx.p[3][3] = 1.0f;
-   m4x3_identity( ortho.transform );
-   vg_camera_update_view( &ortho );
-   vg_camera_finalize( &ortho );
-
-   glDisable(GL_DEPTH_TEST);
-   glDisable(GL_BLEND);
-   glDisable(GL_CULL_FACE);
-   vg_framebuffer_bind( world->heightmap, 1.0f );
-   shader_blitcolour_use();
-   shader_blitcolour_uColour( (v4f){-9999.0f,-9999.0f,-9999.0f,-9999.0f} );
-   render_fsquad();
-
-   glEnable(GL_BLEND);
-   glBlendFunc(GL_ONE, GL_ONE);
-   glBlendEquation(GL_MAX);
-
-   render_world_position( world, &ortho );
-   glDisable(GL_BLEND);
-   glEnable(GL_DEPTH_TEST);
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-
-   /* upload full buffer */
-   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
-   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
-                    sizeof(struct ub_world_lighting), &world->ub_lighting );
-
-   /*
-    * Allocate cubemaps
-    */
-   for( u32 i=0; i<mdl_arrcount(&world->ent_cubemap); i++ ){
-      ent_cubemap *cm = mdl_arritm(&world->ent_cubemap,i);
-
-      glGenTextures( 1, &cm->texture_id );
-      glBindTexture( GL_TEXTURE_CUBE_MAP, cm->texture_id );
-      glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-               glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
-
-               for( u32 j=0; j<6; j ++ ) {
-                       glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + j, 0, GL_RGB, 
-                        WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES, 
-                        0, GL_RGB, GL_UNSIGNED_BYTE, NULL );
-               }
-
-               glGenFramebuffers( 1, &cm->framebuffer_id );
-               glBindFramebuffer( GL_FRAMEBUFFER, cm->framebuffer_id );
-               glGenRenderbuffers(1, &cm->renderbuffer_id );
-               glBindRenderbuffer( GL_RENDERBUFFER, cm->renderbuffer_id );
-               glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 
-                              WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES );
-
-               glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
-                       GL_TEXTURE_CUBE_MAP_POSITIVE_X, cm->texture_id, 0 );
-               glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, 
-                                 GL_RENDERBUFFER, cm->renderbuffer_id );
-
-               glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
-         GL_TEXTURE_CUBE_MAP_POSITIVE_X, cm->texture_id, 0 );
-
-               if( glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE ){
-         vg_error( "Cubemap framebuffer incomplete.\n" );
-      }
-   }
-
-   glBindFramebuffer( GL_FRAMEBUFFER, 0 );
-}
-
-/* Loads textures from the pack file */
-void world_gen_load_surfaces( world_instance *world )
-{
-   vg_info( "Loading textures\n" );
-   world->texture_count = 0;
-
-   world->texture_count = world->meta.textures.count+1;
-   world->textures = vg_linear_alloc( world->heap,
-                              vg_align8(sizeof(GLuint)*world->texture_count) );
-   world->textures[0] = vg.tex_missing;
-
-   for( u32 i=0; i<mdl_arrcount(&world->meta.textures); i++ )
-   {
-      mdl_texture *tex = mdl_arritm( &world->meta.textures, i );
-
-      if( !tex->file.pack_size )
-      {
-         vg_fatal_error( "World models must have packed textures!" );
-      }
-
-      vg_linear_clear( vg_mem.scratch );
-      void *src_data = vg_linear_alloc( vg_mem.scratch, 
-                                        tex->file.pack_size );
-      mdl_fread_pack_file( &world->meta, &tex->file, src_data );
-
-      vg_tex2d_load_qoi_async( src_data, tex->file.pack_size,
-                               VG_TEX2D_NEAREST|VG_TEX2D_REPEAT,
-                               &world->textures[i+1] );
-   }
-
-   vg_info( "Loading materials\n" );
-
-   world->surface_count = world->meta.materials.count+1;
-   world->surfaces = vg_linear_alloc( world->heap,
-               vg_align8(sizeof(struct world_surface)*world->surface_count) );
-
-   /* error material */
-   struct world_surface *errmat = &world->surfaces[0];
-   memset( errmat, 0, sizeof(struct world_surface) );
-                       
-   for( u32 i=0; i<mdl_arrcount(&world->meta.materials); i++ )
-   {
-      struct world_surface *surf = &world->surfaces[i+1];
-      surf->info = *(mdl_material *)mdl_arritm( &world->meta.materials, i );
-      surf->flags = 0;
-
-      if( surf->info.shader == k_shader_water )
-      {
-         struct shader_props_water *props = surf->info.props.compiled;
-         world->ub_lighting.g_water_fog = props->fog_scale;
-      }
-
-      if( surf->info.shader == k_shader_standard_cutout ||
-          surf->info.shader == k_shader_foliage )
-      {
-         struct shader_props_standard *props = surf->info.props.compiled;
-         surf->alpha_tex = props->tex_diffuse;
-      }
-      else
-         surf->alpha_tex = 0;
-   }
-}
diff --git a/world_gen.h b/world_gen.h
deleted file mode 100644 (file)
index c6ffb92..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- *
- * World generation/population. Different to regular loading, since it needs to
- * create geometry, apply procedural stuff and save that image to files etc.
- */
-
-#pragma once
-#include "world.h"
-
-void world_init_blank( world_instance *world );
-void world_gen_load_surfaces( world_instance *world );
-void world_gen_generate_meshes( world_instance *world );
-void world_gen_compute_light_indices( world_instance *world );
-void async_world_postprocess( void *payload, u32 _size );
diff --git a/world_info.h b/world_info.h
deleted file mode 100644 (file)
index 3b29b1f..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#ifndef WORLD_INFO_H
-#define WORLD_INFO_H
-
-/* Purely an information header, shares common strings across client and 
- * server programs. */
-
-struct track_info
-{
-   const char *name, 
-              *achievement_id;
-}
-static track_infos[] = 
-{
-   {
-      .name = "Megapark Green",
-      .achievement_id = "ROUTE_MPG",
-   },
-   {
-      .name = "Megapark Blue",
-      .achievement_id = "ROUTE_MPB",
-   },
-   {
-      .name = "Megapark Yellow",
-      .achievement_id = "ROUTE_MPY",
-   },
-   {
-      .name = "Megapark Red",
-      .achievement_id = "ROUTE_MPR",
-   },
-   {
-      .name = "Coastal Run",
-      .achievement_id = "ROUTE_TC",
-   },
-   {
-      .name = "Docks Jumps",
-      .achievement_id = "ROUTE_TO",
-   }
-};
-
-#endif
diff --git a/world_load.c b/world_load.c
deleted file mode 100644 (file)
index 30c2b2c..0000000
+++ /dev/null
@@ -1,553 +0,0 @@
-#include "world_load.h"
-#include "world_routes.h"
-#include "world_gate.h"
-#include "ent_skateshop.h"
-#include "addon.h"
-#include "save.h"
-#include "vg/vg_msg.h"
-#include "network.h"
-#include "player_remote.h"
-#include "vg/vg_loader.h"
-#include "vg/vg_io.h"
-#include <string.h>
-
-/* 
- * load the .mdl file located in path as a world instance
- */
-static void world_instance_load_mdl( u32 instance_id, const char *path ){
-   world_instance *world = &world_static.instances[ instance_id ];
-   world_init_blank( world );
-   world->status = k_world_status_loading;
-
-   vg_info( "Loading instance[%u]: %s\n", instance_id, path );
-
-   void *allocator = NULL;
-   if( instance_id == 0 ) allocator = world_static.heap;
-   else allocator = world_static.instances[instance_id-1].heap;
-
-   u32 heap_availible = vg_linear_remaining( allocator );
-   u32 min_overhead = sizeof(vg_linear_allocator);
-
-   if( heap_availible < (min_overhead+1024) ){
-      vg_fatal_error( "out of memory" );
-   }
-
-   u32 size = heap_availible - min_overhead;
-   void *heap = vg_create_linear_allocator( allocator, size, VG_MEMORY_SYSTEM );
-
-   world->heap = heap;
-   mdl_context *meta = &world->meta;
-
-   mdl_open( meta, path, world->heap );
-   mdl_load_metadata_block( meta, world->heap );
-   mdl_load_animation_block( meta, world->heap );
-   mdl_load_mesh_block( meta, world->heap );
-
-   vg_info( "%u\n", sizeof(ent_cubemap) );
-
-   MDL_LOAD_ARRAY( meta, &world->ent_gate,      ent_gate,       heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_camera,    ent_camera,     heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_spawn,     ent_spawn,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_light,     ent_light,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_route_node,ent_route_node, heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_path_index,ent_path_index, heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_checkpoint,ent_checkpoint, heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_route,     ent_route,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_water,     ent_water,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_audio_clip,ent_audio_clip, heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_audio,     ent_audio,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_volume,    ent_volume,     heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_traffic,   ent_traffic,    heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_marker,    ent_marker,     heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_skateshop, ent_skateshop,  heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_swspreview,ent_swspreview, heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_ccmd,      ent_ccmd,       heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_objective, ent_objective,  heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_challenge, ent_challenge,  heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_relay,     ent_relay,      heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_cubemap,   ent_cubemap,    heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_miniworld, ent_miniworld,  heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_prop,      ent_prop,       heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_region,    ent_region,     heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_glider,    ent_glider,     heap );
-   MDL_LOAD_ARRAY( meta, &world->ent_npc,       ent_npc,        heap );
-
-   mdl_array_ptr infos;
-   MDL_LOAD_ARRAY( meta, &infos, ent_worldinfo, vg_mem.scratch );
-
-   world->skybox = k_skybox_default;
-   if( mdl_arrcount(&infos) )
-   {
-      world->info = *((ent_worldinfo *)mdl_arritm(&infos,0));
-
-      if( world->meta.info.version >= 104 )
-      {
-         if( MDL_CONST_PSTREQ( &world->meta, world->info.pstr_skybox,"space"))
-         {
-            world->skybox = k_skybox_space;
-         }
-      }
-   }
-   else
-   {
-      world->info.pstr_author = 0;
-      world->info.pstr_desc = 0;
-      world->info.pstr_name = 0;
-      world->info.timezone = 0.0f;
-      world->info.flags = 0;
-   }
-
-   time_t seconds = time(NULL) % ((u32)vg_maxf(1.0f,k_day_length)*60);
-   world->time  = ((f64)(seconds)/(k_day_length*60.0));
-   world->time += (world->info.timezone/24.0);
-
-   /* process resources from pack */
-   u64 t4 = SDL_GetPerformanceCounter();
-   world_gen_load_surfaces( world );
-   u64 t5 = SDL_GetPerformanceCounter();
-   world_gen_routes_ent_init( world );
-   world_gen_entities_init( world );
-   u64 t6 = SDL_GetPerformanceCounter();
-   
-   /* main bulk */
-   u64 t0 = SDL_GetPerformanceCounter();
-   world_gen_generate_meshes( world );
-   u64 t1 = SDL_GetPerformanceCounter();
-   world_gen_routes_generate( instance_id );
-   u64 t2 = SDL_GetPerformanceCounter();
-   world_gen_compute_light_indices( world );
-   u64 t3 = SDL_GetPerformanceCounter();
-   mdl_close( meta );
-
-   u64 utime_mesh = t1-t0,
-       utime_route = t2-t1,
-       utime_indices = t3-t2,
-       utime_tex = t5-t4,
-       utime_ent = t6-t5,
-       ufreq = SDL_GetPerformanceFrequency();
-
-   f64 ftime_mesh = ((f64)utime_mesh / (f64)ufreq)*1000.0,
-       ftime_route = ((f64)utime_route / (f64)ufreq)*1000.0,
-       ftime_ind = ((f64)utime_route / (f64)ufreq)*1000.0,
-       ftime_tex = ((f64)utime_tex / (f64)ufreq)*1000.0,
-       ftime_ent = ((f64)utime_ent / (f64)ufreq)*1000.0;
-
-   vg_info( "wtime:mesh %.2fms route %.2fms ind %.2fms tex %.2fms ent %.2fms\n",
-               ftime_mesh, ftime_route, ftime_ind, ftime_tex, ftime_ent );
-
-   /* init player position.
-    *   - this is overriden by the save state when(if) it loads */
-   world_default_spawn_pos( world, world->player_co );
-
-   /* allocate leaderboard buffers */
-   u32 bs = mdl_arrcount(&world->ent_route)*sizeof(struct leaderboard_cache);
-   world->leaderboard_cache = vg_linear_alloc( heap, bs );
-
-   for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i ++ )
-   {
-      struct leaderboard_cache *board = &world->leaderboard_cache[i];
-      board->data = vg_linear_alloc( heap, NETWORK_REQUEST_MAX );
-      board->status = k_request_status_client_error;
-      board->cache_time = 0.0;
-      board->data_len = 0;
-   }
-
-   world->routes_ui = vg_linear_alloc( heap, 
-         sizeof(struct route_ui)*mdl_arrcount(&world->ent_route) );
-
-   vg_async_call( async_world_postprocess, world, 0 );
-   vg_async_stall();
-}
-
-struct world_load_complete_data{
-   savedata_file save;
-   enum world_purpose purpose;
-};
-
-static void skaterift_world_load_done( void *payload, u32 size )
-{
-   struct world_load_complete_data *data = payload;
-   world_instance *world = &world_static.instances[ data->purpose ];
-
-   vg_msg sav;
-   vg_msg_init( &sav, data->save.buf, data->save.len );
-
-   if( data->purpose != k_world_purpose_hub )
-   {
-      vg_msg player_frame = sav;
-      if( vg_msg_seekframe( &player_frame, "player" ) )
-      {
-         vg_msg_getkvvecf( &player_frame, "position", k_vg_msg_v3f, 
-                           world->player_co, NULL );
-      }
-   }
-
-   world_entity_start( world, &sav );
-   world->status = k_world_status_loaded;
-   world_static.load_state = k_world_loader_none;
-
-   if( world_static.clear_async_op_when_done )
-   {
-      g_client.loaded = 1;
-      world_static.clear_async_op_when_done = 0;
-   }
-}
-
-/*
- * Does a complete world switch using the remaining free slots
- */
-void skaterift_world_load_thread( void *_args )
-{
-   struct world_load_args args = *((struct world_load_args *)_args);
-
-   addon_reg *reg = args.reg;
-   world_static.instance_addons[ args.purpose ] = reg;
-
-   char uid[ADDON_UID_MAX];
-   addon_alias_uid( &reg->alias, uid );
-   vg_info( "LOAD WORLD %s @%d\n", uid, args.purpose );
-
-   char path_buf[4096];
-   vg_str path;
-   vg_strnull( &path, path_buf, 4096 );
-
-   addon_get_content_folder( reg, &path, 1 );
-
-   vg_str folder = path;
-   if( !vg_strgood( &folder ) ) {
-      vg_error( "Load target too long\n" );
-      return;
-   }
-
-   char worlds[k_world_max-1][4096];
-   u32 i=0;
-
-   vg_dir dir;
-   if( !vg_dir_open(&dir, folder.buffer) ){
-      vg_error( "opendir('%s') failed\n", folder.buffer );
-      return;
-   }
-
-   while( vg_dir_next_entry(&dir) ){
-      if( vg_dir_entry_type(&dir) == k_vg_entry_type_file ){
-         const char *d_name = vg_dir_entry_name(&dir);
-         if( d_name[0] == '.' ) continue;
-
-         vg_str file = folder;
-         vg_strcat( &file, "/" );
-         vg_strcat( &file, d_name );
-         if( !vg_strgood( &file ) ) continue;
-
-         char *ext = vg_strch( &file, '.' );
-         if( !ext ) continue;
-         if( strcmp(ext,".mdl") ) continue;
-
-         if( i == k_world_max-1 ){
-            vg_warn( "There are too many .mdl files in the map folder!(3)\n" );
-            break;
-         }
-
-         strcpy( worlds[i++], file.buffer );
-      }
-   }
-   vg_dir_close(&dir);
-
-   if( i == 0 ){
-      vg_warn( "There are no .mdl files in the map folder.\n" );
-   }
-
-   u32 first_index = 0;
-   for( u32 j=0; j<i; j++ ){
-      vg_str name = { .buffer = worlds[j], .i=strlen(worlds[j]), 
-                      sizeof(worlds[j]) };
-      char *fname = vg_strch( &name, '/' );
-      if( fname ){
-         if( !strcmp( fname+1, "main.mdl" ) ){
-            first_index = j;
-         }
-      }
-   }
-
-   world_instance_load_mdl( args.purpose, worlds[first_index] );
-
-   vg_async_item *final_call = 
-      vg_async_alloc( sizeof(struct world_load_complete_data) );
-
-   struct world_load_complete_data *data = final_call->payload;
-   data->purpose = args.purpose;
-
-   skaterift_world_get_save_path( args.purpose, data->save.path );
-   savedata_file_read( &data->save );
-
-   vg_async_dispatch( final_call, skaterift_world_load_done );
-   vg_async_stall();
-}
-
-void skaterift_change_client_world_preupdate(void)
-{
-   if( world_static.load_state != k_world_loader_preload )
-      return;
-
-   /* holding pattern before we can start loading the new world, since we might 
-    * be waiting for audio to stop */
-   for( u32 i=1; i<k_world_max; i++ )
-   {
-      world_instance *inst = &world_static.instances[i];
-      
-      if( inst->status == k_world_status_unloading )
-      {
-         if( world_freeable( inst ) )
-         {
-            world_free( inst );
-         }
-         return;
-      }
-   }
-
-   if( vg_loader_availible() )
-   {
-      vg_info( "worlds cleared, begining load\n" );
-      world_static.load_state = k_world_loader_load;
-
-      vg_linear_clear( vg_async.buffer );
-      struct world_load_args *args = 
-         vg_linear_alloc( vg_async.buffer, sizeof(struct world_load_args) );
-      args->purpose = k_world_purpose_client;
-      args->reg = world_static.instance_addons[ k_world_purpose_client ];
-
-      /* this is replaces the already correct reg but we have to set it again
-       * TOO BAD */
-
-      /* finally can start the loader */
-      vg_loader_start( skaterift_world_load_thread, args );
-   }
-}
-
-/* 
- * places all loaded worlds into unloading state, pass NULL to reload the world 
- */
-void skaterift_change_world_start( addon_reg *reg )
-{
-   if( world_static.instance_addons[ k_world_purpose_client ] == reg )
-   {
-      vg_warn( "World is already loaded\n" );
-      return;
-   }
-
-   if( !reg )
-   {
-      if( world_static.instance_addons[ k_world_purpose_client ] )
-      {
-         reg = world_static.instance_addons[ k_world_purpose_client ];
-         world_static.clear_async_op_when_done = 1;
-      }
-      else 
-      {
-         vg_warn( "No client world loaded\n" );
-         return;
-      }
-   }
-
-   world_static.load_state = k_world_loader_preload;
-
-   if( world_static.active_instance != 0 )
-      g_client.loaded = 0;
-
-   char buf[76];
-   addon_alias_uid( &reg->alias, buf );
-   vg_info( "switching to: %s\n", buf );
-   skaterift_autosave(1);
-
-   vg_linear_clear( vg_mem.scratch ); /* ?? */
-   vg_info( "unloading old worlds\n" );
-
-   world_instance *client_world = 
-      &world_static.instances[ k_world_purpose_client ];
-
-   if( client_world->status == k_world_status_loaded )
-   {
-      client_world->status = k_world_status_unloading;
-      world_fadeout_audio( client_world );
-   }
-
-   world_static.instance_addons[ k_world_purpose_client ] = reg;
-   network_send_item( k_netmsg_playeritem_world1 );
-   relink_all_remote_player_worlds();
-   world_unlink_nonlocal( &world_static.instances[k_world_purpose_hub] );
-}
-
-/* console command for the above function */
-int skaterift_load_world_command( int argc, const char *argv[] )
-{
-   if( !vg_loader_availible() ) 
-   {
-      vg_error( "Loading thread is currently unavailible\n" );
-      return 0;
-   }
-
-   if( argc == 1 )
-   {
-      if( !strcmp( argv[0], "reload" ) )
-      {
-         skaterift_change_world_start( NULL );
-         return 0;
-      }
-
-      addon_alias q;
-      addon_uid_to_alias( argv[0], &q );
-
-      u32 reg_id = addon_match( &q );
-      if( reg_id != 0xffffffff )
-      {
-         addon_reg *reg = get_addon_from_index( k_addon_type_world, reg_id, 0 );
-         skaterift_change_world_start( reg );
-      }
-      else 
-      {
-         vg_error( "Addon '%s' is not installed or not found.\n", argv[0] );
-      }
-   }
-   else 
-   {
-      vg_info( "worlds availible to load:\n" );
-         
-      for( int i=0; i<addon_count(k_addon_type_world,0); i ++ )
-      {
-         addon_reg *w = get_addon_from_index( k_addon_type_world, i, 0);
-
-         char buf[ADDON_UID_MAX];
-         addon_alias_uid( &w->alias, buf );
-
-         if( w->flags & ADDON_REG_HIDDEN )
-            vg_info( "  %s [hidden]\n", buf );
-         else
-            vg_info( "  %s\n", buf );
-      }
-   }
-
-   return 0;
-}
-
-/* 
- * checks:
- *  1. to see if all audios owned by the world have been stopped
- *  2. that this is the least significant world
- */
-int world_freeable( world_instance *world )
-{
-   if( world->status != k_world_status_unloading ) return 0;
-   u8 world_id = (world - world_static.instances) + 1;
-
-   for( u32 i=world_id; i<VG_ARRAY_LEN(world_static.instances); i++ ){
-      if( world_static.instances[i].status != k_world_status_unloaded ){
-         return 0;
-      }
-   }
-
-   int freeable = 1;
-   audio_lock();
-   for( u32 i=0; i<AUDIO_CHANNELS; i++ ){
-      audio_channel *ch = &vg_audio.channels[i];
-      
-      if( ch->allocated && (ch->world_id == world_id)){
-         if( !audio_channel_finished( ch ) ){
-            freeable = 0;
-            break;
-         }
-      }
-   }
-   audio_unlock();
-   return freeable;
-}
-
-/*
- * Free all resources for world instance
- */
-void world_free( world_instance *world )
-{
-   vg_info( "Free world @%p\n", world );
-
-   /* free meshes */
-   mesh_free( &world->mesh_route_lines );
-   mesh_free( &world->mesh_geo );
-   mesh_free( &world->mesh_no_collide );
-   
-   /* glDeleteBuffers silently ignores 0's and names that do not correspond to 
-    * existing buffer objects. 
-    * */
-   glDeleteBuffers( 1, &world->tbo_light_entities );
-   glDeleteTextures( 1, &world->tex_light_entities );
-   glDeleteTextures( 1, &world->tex_light_cubes );
-
-   /* delete textures and meshes */
-   glDeleteTextures( world->texture_count-1, world->textures+1 );
-
-   u32 world_index = world - world_static.instances;
-   if( world_index ){
-      vg_linear_del( world_static.instances[world_index-1].heap, 
-                     vg_linear_header(world->heap) );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_cubemap); i++ ){
-      ent_cubemap *cm = mdl_arritm(&world->ent_cubemap,i);
-      glDeleteTextures( 1, &cm->texture_id );
-      glDeleteFramebuffers( 1, &cm->framebuffer_id );
-      glDeleteRenderbuffers( 1, &cm->renderbuffer_id );
-   }
-
-   world->status = k_world_status_unloaded;
-}
-
-/* 
- * reset the world structure without deallocating persistent buffers 
- * TODO: Make this a memset(0), and have persistent items live in a static loc
- */
-void world_init_blank( world_instance *world )
-{
-   memset( &world->meta, 0, sizeof(mdl_context) );
-
-   world->textures = NULL;
-   world->texture_count = 0;
-   world->surfaces = NULL;
-   world->surface_count = 0;
-
-   world->geo_bh = NULL;
-   world->entity_bh = NULL;
-   world->entity_list = NULL;
-   world->rendering_gate = NULL;
-
-   world->water.enabled = 0;
-   world->time = 0.0;
-
-   /* default lighting conditions 
-    * -------------------------------------------------------------*/
-   struct ub_world_lighting *state = &world->ub_lighting;
-
-   state->g_light_preview = 0;
-   state->g_shadow_samples = 8;
-   state->g_water_fog = 0.04f;
-
-   v4_zero( state->g_water_plane );
-   v4_zero( state->g_depth_bounds );
-
-   state->g_shadow_length = 9.50f;
-   state->g_shadow_spread = 0.65f;
-
-#if 0
-   /* 2023 style */
-   v3_copy( (v3f){0.37f, 0.54f, 0.97f}, state->g_daysky_colour );
-   v3_copy( (v3f){0.03f, 0.05f, 0.20f}, state->g_nightsky_colour );
-   v3_copy( (v3f){1.00f, 0.32f, 0.01f}, state->g_sunset_colour );
-   v3_copy( (v3f){0.13f, 0.17f, 0.35f}, state->g_ambient_colour );
-   v3_copy( (v3f){0.25f, 0.17f, 0.51f}, state->g_sunset_ambient );
-   v3_copy( (v3f){1.10f, 0.89f, 0.35f}, state->g_sun_colour );
-#else
-   /* 2024 style */
-   v3_copy( (v3f){0.308f, 0.543f, 0.904f}, state->g_daysky_colour );
-   v3_copy( (v3f){0.030f, 0.050f, 0.200f}, state->g_nightsky_colour );
-   v3_copy( (v3f){1.000f, 0.320f, 0.010f}, state->g_sunset_colour );
-   v3_copy( (v3f){0.130f, 0.170f, 0.350f}, state->g_ambient_colour );
-   v3_copy( (v3f){0.25f, 0.17f, 0.51f}, state->g_sunset_ambient );
-   v3_copy( (v3f){1.000f, 0.809f, 0.318f}, state->g_sun_colour );
-#endif
-}
diff --git a/world_load.h b/world_load.h
deleted file mode 100644 (file)
index 038233d..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma once
-#include <time.h>
-
-#include "world.h"
-#include "addon.h"
-
-void world_free( world_instance *world );
-int world_freeable( world_instance *world );
-int skaterift_load_world_command( int argc, const char *argv[] );
-void skaterift_change_world_start( addon_reg *reg );
-void skaterift_change_client_world_preupdate(void);
diff --git a/world_map.c b/world_map.c
deleted file mode 100644 (file)
index 2799428..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-#include "skaterift.h"
-#include "world_map.h"
-#include "world.h"
-#include "input.h"
-#include "gui.h"
-#include "menu.h"
-#include "scene.h"
-
-struct world_map world_map;
-
-static void world_map_get_dir( v3f dir )
-{
-   /* idk */
-   dir[0] = -sqrtf(0.5f);
-   dir[2] =  sqrtf(0.5f);
-   dir[1] =  1.0f;
-   v3_normalize(dir);
-}
-
-static void world_map_get_plane( v4f plane )
-{
-   world_instance *world = &world_static.instances[ world_map.world_id ];
-   f32 h = localplayer.rb.co[1];
-   if( world_map.world_id != world_static.active_instance )
-      h = (world->scene_geo.bbx[0][1] + world->scene_geo.bbx[1][1]) * 0.5f;
-
-   v4_copy( (v4f){0.0f,1.0f,0.0f,h}, plane );
-}
-
-static void respawn_world_to_plane_pos( v3f pos, v2f plane_pos )
-{
-   v3f dir;
-   world_map_get_dir( dir );
-   v3_negate(dir,dir);
-   v4f plane;
-   world_map_get_plane( plane );
-
-   v3f co;
-   f32 t = ray_plane( plane, pos, dir );
-   v3_muladds( pos, dir, t, co );
-   plane_pos[0] = co[0];
-   plane_pos[1] = co[2];
-}
-
-static void respawn_map_draw_icon( vg_camera *cam, 
-                                   enum gui_icon icon, v3f pos, f32 size )
-{
-   v4f v;
-   v3_copy( pos, v );
-   v[3] = 1.0f;
-   m4x4_mulv( cam->mtx.pv, v, v );
-   v2_divs( v, v[3], v );
-
-   gui_draw_icon( icon, (v2f){ v[0]*0.5f+0.5f,v[1]*0.5f+0.5f }, size );
-}
-
-static void world_map_select_close(void)
-{
-   world_map.sel_spawn = world_map.close_spawn;
-   gui_helper_clear();
-
-   vg_str text;
-   if( gui_new_helper( input_button_list[k_srbind_maccept], &text ) )
-      vg_strcat( &text, "Spawn Here" );
-   if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
-      vg_strcat( &text, "Back" );
-}
-
-void world_map_click(void)
-{
-   world_map_select_close();
-}
-
-static void world_map_help_normal(void)
-{
-   gui_helper_clear();
-
-   vg_str text;
-   if( gui_new_helper( input_joy_list[k_srjoystick_steer], &text ) )
-      vg_strcat( &text, "Move" );
-
-   if( gui_new_helper( input_button_list[k_srbind_maccept], &text ) )
-      vg_strcat( &text, "Select" );
-
-   if( gui_new_helper( input_button_list[k_srbind_mback], &text ) )
-      vg_strcat( &text, "Exit" );
-
-   if( world_static.instances[1].status == k_world_status_loaded )
-   {
-      if( gui_new_helper( input_button_list[k_srbind_mhub], &text ) )
-         vg_strcat( &text, world_static.active_instance? 
-                              "Go to Hub": "Go to Active World" );
-   }
-}
-
-void world_map_pre_update(void)
-{
-   if( menu_viewing_map() )
-   {
-      if( !world_map.view_ready )
-      {
-         world_map.world_id = world_static.active_instance;
-
-         world_instance *world = &world_static.instances[ world_map.world_id ];
-         v3f *bbx = world->scene_geo.bbx;
-
-         v3_copy( localplayer.rb.co, world->player_co );
-         respawn_world_to_plane_pos( localplayer.rb.co, world_map.plane_pos );
-         world_map.boom_dist = 400.0f;
-         world_map.home_select = 0;
-         world_map.view_ready = 1;
-         world_map.sel_spawn = NULL;
-         world_map.close_spawn = NULL;
-
-         world_map_help_normal();
-      }
-   }
-   else
-   {
-      if( world_map.view_ready )
-      {
-         gui_helper_clear();
-         world_map.view_ready = 0;
-      }
-
-      return;
-   }
-
-   world_instance *world = &world_static.instances[ world_map.world_id ];
-   v3f *bbx = world->scene_geo.bbx;
-   f32 *pos = world_map.plane_pos;
-
-   v2f steer;
-   joystick_state( k_srjoystick_steer, steer );
-   v2_normalize_clamp( steer );
-
-   if( !world_map.sel_spawn )
-   {
-      f32 *pos = world_map.plane_pos;
-      m2x2f rm;
-      m2x2_create_rotation( rm, -0.25f*VG_PIf );
-      m2x2_mulv( rm, steer, steer );
-      v2_muladds( pos, steer, vg.time_frame_delta * 200.0f, pos );
-   }
-
-   f32 bd_target = 400.0f,
-       interp = vg.time_frame_delta*2.0f;
-
-   if( world_map.sel_spawn )
-   {
-      v2f pp;
-      respawn_world_to_plane_pos( world_map.sel_spawn->transform.co, pp );
-      v2_lerp( pos, pp, interp, pos );
-
-      bd_target = 200.0f;
-   }
-   world_map.boom_dist = vg_lerpf( world_map.boom_dist, bd_target, interp );
-
-   v2_minv( (v2f){ bbx[1][0], bbx[1][2] }, pos, pos );
-   v2_maxv( (v2f){ bbx[0][0], bbx[0][2] }, pos, pos );
-
-   /* update camera */
-   vg_camera *cam = &world_map.cam;
-   v3f dir;
-   world_map_get_dir(dir);
-
-   v4f plane;
-   world_map_get_plane( plane );
-
-   v3f co = { pos[0], plane[3]*plane[1], pos[1] };
-   v3_muladds( co, dir, world_map.boom_dist, cam->pos );
-
-   vg_line_cross( co, VG__RED, 10.0f );
-
-   cam->angles[0] = 0.25f * VG_PIf;
-   cam->angles[1] = 0.25f * VG_PIf;
-   cam->farz = 5000.0f;
-   cam->nearz = 10.0f;
-   cam->fov = 40.0f;
-
-   vg_camera_update_transform( cam );
-   vg_camera_update_view( cam );
-   vg_camera_update_projection( cam );
-   vg_camera_finalize( cam );
-
-   /* pick spawn */
-   f32 closest2 = INFINITY;
-   v2f centroid = { 0, 0 };
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i++ )
-   {
-      ent_spawn *spawn = mdl_arritm(&world->ent_spawn,i);
-
-      v4f v;
-      v3_copy( spawn->transform.co, v );
-      v[3] = 1.0f;
-      m4x4_mulv( cam->mtx.pv, v, v );
-      v2_divs( v, v[3], v );
-
-      f32 d2 = v2_dist2(v, centroid);
-      if( d2 < closest2 )
-      {
-         world_map.close_spawn = spawn;
-         closest2 = d2;
-      }
-      spawn->transform.s[0] = d2;
-   }
-
-   if( button_down( k_srbind_maccept ) )
-   {
-      if( world_map.sel_spawn )
-      {
-         skaterift.activity = k_skaterift_default;
-         world_static.active_instance = world_map.world_id;
-         srinput.state = k_input_state_resume;
-         player__spawn( world_map.sel_spawn );
-         return;
-      }
-      else
-      {
-         world_map_select_close();
-      }
-   }
-
-   if( button_down( k_srbind_mback ) )
-   {
-      if( world_map.sel_spawn )
-      {
-         world_map.sel_spawn = NULL;
-         world_map_help_normal();
-      }
-      else
-      {
-         srinput.state = k_input_state_resume;
-         skaterift.activity = k_skaterift_default;
-         return;
-      }
-   }
-
-   /* icons
-    * ---------------------*/
-   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ )
-   {
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
-
-      enum gui_icon icon = k_gui_icon_exclaim_2d;
-      if( challenge->status )
-         icon = k_gui_icon_tick_2d;
-
-      respawn_map_draw_icon( cam, icon, challenge->transform.co, 1.0f );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_spawn); i ++ )
-   {
-      ent_spawn *spawn = mdl_arritm( &world->ent_spawn, i );
-
-      if( spawn->transform.s[0] > 0.3f )
-         continue;
-
-      f32 s = 1.0f-(spawn->transform.s[0] / 0.3f);
-      respawn_map_draw_icon( cam, 
-            spawn==world_map.sel_spawn? 
-               k_gui_icon_spawn_select: k_gui_icon_spawn,
-            spawn->transform.co, s );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_skateshop); i++ )
-   {
-      ent_skateshop *shop = mdl_arritm( &world->ent_skateshop, i );
-      if( shop->type == k_skateshop_type_boardshop )
-      {
-         respawn_map_draw_icon( cam, k_gui_icon_board, shop->transform.co, 1 );
-      }
-      else if( shop->type == k_skateshop_type_worldshop )
-      {
-         respawn_map_draw_icon( cam, k_gui_icon_world, shop->transform.co, 1 );
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ )
-   {
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-      if( gate->flags & k_ent_gate_nonlocal )
-      {
-         respawn_map_draw_icon( cam, k_gui_icon_rift, gate->co[0], 1 );
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ )
-   {
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-
-      v4f colour;
-      v4_copy( route->colour, colour );
-      v3_muls( colour, 1.6666f, colour );
-      gui_icon_setcolour( colour );
-      respawn_map_draw_icon( cam, k_gui_icon_rift_run_2d, 
-                             route->board_transform[3], 1 );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_glider); i ++ )
-   {
-      ent_glider *glider = mdl_arritm( &world->ent_glider, i );
-
-      v4f colour = { 1,1,1,1 };
-
-      if( !(glider->flags & 0x1) )
-         v3_muls( colour, 0.5f, colour );
-      gui_icon_setcolour( colour );
-
-      respawn_map_draw_icon( cam, k_gui_icon_glider, glider->transform.co, 1 );
-   }
-}
diff --git a/world_map.h b/world_map.h
deleted file mode 100644 (file)
index 3899363..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-#include "vg/vg_platform.h"
-#include "vg/vg_camera.h"
-#include "world_entity.h"
-
-struct world_map
-{
-   v2f plane_pos;
-   f32 boom_dist;
-   u32 world_id;
-   u32 home_select;
-
-   ent_spawn *sel_spawn, *close_spawn;
-   vg_camera cam;
-
-   bool view_ready;
-}
-extern world_map;
-void world_map_pre_update(void);
diff --git a/world_physics.c b/world_physics.c
deleted file mode 100644 (file)
index 03be1fc..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-#ifndef WORLD_PHYSICS_C
-#define WORLD_PHYSICS_C
-
-#include "world.h"
-#include "world_physics.h"
-
-void ray_world_get_tri( world_instance *world, ray_hit *hit, v3f tri[3] )
-{
-   for( int i=0; i<3; i++ )
-      v3_copy( world->scene_geo.arrvertices[ hit->tri[i] ].co, tri[i] );
-}
-
-int ray_world( world_instance *world,
-               v3f pos, v3f dir, ray_hit *hit, u16 ignore )
-{
-   return scene_raycast( &world->scene_geo, world->geo_bh, pos, dir, hit, 
-                         ignore );
-}
-
-/*
- * Cast a sphere from a to b and see what time it hits
- */
-int spherecast_world( world_instance *world,
-                      v3f pa, v3f pb, float r, float *t, v3f n, u16 ignore )
-{
-   boxf region;
-   box_init_inf( region );
-   box_addpt( region, pa );
-   box_addpt( region, pb );
-   
-   v3_add( (v3f){ r, r, r}, region[1], region[1] );
-   v3_add( (v3f){-r,-r,-r}, region[0], region[0] );
-
-   v3f dir;
-   v3_sub( pb, pa, dir );
-
-   v3f dir_inv;
-   dir_inv[0] = 1.0f/dir[0];
-   dir_inv[1] = 1.0f/dir[1];
-   dir_inv[2] = 1.0f/dir[2];
-
-   int hit = -1;
-   float min_t = 1.0f;
-
-   bh_iter it;
-   bh_iter_init_box( 0, &it, region );
-   i32 idx;
-   while( bh_next( world->geo_bh, &it, &idx ) ){
-      u32 *ptri = &world->scene_geo.arrindices[ idx*3 ];
-      if( world->scene_geo.arrvertices[ptri[0]].flags & ignore ) continue;
-
-      v3f tri[3];
-      boxf box;
-      box_init_inf( box );
-      for( int j=0; j<3; j++ ){
-         v3_copy( world->scene_geo.arrvertices[ptri[j]].co, tri[j] );
-         box_addpt( box, tri[j] );
-      }
-
-      v3_add( (v3f){ r, r, r}, box[1], box[1] );
-      v3_add( (v3f){-r,-r,-r}, box[0], box[0] );
-
-      if( !ray_aabb1( box, pa, dir_inv, 1.0f ) )
-         continue;
-      
-      float t;
-      v3f n1;
-      if( spherecast_triangle( tri, pa, dir, r, &t, n1 ) ){
-         if( t < min_t ){
-            min_t = t;
-            hit = idx;
-            v3_copy( n1, n );
-         }
-      }
-   }
-
-   *t = min_t;
-   return hit;
-}
-
-struct world_surface *world_tri_index_surface( world_instance *world, 
-                                               u32 index )
-{
-   for( int i=1; i<world->surface_count; i++ ){
-      struct world_surface *surf = &world->surfaces[i];
-
-      if( (index >= surf->sm_geo.vertex_start) &&
-          (index  < surf->sm_geo.vertex_start+surf->sm_geo.vertex_count ) )
-      {
-         return surf;
-      }
-   }
-
-   return &world->surfaces[0];
-}
-
-struct world_surface *world_contact_surface( world_instance *world, rb_ct *ct )
-{
-   return world_tri_index_surface( world, ct->element_id );
-}
-
-struct world_surface *ray_hit_surface( world_instance *world, ray_hit *hit )
-{
-   return world_tri_index_surface( world, hit->tri[0] );
-}
-
-#endif /* WORLD_PHYSICS_C */
diff --git a/world_physics.h b/world_physics.h
deleted file mode 100644 (file)
index 06143b9..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-#include "world.h"
-#include "vg/vg_rigidbody.h"
-#include "vg/vg_rigidbody_collision.h"
-#include "vg/vg_bvh.h"
-
-void ray_world_get_tri( world_instance *world,
-                           ray_hit *hit, v3f tri[3] );
-
-int ray_world( world_instance *world,
-                  v3f pos, v3f dir, ray_hit *hit, u16 ignore );
-
-int spherecast_world( world_instance *world,
-                         v3f pa, v3f pb, float r, float *t, v3f n,
-                         u16 ignore );
-
-struct world_surface *world_tri_index_surface( world_instance *world, 
-                                                  u32 index );
-
-struct world_surface *world_contact_surface( world_instance *world,
-                                                  rb_ct *ct );
-
-struct world_surface *ray_hit_surface( world_instance *world,
-                                                 ray_hit *hit );
diff --git a/world_render.c b/world_render.c
deleted file mode 100644 (file)
index 3eb07a1..0000000
+++ /dev/null
@@ -1,1377 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#include "world.h"
-#include "world_render.h"
-#include "font.h"
-#include "gui.h"
-#include "world_map.h"
-#include "ent_miniworld.h"
-#include "player_remote.h"
-#include "ent_skateshop.h"
-#include "ent_npc.h"
-#include "shaders/model_entity.h"
-
-struct world_render world_render;
-
-static int ccmd_set_time( int argc, const char *argv[] ){
-   world_instance *world = world_current_instance();
-   if( argc == 1 )
-      world->time = atof( argv[0] );
-   else 
-      vg_error( "Usage set_time <0-1.0> (current time: %f)\n", world->time );
-   return 0;
-}
-
-static void async_world_render_init( void *payload, u32 size )
-{
-   vg_info( "Allocate uniform buffers\n" );
-   for( int i=0; i<k_world_max; i++ )
-   {
-      world_instance *world = &world_static.instances[i];
-      world->ubo_bind_point = i;
-
-      glGenBuffers( 1, &world->ubo_lighting );
-      glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
-      glBufferData( GL_UNIFORM_BUFFER, sizeof(struct ub_world_lighting), 
-                    NULL, GL_DYNAMIC_DRAW );
-
-      glBindBufferBase( GL_UNIFORM_BUFFER, i, world->ubo_lighting );
-   }
-}
-
-void world_render_init(void)
-{
-   VG_VAR_F32( k_day_length );
-   VG_VAR_I32( k_debug_light_indices );
-   VG_VAR_I32( k_debug_light_complexity );
-   VG_VAR_I32( k_light_preview );
-   VG_VAR_I32( k_light_editor );
-   vg_console_reg_cmd( "set_time", ccmd_set_time, NULL );
-
-   world_render.sky_rate = 1.0;
-   world_render.sky_target_rate = 1.0;
-
-   vg_info( "Loading world resources\n" );
-   vg_linear_clear( vg_mem.scratch );
-
-   mdl_context msky;
-   mdl_open( &msky, "models/rs_skydome.mdl", vg_mem.scratch );
-   mdl_load_metadata_block( &msky, vg_mem.scratch );
-   mdl_async_load_glmesh( &msky, &world_render.skydome, NULL );
-   mdl_close( &msky );
-
-   vg_info( "Loading default world textures\n" );
-   vg_tex2d_load_qoi_async_file( "textures/garbage.qoi", 
-                                 VG_TEX2D_NEAREST|VG_TEX2D_REPEAT, 
-                                 &world_render.tex_terrain_noise );
-
-   vg_info( "Allocate frame buffers\n" );
-   for( int i=0; i<k_world_max; i++ )
-   {
-      world_instance *world = &world_static.instances[i];
-      world->heightmap = vg_framebuffer_allocate( vg_mem.rtmemory, 1, 0 );
-      world->heightmap->display_name = NULL;
-      world->heightmap->fixed_w = 1024;
-      world->heightmap->fixed_h = 1024;
-      world->heightmap->resolution_div = 0;
-      world->heightmap->attachments[0] = (vg_framebuffer_attachment)
-      {
-         NULL, k_framebuffer_attachment_type_texture,
-         .internalformat = GL_RG16F,
-         .format         = GL_RG,
-         .type           = GL_FLOAT,
-         .attachment     = GL_COLOR_ATTACHMENT0
-      };
-      vg_framebuffer_create( world->heightmap );
-   }
-
-   vg_async_call( async_world_render_init, NULL, 0 );
-}
-
-/* 
- * standard uniform bindings
- * ----------------------------------------------------------------------------
- */
-void world_link_lighting_ub( world_instance *world, GLuint shader )
-{
-   GLuint idx = glGetUniformBlockIndex( shader, "ub_world_lighting" );   
-   glUniformBlockBinding( shader, idx, world->ubo_bind_point );
-}
-
-void world_bind_position_texture( world_instance *world, 
-                                  GLuint shader, GLuint location,
-                                  int slot )
-{
-   vg_framebuffer_bind_texture( world->heightmap, 0, slot );
-   glUniform1i( location, slot );
-}
-
-void world_bind_light_array( world_instance *world,
-                             GLuint shader, GLuint location, 
-                             int slot )
-{
-   glActiveTexture( GL_TEXTURE0 + slot );
-   glBindTexture( GL_TEXTURE_BUFFER, world->tex_light_entities );
-   glUniform1i( location, slot );
-}
-
-void world_bind_light_index( world_instance *world,
-                             GLuint shader, GLuint location,
-                             int slot )
-{
-   glActiveTexture( GL_TEXTURE0 + slot );
-   glBindTexture( GL_TEXTURE_3D, world->tex_light_cubes );
-   glUniform1i( location, slot );
-}
-
-void bind_terrain_noise(void)
-{
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
-}
-
-/* 
- * Get OpenGL texture name from texture ID.
- */
-static GLuint world_get_texture( world_instance *world, u32 id ){
-   if( id & 0x80000000 ) return skaterift.rt_textures[id & ~0x80000000];
-   else                  return world->textures[ id ];
-}
-
-/*
- * Passes Rendering
- * ----------------------------------------------------------------------------
- */
-
-struct world_pass
-{
-   vg_camera *cam;
-   enum mdl_shader shader;
-   enum world_geo_type geo_type;
-
-   void (*fn_bind)( world_instance *world, struct world_surface *mat );
-   void (*fn_set_mdl)( m4x3f mdl );
-   void (*fn_set_uPvmPrev)( m4x4f pvm );
-   void (*fn_set_uNormalMtx)( m3x3f mnorm );
-};
-
-void render_world_depth( world_instance *world, vg_camera *cam );
-
-/*
- * Render a run of submeshes, only of those which match material_id
- */
-static void world_render_submeshes( world_instance *world,
-                                    struct world_pass *pass, 
-                                    mdl_transform *transform, 
-                                    u32 start, u32 count, u32 material_id )
-{
-   for( u32 k=0; k<count; k++ )
-   {
-      mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, start+k );
-      if( sm->material_id != material_id ) 
-         continue;
-
-      m4x3f mmdl;
-      mdl_transform_m4x3( transform, mmdl );
-
-      m4x4f m4mdl;
-      m4x3_expand( mmdl, m4mdl );
-      m4x4_mul( pass->cam->mtx_prev.pv, m4mdl, m4mdl );
-
-      pass->fn_set_mdl( mmdl );
-      pass->fn_set_uPvmPrev( m4mdl );
-
-      mdl_draw_submesh( sm );
-   }
-}
-
-/*
- * Render props attached to this material
- */
-static void world_render_props( world_instance *world, u32 material_id,
-                                struct world_pass *pass )
-{
-   struct world_surface *mat = &world->surfaces[ material_id ];
-   if( !(mat->flags & WORLD_SURFACE_HAS_PROPS) ) return;
-
-   pass->fn_bind( world, mat );
-
-   for( u32 j=0; j<mdl_arrcount( &world->ent_prop ); j++ ){
-      ent_prop *prop = mdl_arritm( &world->ent_prop, j );
-      if( prop->flags & 0x1 ) continue;
-
-      world_render_submeshes( world, pass, &prop->transform, 
-            prop->submesh_start, prop->submesh_count, material_id );
-   }
-}
-
-/*
- * Render traffic models attactched to this material
- */
-static void world_render_traffic( world_instance *world, u32 material_id,
-                                  struct world_pass *pass )
-{
-   struct world_surface *mat = &world->surfaces[ material_id ];
-   if( !(mat->flags & WORLD_SURFACE_HAS_TRAFFIC) ) return;
-
-   pass->fn_bind( world, mat );
-
-   for( u32 j=0; j<mdl_arrcount( &world->ent_traffic ); j++ ){
-      ent_traffic *traffic = mdl_arritm( &world->ent_traffic, j );
-
-      world_render_submeshes( world, pass, &traffic->transform,
-                              traffic->submesh_start, traffic->submesh_count,
-                              material_id );
-   }
-}
-
-/*
- * Iterate and render all materials which match the passes shader and geometry
- * type. Includes props/traffic.
- */
-static void world_render_pass( world_instance *world, struct world_pass *pass )
-{
-   for( int i=0; i<world->surface_count; i++ )
-   {
-      struct world_surface *mat = &world->surfaces[i];
-
-      if( mat->info.shader == pass->shader )
-      {
-         mdl_submesh *sm;
-
-         if( pass->geo_type == k_world_geo_type_solid )
-         {
-            sm = &mat->sm_geo;
-         }
-         else
-         {
-            world_render_traffic( world, i, pass );
-            world_render_props( world, i, pass );
-            sm = &mat->sm_no_collide;
-         }
-
-         if( !sm->indice_count )
-            continue;
-
-         m4x3f mmdl;
-         m4x3_identity( mmdl );
-         pass->fn_set_mdl( mmdl );
-         pass->fn_set_uPvmPrev( pass->cam->mtx_prev.pv );
-         pass->fn_bind( world, mat );
-         mdl_draw_submesh( sm );
-      }
-   }
-}
-
-/*
- * Specific shader instructions
- * ----------------------------------------------------------------------------
- */
-
-static void world_render_both_stages( world_instance *world, 
-                                      struct world_pass *pass )
-{
-   mesh_bind( &world->mesh_geo );
-   pass->geo_type = k_world_geo_type_solid;
-   world_render_pass( world, pass );
-
-   glDisable( GL_CULL_FACE );
-   mesh_bind( &world->mesh_no_collide );
-   pass->geo_type = k_world_geo_type_nonsolid;
-   world_render_pass( world, pass );
-   glEnable( GL_CULL_FACE );
-}
-
-static void bindpoint_world_vb( world_instance *world,
-                                struct world_surface *mat )
-{
-   struct shader_props_vertex_blend *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
-
-#if 0
-   shader_scene_vertex_blend_uOffset( props->blend_offset );
-#endif
-}
-
-static void render_world_vb( world_instance *world, vg_camera *cam )
-{
-   shader_scene_vertex_blend_use();
-   shader_scene_vertex_blend_uTexGarbage(0);
-   shader_scene_vertex_blend_uTexGradients(1);
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_vertex_blend );
-
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
-
-   shader_scene_vertex_blend_uPv( cam->mtx.pv );
-   shader_scene_vertex_blend_uCamera( cam->transform[3] );
-
-   struct world_pass pass = 
-   {
-      .shader = k_shader_standard_vertex_blend,
-      .cam = cam,
-      .fn_bind = bindpoint_world_vb,
-      .fn_set_mdl = shader_scene_vertex_blend_uMdl,
-      .fn_set_uPvmPrev = shader_scene_vertex_blend_uPvmPrev,
-   };
-
-   world_render_both_stages( world, &pass );
-}
-
-static void world_shader_standard_bind( world_instance *world, vg_camera *cam )
-{
-   shader_scene_standard_use();
-   shader_scene_standard_uTexGarbage(0);
-   shader_scene_standard_uTexMain(1);
-   shader_scene_standard_uPv( cam->mtx.pv );
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_standard );
-
-   bind_terrain_noise();
-   shader_scene_standard_uCamera( cam->transform[3] );
-}
-
-static void bindpoint_standard( world_instance *world, 
-                                struct world_surface *mat )
-{
-   struct shader_props_standard *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
-}
-
-static void render_world_standard( world_instance *world, vg_camera *cam )
-{
-   world_shader_standard_bind( world, cam );
-   struct world_pass pass = 
-   {
-      .shader = k_shader_standard,
-      .cam = cam,
-      .fn_bind = bindpoint_standard,
-      .fn_set_mdl = shader_scene_standard_uMdl,
-      .fn_set_uPvmPrev = shader_scene_standard_uPvmPrev,
-   };
-
-   world_render_both_stages( world, &pass );
-}
-
-static void bindpoint_world_cubemapped( world_instance *world,
-                                        struct world_surface *mat )
-{
-   struct shader_props_cubemapped *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, 
-                  world_get_texture( world,props->tex_diffuse ) );
-
-   u32 cubemap_id = props->cubemap_entity,
-       cubemap_index = 0;
-
-   if( mdl_entity_id_type( cubemap_id ) == k_ent_cubemap )
-   {
-      cubemap_index = mdl_entity_id_id( cubemap_id );
-   }
-
-   ent_cubemap *cm = mdl_arritm( &world->ent_cubemap, cubemap_index );
-   glActiveTexture( GL_TEXTURE10 );
-   glBindTexture( GL_TEXTURE_CUBE_MAP, cm->texture_id );
-
-   shader_scene_cubemapped_uColour( props->tint );
-}
-
-static void bindpoint_world_cubemapped_disabled( world_instance *world,
-                                                 struct world_surface *mat )
-{
-   struct shader_props_cubemapped *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, 
-                  world_get_texture( world, props->tex_diffuse ) );
-}
-
-static void render_world_cubemapped( world_instance *world, vg_camera *cam,
-                                     int enabled )
-{
-   if( !mdl_arrcount( &world->ent_cubemap ) )
-      return;
-
-   if( !enabled )
-   {
-      world_shader_standard_bind( world, cam );
-
-      struct world_pass pass = 
-      {
-         .shader = k_shader_cubemap,
-         .cam = cam,
-         .fn_bind = bindpoint_world_cubemapped_disabled,
-         .fn_set_mdl = shader_scene_standard_uMdl,
-         .fn_set_uPvmPrev = shader_scene_standard_uPvmPrev,
-      };
-
-      world_render_both_stages( world, &pass );
-   }
-   else 
-   {
-      shader_scene_cubemapped_use();
-      shader_scene_cubemapped_uTexGarbage(0);
-      shader_scene_cubemapped_uTexMain(1);
-      shader_scene_cubemapped_uTexCubemap(10);
-      shader_scene_cubemapped_uPv( cam->mtx.pv );
-
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_cubemapped );
-
-      bind_terrain_noise();
-      shader_scene_cubemapped_uCamera( cam->transform[3] );
-      
-      struct world_pass pass = 
-      {
-         .shader = k_shader_cubemap,
-         .cam = cam,
-         .fn_bind = bindpoint_world_cubemapped,
-         .fn_set_mdl = shader_scene_cubemapped_uMdl,
-         .fn_set_uPvmPrev = shader_scene_cubemapped_uPvmPrev,
-      };
-
-      world_render_both_stages( world, &pass );
-   }
-}
-
-static void render_world_alphatest( world_instance *world, vg_camera *cam )
-{
-   shader_scene_standard_alphatest_use();
-   shader_scene_standard_alphatest_uTexGarbage(0);
-   shader_scene_standard_alphatest_uTexMain(1);
-   shader_scene_standard_alphatest_uPv( cam->mtx.pv );
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_standard_alphatest );
-
-   bind_terrain_noise();
-   shader_scene_standard_alphatest_uCamera( cam->transform[3] );
-   glDisable(GL_CULL_FACE);
-
-   struct world_pass pass = 
-   {
-      .shader = k_shader_standard_cutout,
-      .cam = cam,
-      .fn_bind = bindpoint_standard,
-      .fn_set_mdl = shader_scene_standard_alphatest_uMdl,
-      .fn_set_uPvmPrev = shader_scene_standard_alphatest_uPvmPrev,
-   };
-
-   world_render_both_stages( world, &pass );
-   glEnable(GL_CULL_FACE);
-}
-
-static void render_world_foliage( world_instance *world, vg_camera *cam )
-{
-   shader_scene_foliage_use();
-   shader_scene_foliage_uTexGarbage(0);
-   shader_scene_foliage_uTexMain(1);
-   shader_scene_foliage_uPv( cam->mtx.pv );
-   shader_scene_foliage_uTime( vg.time );
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_foliage );
-   bind_terrain_noise();
-
-   shader_scene_foliage_uCamera( cam->transform[3] );
-   glDisable(GL_CULL_FACE);
-   struct world_pass pass = 
-   {
-      .shader = k_shader_foliage,
-      .cam = cam,
-      .fn_bind = bindpoint_standard,
-      .fn_set_mdl = shader_scene_foliage_uMdl,
-      .fn_set_uPvmPrev = shader_scene_foliage_uPvmPrev,
-   };
-   world_render_both_stages( world, &pass );
-   glEnable(GL_CULL_FACE);
-}
-
-static void world_render_challenges( world_instance *world, 
-                                     struct world_pass *pass, v3f pos )
-{
-   if( !world ) return;
-   if( skaterift.activity == k_skaterift_replay ) return;
-   if( world != world_current_instance() ) return;
-
-   /* sort lists */
-   f32 radius = 40.0f;
-
-   u32 objective_list[ 32 ],
-       challenge_list[ 16 ];
-
-   v2f objective_uv_offsets[ 32 ];
-
-   u32 objective_count = 0,
-       challenge_count = 0;
-
-   ent_challenge *active_challenge = NULL;
-   int running = 0;
-   if( mdl_entity_id_type( world_static.focused_entity ) == k_ent_challenge ){
-      if( (skaterift.activity == k_skaterift_default) &&
-           world_static.challenge_target ){
-         running = 1;
-      }
-
-      if( !((skaterift.activity != k_skaterift_ent_focus) &&
-            !world_static.challenge_target) ){
-         world_instance *challenge_world = world_current_instance();
-         u32 index = mdl_entity_id_id( world_static.focused_entity );
-         active_challenge = mdl_arritm(&challenge_world->ent_challenge, index);
-      }
-   }
-
-   if( active_challenge ){
-      shader_scene_fxglow_uUvOffset( (v2f){ 8.0f/256.0f, 0.0f } );
-      challenge_list[ challenge_count ++ ] = world_static.focused_entity;
-
-      u32 next = active_challenge->first;
-      while( mdl_entity_id_type(next) == k_ent_objective ){
-         u32 index = mdl_entity_id_id( next );
-         objective_list[ objective_count ++ ] = index;
-
-         ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-         next = objective->id_next;
-      }
-
-      radius = 10000.0f;
-   }
-   else {
-      shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
-      bh_iter it;
-      bh_iter_init_range( 0, &it, pos, radius+10.0f );
-      i32 idx;
-      while( bh_next( world->entity_bh, &it, &idx ) ){
-         u32 id    = world->entity_list[ idx ],
-             type  = mdl_entity_id_type( id ),
-             index = mdl_entity_id_id( id );
-
-         if( type == k_ent_objective ) {
-            if( objective_count < VG_ARRAY_LEN(objective_list) )
-               objective_list[ objective_count ++ ] = index;
-         }
-         else if( type == k_ent_challenge ){
-            if( challenge_count < VG_ARRAY_LEN(challenge_list) )
-               challenge_list[ challenge_count ++ ] = index;
-         }
-      }
-   }
-
-   /* render objectives */
-   glDisable( GL_CULL_FACE );
-   mesh_bind( &world->mesh_no_collide );
-   u32 last_material = 0;
-   for( u32 i=0; i<objective_count; i++ )
-   {
-      u32 index = objective_list[ i ];
-      ent_objective *objective = mdl_arritm( &world->ent_objective, index );
-      if( (objective->flags & k_ent_objective_hidden) &&
-          !active_challenge ) continue;
-
-      f32 scale = 1.0f;
-
-      if( running )
-      {
-         u32 passed = objective->flags & k_ent_objective_passed;
-         f32 target = passed? 0.0f: 1.0f;
-         vg_slewf(&objective->transform.s[0], target, vg.time_frame_delta*4.0f);
-         scale = vg_smoothstepf( objective->transform.s[0] );
-
-         if( (objective == world_static.challenge_target) || passed )
-            shader_scene_fxglow_uUvOffset( (v2f){ 16.0f/256.0f, 0.0f } );
-         else
-            shader_scene_fxglow_uUvOffset( (v2f){ 8.0f/256.0f, 0.0f } );
-      }
-      else 
-      {
-         f32 dist = v3_dist( objective->transform.co, pos ) * (1.0f/radius);
-         scale = vg_smoothstepf( vg_clampf( 5.0f-dist*5.0f, 0.0f,1.0f ) );
-      }
-
-      m4x3f mmdl;
-      q_m3x3( objective->transform.q, mmdl );
-      m3x3_scalef( mmdl, scale );
-      v3_copy( objective->transform.co, mmdl[3] );
-      shader_scene_fxglow_uMdl( mmdl );
-
-      for( u32 j=0; j<objective->submesh_count; j++ )
-      {
-         mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                       objective->submesh_start + j );
-
-         if( sm->material_id != last_material )
-         {
-            last_material = sm->material_id;
-            pass->fn_bind( world, &world->surfaces[sm->material_id] );
-         }
-         mdl_draw_submesh( sm );
-      }
-   }
-
-   /* render texts */
-   font3d_bind( &gui.font, k_font_shader_world, 0, world, &g_render.cam );
-
-   u32 count = 0;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_challenge); i++ )
-   {
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, i );
-      if( challenge->status ) count ++;
-   }
-
-   char buf[32];
-   vg_str str;
-   vg_strnull( &str, buf, sizeof(buf) );
-   vg_strcati32( &str, count );
-   vg_strcatch( &str, '/' );
-   vg_strcati32( &str, mdl_arrcount(&world->ent_challenge) );
-
-   f32 w = font3d_string_width( 1, buf );
-   m4x3f mlocal;
-   m3x3_identity( mlocal );
-   mlocal[3][0] = -w*0.5f;
-   mlocal[3][1] = 0.0f;
-   mlocal[3][2] = 0.0f;
-
-   for( u32 i=0; i<challenge_count; i++ )
-   {
-      u32 index = challenge_list[ i ];
-      ent_challenge *challenge = mdl_arritm( &world->ent_challenge, index );
-      m4x3f mmdl;
-      mdl_transform_m4x3( &challenge->transform, mmdl );
-      m4x3_mul( mmdl, mlocal, mmdl );
-
-      vg_line_point( challenge->transform.co, 0.25f, VG__RED );
-
-      f32 dist = v3_dist( challenge->transform.co, pos ) * (1.0f/radius),
-          scale = vg_smoothstepf( vg_clampf( 10.0f-dist*10.0f, 0.0f,1.0f ) ),
-          colour = 0.0f;
-
-      if( challenge->status )
-         colour = 1.0f;
-
-      shader_scene_font_uOpacity( scale );
-      shader_scene_font_uColourize( colour );
-      font3d_simple_draw( 1, buf, &g_render.cam, mmdl );
-   }
-}
-
-static void bindpoint_fxglow( world_instance *world,
-                              struct world_surface *mat )
-{
-   struct shader_props_standard *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
-}
-
-static void render_world_fxglow( world_instance *host_world, 
-                                 world_instance *world, vg_camera *cam,
-                                 m4x3f world_mmdl,
-                                 int generic, int challenges, int regions )
-{
-   shader_scene_fxglow_use();
-   shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
-   shader_scene_fxglow_uTexMain(1);
-   shader_scene_fxglow_uPv( cam->mtx.pv );
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_fxglow );
-
-   shader_scene_fxglow_uCamera( cam->transform[3] );
-   glDisable(GL_CULL_FACE);
-
-   struct world_pass pass = 
-   {
-      .shader = k_shader_fxglow,
-      .cam = cam,
-      .fn_bind = bindpoint_fxglow,
-      .fn_set_mdl = shader_scene_fxglow_uMdl,
-      .fn_set_uPvmPrev = shader_scene_fxglow_uPvmPrev,
-   };
-
-   if( generic )
-      world_render_both_stages( world, &pass );
-
-   if( regions ){
-      mesh_bind( &world->mesh_no_collide );
-
-      u32 last_material = 0;
-      for( u32 i=0; i<mdl_arrcount(&world->ent_region); i ++ ){
-         shader_scene_fxglow_uUvOffset( (v2f){ 0.0f, 0.0f } );
-         ent_region *region = mdl_arritm( &world->ent_region, i );
-
-         f32 offset = 0.0f;
-         if( region->flags & k_ent_route_flag_achieve_gold )
-            offset = 2.0f;
-         else if( region->flags & k_ent_route_flag_achieve_silver )
-            offset = 1.0f;
-
-         shader_scene_fxglow_uUvOffset( (v2f){ (8.0f/256.0f)*offset, 0.0f } );
-
-         m4x3f mmdl;
-         mdl_transform_m4x3( &region->transform, mmdl );
-         m4x3_mul( world_mmdl, mmdl, mmdl );
-         shader_scene_fxglow_uMdl( mmdl );
-
-         for( u32 j=0; j<region->submesh_count; j++ )
-         {
-            mdl_submesh *sm = mdl_arritm( &world->meta.submeshs, 
-                                          region->submesh_start + j );
-
-            if( sm->material_id != last_material )
-            {
-               last_material = sm->material_id;
-               pass.fn_bind( world, &world->surfaces[sm->material_id] );
-            }
-            mdl_draw_submesh( sm );
-         }
-      }
-   }
-
-   if( challenges )
-      world_render_challenges( world, &pass, cam->pos );
-
-   glEnable(GL_CULL_FACE);
-}
-
-static void bindpoint_terrain( world_instance *world,
-                               struct world_surface *mat )
-{
-   struct shader_props_terrain *props = mat->info.props.compiled;
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_get_texture(world, props->tex_diffuse) );
-   shader_scene_terrain_uBlendOffset( props->blend_offset );
-   shader_scene_terrain_uSandColour( props->sand_colour );
-}
-
-static void bindpoint_override( world_instance *world,
-                                   struct world_surface *mat )
-{
-   if( mat->info.flags & k_material_flag_collision )
-   {
-      shader_scene_override_uAlphatest(0);
-   }
-   else
-   {
-      glActiveTexture( GL_TEXTURE1 );
-      glBindTexture( GL_TEXTURE_2D, world_get_texture(world, mat->alpha_tex) );
-      shader_scene_override_uAlphatest(1);
-   }
-}
-
-static void render_terrain( world_instance *world, vg_camera *cam )
-{
-   shader_scene_terrain_use();
-   shader_scene_terrain_uTexGarbage(0);
-   shader_scene_terrain_uTexGradients(1);
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_terrain );
-   glActiveTexture( GL_TEXTURE0 );
-   glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
-
-   shader_scene_terrain_uPv( cam->mtx.pv );
-   shader_scene_terrain_uCamera( cam->transform[3] );
-
-   struct world_pass pass = 
-   {
-      .shader = k_shader_terrain_blend,
-      .cam = cam,
-      .fn_bind = bindpoint_terrain,
-      .fn_set_mdl = shader_scene_terrain_uMdl,
-      .fn_set_uPvmPrev = shader_scene_terrain_uPvmPrev,
-   };
-
-   world_render_both_stages( world, &pass );
-}
-
-static void render_sky( world_instance *world, vg_camera *cam )
-{
-   /* 
-    * Modify matrix to remove clipping and view translation
-    */
-   m4x4f v,
-         v_prev,
-         pv,
-         pv_prev;
-
-   m4x4_copy( cam->mtx.v, v );
-   m4x4_copy( cam->mtx_prev.v, v_prev );
-
-   for( int i=0; i<3; i++ ){
-      v3_normalize(v[i]);
-      v3_normalize(v_prev[i]);
-   }
-   v3_zero( v[3] );
-   v3_zero( v_prev[3] );
-
-   m4x4_copy( cam->mtx.p,      pv );
-   m4x4_copy( cam->mtx_prev.p, pv_prev );
-   m4x4_reset_clipping( pv,      100.0f, 0.1f );
-   m4x4_reset_clipping( pv_prev, 100.0f, 0.1f );
-
-   m4x4_mul( pv,      v,      pv );
-   m4x4_mul( pv_prev, v_prev, pv_prev );
-
-   m4x3f identity_matrix;
-   m4x3_identity( identity_matrix );
-   
-   /*
-    * Draw
-    */
-   if( world->skybox == k_skybox_default ){
-      shader_model_sky_use();
-      shader_model_sky_uMdl( identity_matrix );
-      shader_model_sky_uPv( pv );
-      shader_model_sky_uPvmPrev( pv_prev );
-      shader_model_sky_uTexGarbage(0);
-      world_link_lighting_ub( world, _shader_model_sky.id );
-
-      glActiveTexture( GL_TEXTURE0 );
-      glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
-   }
-   else if( world->skybox == k_skybox_space ){
-      shader_model_sky_space_use();
-
-      shader_model_sky_space_uMdl( identity_matrix );
-      shader_model_sky_space_uPv( pv );
-      shader_model_sky_space_uPvmPrev( pv_prev );
-      shader_model_sky_space_uTexGarbage(0);
-      world_link_lighting_ub( world, _shader_model_sky_space.id );
-
-      glActiveTexture( GL_TEXTURE0 );
-      glBindTexture( GL_TEXTURE_2D, world_render.tex_terrain_noise );
-   }
-   else {
-      vg_fatal_error( "Programming error\n" );
-   }
-
-   glDepthMask( GL_FALSE );
-   glDisable( GL_DEPTH_TEST );
-
-   mesh_bind( &world_render.skydome );
-   mesh_draw( &world_render.skydome );
-   
-   glEnable( GL_DEPTH_TEST );
-   glDepthMask( GL_TRUE );
-}
-
-void render_world_gates( world_instance *world, vg_camera *cam )
-{
-   float closest = INFINITY;
-   struct ent_gate *gate = NULL;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
-      ent_gate *gi = mdl_arritm( &world->ent_gate, i );
-
-      if( !(gi->flags & k_ent_gate_nonlocal) )
-         if( !(gi->flags & k_ent_gate_linked) )
-            continue;
-
-      float dist = v3_dist2( gi->co[0], cam->transform[3] );
-
-      vg_line_point( gi->co[0], 0.25f, VG__BLUE );
-
-      if( dist < closest ){
-         closest = dist;
-         gate = gi;
-      }
-   }
-   
-   world->rendering_gate = gate;
-
-   if( gate ){
-      if( gate->flags & k_ent_gate_locked ){
-         world->rendering_gate = NULL;
-         return;
-      }
-
-      if( gate->flags & k_ent_gate_nonlocal ){
-         if( !(gate->flags & k_ent_gate_linked) ||
-             (world_static.load_state != k_world_loader_none) ){
-            world->rendering_gate = NULL;
-            render_gate_unlinked( world, gate, cam );
-            return;
-         }
-
-         world_instance *dest_world = &world_static.instances[ gate->target ];
-         render_gate( world, dest_world, gate, cam );
-      }
-      else
-         render_gate( world, world, gate, cam );
-   }
-}
-
-void world_prerender( world_instance *world )
-{
-   if( mdl_arrcount( &world->ent_light ) ){
-      f32 rate = vg_maxf(0.1f, fabsf(k_day_length)) * vg_signf(k_day_length);
-      world->time += vg.time_frame_delta * (1.0/(rate*60.0));
-   }
-   else{
-      world->time = 0.834;
-   }
-
-   if( world->info.flags & 0x1 ){
-      world->time = world->info.timezone;
-   }
-
-   struct ub_world_lighting *state = &world->ub_lighting;
-
-   state->g_time = world->time;
-   state->g_realtime = vg.time_real;
-   state->g_debug_indices = k_debug_light_indices;
-   state->g_light_preview = k_light_preview;
-   state->g_debug_complexity = k_debug_light_complexity;
-   state->g_time_of_day = vg_fractf( world->time );
-
-   if( vg.quality_profile == k_quality_profile_high )
-      state->g_shadow_samples = 8;
-   else if( vg.quality_profile == k_quality_profile_low )
-      state->g_shadow_samples = 2;
-   else
-      state->g_shadow_samples = 0;
-
-   state->g_day_phase   = cosf( state->g_time_of_day * VG_PIf * 2.0f );
-   state->g_sunset_phase= cosf( state->g_time_of_day * VG_PIf * 4.0f + VG_PIf );
-
-   state->g_day_phase    =       state->g_day_phase    * 0.5f + 0.5f;
-   state->g_sunset_phase = powf( state->g_sunset_phase * 0.5f + 0.5f, 6.0f );
-
-   float a = state->g_time_of_day * VG_PIf * 2.0f;
-   state->g_sun_dir[0] = sinf( a );
-   state->g_sun_dir[1] = cosf( a );
-   state->g_sun_dir[2] = 0.2f;
-   v3_normalize( state->g_sun_dir );
-
-   world->probabilities[ k_probability_curve_constant ] = 1.0f;
-   float dp = state->g_day_phase;
-
-   world->probabilities[ k_probability_curve_wildlife_day ] =
-      (dp*dp*0.8f+state->g_sunset_phase)*0.8f;
-   world->probabilities[ k_probability_curve_wildlife_night ] = 
-      1.0f-powf(fabsf((state->g_time_of_day-0.5f)*5.0f),5.0f);
-      
-   glBindBuffer( GL_UNIFORM_BUFFER, world->ubo_lighting );
-   glBufferSubData( GL_UNIFORM_BUFFER, 0, 
-                    sizeof(struct ub_world_lighting), &world->ub_lighting );
-}
-
-static void render_other_entities( world_instance *world, vg_camera *cam )
-{
-   f32 radius = 40.0f;
-   bh_iter it;
-   bh_iter_init_range( 0, &it, cam->pos, radius+10.0f );
-
-   u32 glider_list[4],
-       glider_count = 0,
-       npc_list[4],
-       npc_count = 0;
-
-   i32 idx;
-   while( bh_next( world->entity_bh, &it, &idx ) ){
-      u32 id    = world->entity_list[ idx ],
-          type  = mdl_entity_id_type( id ),
-          index = mdl_entity_id_id( id );
-
-      if( type == k_ent_glider ) 
-      {
-         if( glider_count < VG_ARRAY_LEN(glider_list) )
-            glider_list[ glider_count ++ ] = index;
-      }
-      else if( type == k_ent_npc ) 
-      {
-         if( npc_count < VG_ARRAY_LEN(npc_list) )
-            npc_list[ npc_count ++ ] = index;
-      }
-   }
-
-   shader_model_entity_use();
-   shader_model_entity_uTexMain( 0 );
-   shader_model_entity_uCamera( cam->transform[3] );
-   shader_model_entity_uPv( cam->mtx.pv );
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, model_entity );
-
-   for( u32 j=0; j<glider_count; j ++ )
-   {
-      ent_glider *glider = mdl_arritm( &world->ent_glider, glider_list[j] );
-
-      if( !(glider->flags & 0x1) )
-         continue;
-
-      m4x3f mdl;
-      mdl_transform_m4x3( &glider->transform, mdl );
-
-      f32 dist  = v3_dist( glider->transform.co, cam->pos ) * (1.0f/radius),
-          scale = vg_smoothstepf( vg_clampf( 5.0f-dist*5.0f, 0.0f,1.0f ) );
-      m3x3_scalef( mdl, scale );
-
-      render_glider_model( cam, world, mdl, k_board_shader_entity );
-   }
-
-   for( u32 j=0; j<npc_count; j ++ )
-   {
-      u32 index = npc_list[j];
-      ent_npc *npc = mdl_arritm( &world->ent_npc, npc_list[j] );
-      npc_update( npc );
-      npc_render( npc, world, cam );
-   }
-}
-
-void render_world( world_instance *world, vg_camera *cam,
-                   int stenciled, int viewing_from_gate, 
-                   int with_water, int with_cubemaps )
-{
-   if( stenciled ){
-      glClear( GL_DEPTH_BUFFER_BIT );
-      glStencilFunc( GL_EQUAL, 1, 0xFF );
-      glStencilMask( 0x00 ); 
-      glEnable( GL_CULL_FACE );
-      glEnable( GL_STENCIL_TEST );
-   }
-   else {
-      glStencilMask( 0xFF );
-      glStencilFunc( GL_ALWAYS, 1, 0xFF );
-      glDisable( GL_STENCIL_TEST );
-   }
-
-   render_sky( world, cam );
-
-   m4x3f identity;
-   m4x3_identity(identity);
-   render_world_routes( world, world, identity, cam, viewing_from_gate, 0 );
-   render_world_standard( world, cam );
-   render_world_cubemapped( world, cam, with_cubemaps );
-
-   render_world_vb( world, cam );
-   render_world_alphatest( world, cam );
-   render_world_foliage( world, cam );
-   render_terrain( world, cam );
-
-   if( !viewing_from_gate ){
-      world_entity_focus_render();
-
-      /* Render SFD's */
-      u32 closest = 0;
-      float min_dist = INFINITY;
-
-      if( mdl_arrcount( &world->ent_route ) ){
-         for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
-            ent_route *route = mdl_arritm( &world->ent_route, i );
-            float dist = v3_dist2( route->board_transform[3], cam->pos );
-
-            if( dist < min_dist ){
-               min_dist = dist;
-               closest = i;
-            }
-         }
-
-         ent_route *route = mdl_arritm( &world->ent_route, closest );
-         sfd_render( world, cam, route->board_transform );
-      }
-   }
-
-   if( !viewing_from_gate ){
-      f32 greyout = 0.0f;
-      if( mdl_entity_id_type(world_static.focused_entity) == k_ent_challenge )
-         greyout = world_static.focus_strength;
-
-      if( greyout > 0.0f ){
-         glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
-         glEnable(GL_BLEND);
-         glDisable(GL_DEPTH_TEST);
-         glDepthMask(GL_FALSE);
-         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-         glBlendEquation(GL_FUNC_ADD);
-
-         shader_blitcolour_use();
-         shader_blitcolour_uColour( (v4f){ 0.5f, 0.5f, 0.5f, greyout*0.56f } );
-         render_fsquad();
-         
-         glDisable(GL_BLEND);
-         glEnable(GL_DEPTH_TEST);
-         glDepthMask(GL_TRUE);
-         glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, 
-                                       GL_COLOR_ATTACHMENT1 } );
-      }
-
-      render_world_fxglow( world, world, cam, NULL, 1, 1, 0 );
-   }
-   
-   if( with_water )
-   {
-      render_water_texture( world, cam );
-      vg_framebuffer_bind( g_render.fb_main, k_render_scale );
-   }
-
-   if( stenciled )
-   {
-      glStencilFunc( GL_EQUAL, 1, 0xFF );
-      glStencilMask( 0x00 ); 
-      glEnable( GL_CULL_FACE );
-      glEnable( GL_STENCIL_TEST );
-   }
-
-   if( with_water )
-   {
-      render_water_surface( world, cam );
-   }
-
-   render_remote_players( world, cam );
-   render_other_entities( world, cam );
-   ent_miniworld_render( world, cam );
-
-   if( stenciled )
-   {
-      glStencilMask( 0xFF );
-      glStencilFunc( GL_ALWAYS, 1, 0xFF );
-      glDisable( GL_STENCIL_TEST );
-   }
-}
-
-
-static void render_world_override_pass( world_instance *world,
-                                        struct world_pass *pass,
-                                        m4x3f mmdl, m3x3f mnormal,
-                                        m4x4f mpvm_prev )
-{
-   for( int i=0; i<world->surface_count; i++ )
-   {
-      struct world_surface *mat = &world->surfaces[i];
-      if( mat->info.flags & k_material_flag_ghosts ) continue;
-
-      mdl_submesh *sm;
-      if( pass->geo_type == k_world_geo_type_solid )
-         sm = &mat->sm_geo;
-      else
-         sm = &mat->sm_no_collide;
-
-      if( !sm->indice_count )
-         continue;
-
-      pass->fn_set_mdl( mmdl );
-      pass->fn_set_uNormalMtx( mnormal );
-      pass->fn_set_uPvmPrev( mpvm_prev );
-      pass->fn_bind( world, mat );
-      mdl_draw_submesh( sm );
-   }
-}
-
-void render_world_override( world_instance *world,
-                            world_instance *lighting_source,
-                            m4x3f mmdl,
-                            vg_camera *cam,
-                            ent_spawn *dest_spawn, v4f map_info )
-{
-   struct world_pass pass = 
-   {
-      .cam = cam,
-      .fn_bind = bindpoint_override,
-      .fn_set_mdl = shader_scene_override_uMdl,
-      .fn_set_uPvmPrev = shader_scene_override_uPvmPrev,
-      .fn_set_uNormalMtx = shader_scene_override_uNormalMtx,
-      .shader = k_shader_override
-   };
-
-   shader_scene_override_use();
-   shader_scene_override_uTexGarbage(0);
-   shader_scene_override_uTexMain(1);
-   shader_scene_override_uPv( pass.cam->mtx.pv );
-   shader_scene_override_uMapInfo( map_info );
-
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( lighting_source, scene_override );
-   bind_terrain_noise();
-
-   shader_scene_override_uCamera( pass.cam->transform[3] );
-
-   m4x4f mpvm_prev;
-   m4x3_expand( mmdl, mpvm_prev );
-   m4x4_mul( cam->mtx_prev.pv, mpvm_prev, mpvm_prev );
-
-   m3x3f mnormal;
-   m3x3_inv( mmdl, mnormal );
-   m3x3_transpose( mnormal, mnormal );
-   v3_normalize( mnormal[0] );
-   v3_normalize( mnormal[1] );
-   v3_normalize( mnormal[2] );
-
-   v4f uPlayerPos, uSpawnPos;
-   v4_zero( uPlayerPos );
-   v4_zero( uSpawnPos );
-   v3_copy( world->player_co, uPlayerPos );
-   
-   if( dest_spawn && (v3_dist2(dest_spawn->transform.co,uPlayerPos) > 0.1f) )
-      v3_copy( dest_spawn->transform.co, uSpawnPos );
-   else
-      v3_add( uPlayerPos, (v3f){0,-1,0}, uSpawnPos );
-
-   uPlayerPos[3] = v3_dist(uPlayerPos,uSpawnPos);
-   uSpawnPos[3] = 1.0f/uPlayerPos[3];
-
-   shader_scene_override_uPlayerPos( uPlayerPos );
-   shader_scene_override_uSpawnPos( uSpawnPos );
-
-
-   glDisable( GL_CULL_FACE );
-   mesh_bind( &world->mesh_geo );
-   pass.geo_type = k_world_geo_type_solid;
-   render_world_override_pass( world, &pass, mmdl, mnormal, mpvm_prev );
-   mesh_bind( &world->mesh_no_collide );
-   pass.geo_type = k_world_geo_type_nonsolid;
-   render_world_override_pass( world, &pass, mmdl, mnormal, mpvm_prev );
-   glEnable( GL_CULL_FACE );
-
-   render_world_fxglow( world, world, cam, mmdl, 0, 0, 1 );
-}
-
-static void render_cubemap_side( world_instance *world, ent_cubemap *cm, 
-                                    u32 side ){
-   vg_camera cam;
-   glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
-         GL_TEXTURE_CUBE_MAP_POSITIVE_X + side, cm->texture_id, 0 );
-   glClear( GL_DEPTH_BUFFER_BIT );
-
-   v3f forward[6] = {
-      { -1.0f,  0.0f,  0.0f },
-      {  1.0f,  0.0f,  0.0f },
-      {  0.0f, -1.0f,  0.0f },
-      {  0.0f,  1.0f,  0.0f },
-      {  0.0f,  0.0f, -1.0f },
-      {  0.0f,  0.0f,  1.0f }
-   };
-   v3f up[6] = {
-      { 0.0f, -1.0f,  0.0f },
-      { 0.0f, -1.0f,  0.0f },
-      { 0.0f,  0.0f,  1.0f },
-      { 0.0f,  0.0f, -1.0f },
-      { 0.0f, -1.0f,  0.0f },
-      { 0.0f, -1.0f,  0.0f }
-   };
-
-   v3_zero( cam.angles );
-   v3_copy( cm->co, cam.pos );
-
-   v3_copy( forward[side], cam.transform[2] );
-   v3_copy( up[side], cam.transform[1] );
-   v3_cross( up[side], forward[side], cam.transform[0] );
-   v3_copy( cm->co, cam.transform[3] );
-   m4x3_invert_affine( cam.transform, cam.transform_inverse );
-
-   vg_camera_update_view( &cam );
-
-   cam.nearz = 0.1f;
-   cam.farz = 1000.0f;
-   cam.fov = 90.0f;
-   m4x4_copy( cam.mtx.p,  cam.mtx_prev.p );
-   m4x4_projection( cam.mtx.p, cam.fov, 1.0f, cam.nearz, cam.farz );
-   vg_camera_finalize( &cam );
-   vg_camera_finalize( &cam );
-
-   render_world( world, &cam, 0, 1, 1, 0 );
-}
-
-void render_world_cubemaps( world_instance *world )
-{
-   if( world->cubemap_cooldown )
-      world->cubemap_cooldown --;
-   else{
-      world->cubemap_cooldown = 60;
-
-      glViewport( 0, 0, WORLD_CUBEMAP_RES, WORLD_CUBEMAP_RES );
-      for( u32 i=0; i<mdl_arrcount( &world->ent_cubemap ); i++ ){
-         ent_cubemap *cm = mdl_arritm( &world->ent_cubemap, i );
-         glBindFramebuffer( GL_FRAMEBUFFER, cm->framebuffer_id );
-
-         world->cubemap_side ++;
-         if( world->cubemap_side >= 6 )
-            world->cubemap_side = 0;
-
-         render_cubemap_side( world, cm, world->cubemap_side );
-      }
-   }
-}
-
-/*
- * Geo shaders
- * ---------------------------------------------
- */
-
-void render_world_depth( world_instance *world, vg_camera *cam )
-{
-   m4x3f identity_matrix;
-   m4x3_identity( identity_matrix );
-
-   shader_scene_depth_use();
-   shader_scene_depth_uCamera( cam->transform[3] );
-   shader_scene_depth_uPv( cam->mtx.pv );
-   shader_scene_depth_uPvmPrev( cam->mtx_prev.pv );
-   shader_scene_depth_uMdl( identity_matrix );
-   world_link_lighting_ub( world, _shader_scene_depth.id );
-
-   mesh_bind( &world->mesh_geo );
-   mesh_draw( &world->mesh_geo );
-}
-
-void render_world_position( world_instance *world, vg_camera *cam )
-{
-   m4x3f identity_matrix;
-   m4x3_identity( identity_matrix );
-
-   shader_scene_position_use();
-   shader_scene_position_uCamera( cam->transform[3] );
-   shader_scene_position_uPv( cam->mtx.pv );
-   shader_scene_position_uPvmPrev( cam->mtx_prev.pv );
-   shader_scene_position_uMdl( identity_matrix );
-   world_link_lighting_ub( world, _shader_scene_position.id );
-
-   mesh_bind( &world->mesh_geo );
-   mesh_draw( &world->mesh_geo );
-}
-
-struct ui_enum_opt skybox_setting_options[] = { 
-   { 0, "g_daysky_colour" },
-   { 1, "g_nightsky_colour" },
-   { 2, "g_sunset_colour" },
-   { 3, "g_ambient_colour" },
-   { 4, "g_sun_colour" },
-};
-
-static f32 *skybox_prop_location( world_instance *world, i32 index ){
-   switch( index ){
-      case 0: return world->ub_lighting.g_daysky_colour; break;
-      case 1: return world->ub_lighting.g_nightsky_colour; break;
-      case 2: return world->ub_lighting.g_sunset_colour; break;
-      case 3: return world->ub_lighting.g_ambient_colour; break;
-      case 4: return world->ub_lighting.g_sun_colour; break;
-      default: return NULL;
-   }
-}
-
-void imgui_world_light_edit( ui_context *ctx, world_instance *world )
-{
-   ui_rect panel = { vg.window_x-400, 0, 400, vg.window_y };
-   ui_fill( ctx, panel, ui_colour( ctx, k_ui_bg+1 ) );
-   ui_outline( ctx, panel, 1, ui_colour( ctx, k_ui_bg+7 ), 0 );
-   ui_rect_pad( panel, (ui_px[2]){ 8, 8 } );
-   ui_capture_mouse(ctx, 1);
-
-   static i32 option_to_edit = 0;
-   ui_enum( ctx, panel, "option", skybox_setting_options, 5, &option_to_edit );
-   ui_colourpicker( ctx, panel, "colour", 
-                    skybox_prop_location( world, option_to_edit ) );
-
-   if( ui_button( ctx, panel, "save tweaker file ('/tmp/tweaker.txt')\n" ) == 1 )
-   {
-      FILE *fp = fopen( "/tmp/tweaker.txt", "w" );
-      
-      for( i32 i=0; i<5; i ++ ){
-         struct ui_enum_opt *opt = &skybox_setting_options[i];
-         f32 *val = skybox_prop_location( world, i );
-         fprintf( fp, "%s = {%.3ff, %.3ff, %.3ff, %.3ff},\n",
-                  opt->alias, val[0], val[1], val[2], val[3] );
-      }
-      fclose( fp );
-   }
-}
diff --git a/world_render.h b/world_render.h
deleted file mode 100644 (file)
index 1eb5ab1..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-
-#define WORLD_CUBEMAP_RES 32
-
-#include "vg/vg_camera.h"
-#include "world.h"
-#include "shaders/scene_standard.h"
-#include "shaders/scene_standard_alphatest.h"
-#include "shaders/scene_foliage.h"
-#include "shaders/scene_override.h"
-#include "shaders/scene_cubemapped.h"
-#include "shaders/scene_vertex_blend.h"
-#include "shaders/scene_terrain.h"
-#include "shaders/scene_fxglow.h"
-#include "shaders/scene_depth.h"
-#include "shaders/scene_position.h"
-#include "shaders/scene_font.h"
-#include "shaders/model_sky.h"
-#include "shaders/model_sky_space.h"
-
-static const float k_world_light_cube_size = 8.0f;
-
-struct world_render
-{
-   GLuint tex_terrain_noise;
-
-   /* rendering */
-   glmesh skydome;
-
-   double sky_time, sky_rate, sky_target_rate;
-
-   v3f render_gate_pos;
-   struct timer_text{
-      char text[8];
-      m4x3f transform;
-      ent_gate *gate;
-      ent_route *route;
-   }
-   timer_texts[4];
-   u32 timer_text_count;
-
-   struct text_particle{
-      rigidbody rb;
-      m4x3f mlocal;
-      ent_glyph *glyph;
-      v4f colour;
-      m4x3f mdl;
-      f32 radius;
-   }
-   text_particles[6*4];
-   u32 text_particle_count;
-}
-extern world_render;
-
-void world_render_init(void);
-
-void world_prerender( world_instance *world );
-void world_link_lighting_ub( world_instance *world, GLuint shader );
-void world_bind_position_texture( world_instance *world, 
-                                  GLuint shader, GLuint location,
-                                  int slot );
-void world_bind_light_array( world_instance *world,
-                             GLuint shader, GLuint location, 
-                             int slot );
-void world_bind_light_index( world_instance *world,
-                             GLuint shader, GLuint location,
-                             int slot );
-void render_world_position( world_instance *world, vg_camera *cam );
-void render_world_depth( world_instance *world, vg_camera *cam );
-void render_world( world_instance *world, vg_camera *cam,
-                   int stenciled, int viewing_from_gate, 
-                   int with_water, int with_cubemaps );
-void render_world_cubemaps( world_instance *world );
-void bind_terrain_noise(void);
-void render_world_override( world_instance *world,
-                            world_instance *lighting_source,
-                            m4x3f mmdl,
-                            vg_camera *cam,
-                            ent_spawn *dest_spawn, v4f map_info );
-void render_world_gates( world_instance *world, vg_camera *cam );
-void imgui_world_light_edit( ui_context *ctx, world_instance *world );
-
-#define WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( WORLD, SHADER )            \
-   world_link_lighting_ub( WORLD, _shader_##SHADER.id );                \
-   world_bind_position_texture( WORLD, _shader_##SHADER.id,             \
-                                _uniform_##SHADER##_g_world_depth, 2 ); \
-   world_bind_light_array( WORLD, _shader_##SHADER.id,                  \
-                           _uniform_##SHADER##_uLightsArray, 3 );       \
-   world_bind_light_index( WORLD, _shader_##SHADER.id,                  \
-                           _uniform_##SHADER##_uLightsIndex, 4 );
-
diff --git a/world_routes.c b/world_routes.c
deleted file mode 100644 (file)
index e4fd80e..0000000
+++ /dev/null
@@ -1,1089 +0,0 @@
-#pragma once
-
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software - All Rights Reserved
- *
- * World routes
- */
-
-#include <time.h>
-#include "entity.h"
-#include "world_routes.h"
-#include "world_gate.h"
-#include "world_load.h"
-#include "network.h"
-
-#include "font.h"
-#include "gui.h"
-#include "steam.h"
-#include "network_msg.h"
-#include "network_common.h"
-
-#include "shaders/scene_route.h"
-#include "shaders/routeui.h"
-#include "ent_region.h"
-#include "scene_rigidbody.h"
-
-void world_routes_clear( world_instance *world )
-{
-   for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-      route->active_checkpoint = 0xffff;
-   }
-
-   for( u32 i=0; i<mdl_arrcount( &world->ent_gate ); i++ ){
-      ent_gate *rg = mdl_arritm( &world->ent_gate, i );
-      rg->timing_version = 0;
-      rg->timing_time = 0.0;
-   }
-
-   world_static.current_run_version += 4;
-   world_static.last_use = 0.0;
-}
-
-static void world_routes_time_lap( world_instance *world, ent_route *route ){
-   vg_info( "------- time lap %s -------\n", 
-            mdl_pstr(&world->meta,route->pstr_name) );
-
-   double start_time = 0.0;
-   u32 last_version=0;
-   f64 last_time = 0.0;
-   ent_checkpoint *last_cp = NULL;
-
-   u32 valid_sections=0;
-   int clean = !localplayer.rewinded_since_last_gate;
-
-   for( u32 i=0; i<route->checkpoints_count; i++ ){
-      u32 cpid  = (i+route->active_checkpoint) % route->checkpoints_count;
-          cpid += route->checkpoints_start;
-
-      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, cpid );
-      ent_gate *rg = mdl_arritm( &world->ent_gate, cp->gate_index );
-                rg = mdl_arritm( &world->ent_gate, rg->target );
-
-      if( i == 1 ){
-         route->timing_base = rg->timing_time;
-      }
-
-      if( i == 0 )
-         start_time = rg->timing_time;
-      else{
-         if( last_version+1 == rg->timing_version ) valid_sections ++;
-         else valid_sections = 0;
-      }
-
-      vg_info( "%u %f [%s]\n", rg->timing_version, rg->timing_time,
-            i? ((rg->flags & k_ent_gate_clean_pass)? "CLEAN": "     "):
-                " N/A ");
-
-      if( !(rg->flags & k_ent_gate_clean_pass) )
-         clean = 0;
-
-      last_version = rg->timing_version;
-      last_time = rg->timing_time;
-      last_cp = cp;
-   }
-
-   if( world_static.current_run_version == last_version+1 ){
-      valid_sections ++;
-
-      if( route->checkpoints_count == 1 ){
-         route->timing_base = world_static.time;
-      }
-
-      f32 section = world_static.time - last_time;
-      if( (section < last_cp->best_time) || (last_cp->best_time == 0.0f) ){
-         last_cp->best_time = section;
-      }
-   }
-   else valid_sections = 0;
-
-   vg_info( "%u %f [%s]\n", 
-            world_static.current_run_version, world_static.time,
-           !localplayer.rewinded_since_last_gate? "CLEAN": "     " );
-
-   if( valid_sections==route->checkpoints_count ){
-      f64 lap_time = world_static.time - start_time;
-
-      if( (route->best_laptime == 0.0) || (lap_time < route->best_laptime) ){
-         route->best_laptime = lap_time;
-      }
-
-      route->flags |= k_ent_route_flag_achieve_silver;
-      if( clean ) route->flags |= k_ent_route_flag_achieve_gold;
-      ent_region_re_eval( world );
-
-      /* for steam achievements. */
-      if( route->anon.official_track_id != 0xffffffff ){
-         struct track_info *ti = &track_infos[ route->anon.official_track_id ];
-         if( ti->achievement_id ){
-            steam_set_achievement( ti->achievement_id );
-            steam_store_achievements();
-         }
-      }
-
-      addon_alias *alias = 
-         &world_static.instance_addons[ world_static.active_instance ]->alias;
-
-      char mod_uid[ ADDON_UID_MAX ];
-      addon_alias_uid( alias, mod_uid );
-      network_publish_laptime( mod_uid, 
-                               mdl_pstr( &world->meta, route->pstr_name ),
-                               lap_time );
-   }
-
-   route->valid_checkpoints = valid_sections+1;
-
-   vg_info( "valid sections: %u\n", valid_sections );
-   vg_info( "----------------------------\n" );
-
-   route->ui_residual = 1.0f;
-   route->ui_residual_block_w = route->ui_first_block_width;
-}
-
-/*
- * When going through a gate this is called for bookkeeping purposes
- */
-void world_routes_activate_entry_gate( world_instance *world, ent_gate *rg )
-{
-   world_static.last_use = world_static.time;
-   ent_gate *dest = mdl_arritm( &world->ent_gate, rg->target );
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-
-      u32 active_prev = route->active_checkpoint;
-      route->active_checkpoint = 0xffff;
-
-      for( u32 j=0; j<4; j++ ){
-         if( dest->routes[j] == i ){
-            for( u32 k=0; k<route->checkpoints_count; k++ ){
-               ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, 
-                                                 route->checkpoints_start+k );
-
-               ent_gate *gk = mdl_arritm( &world->ent_gate, cp->gate_index );
-                         gk = mdl_arritm( &world->ent_gate, gk->target );
-               if( gk == dest ){
-                  route->active_checkpoint = k;
-                  world_routes_time_lap( world, route );
-                  break;
-               }
-            }
-            break;
-         }
-      }
-   }
-
-   dest->timing_version = world_static.current_run_version;
-   dest->timing_time = world_static.time;
-
-   if( localplayer.rewinded_since_last_gate ){
-      localplayer.rewinded_since_last_gate = 0;
-      dest->flags &= ~k_ent_gate_clean_pass;
-   }
-   else
-      dest->flags |= k_ent_gate_clean_pass;
-
-   world_static.current_run_version ++;
-}
-
-/* draw lines along the paths */
-static void world_routes_debug( world_instance *world )
-{
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route_node); i++ ){
-      ent_route_node *rn = mdl_arritm(&world->ent_route_node,i);
-      vg_line_point( rn->co, 0.25f, VG__WHITE );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm(&world->ent_route, i);
-
-      u32 colours[] = { 0xfff58142, 0xff42cbf5, 0xff42f56c, 0xfff542b3,
-                        0xff5442f5 };
-
-      u32 cc = 0xffcccccc;
-      if( route->active_checkpoint != 0xffff ){
-         cc = colours[i%VG_ARRAY_LEN(colours)];
-      }
-
-      for( int i=0; i<route->checkpoints_count; i++ ){
-         int i0 = route->checkpoints_start+i,
-             i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
-
-         ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
-                        *c1 = mdl_arritm(&world->ent_checkpoint, i1);
-
-         ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
-         ent_gate *end_gate = mdl_arritm( &world->ent_gate, c1->gate_index );
-
-         v3f p0, p1;
-         v3_copy( start_gate->co[1], p0 );
-
-         for( int j=0; j<c0->path_count; j ++ ){
-            ent_path_index *index = mdl_arritm( &world->ent_path_index, 
-                                                c0->path_start+j );
-
-            ent_route_node *rn = mdl_arritm( &world->ent_route_node,
-                                             index->index );
-
-            v3_copy( rn->co, p1 );
-            vg_line( p0, p1, cc );
-            v3_copy( p1, p0 );
-         }
-
-         v3_copy( end_gate->co[0], p1 );
-         vg_line( p0, p1, cc );
-      }
-   }
-}
-
-
-static 
-void world_routes_place_curve( world_instance *world, ent_route *route,
-                               v4f h[3], v3f n0, v3f n2, scene_context *scene )
-{
-   float t;
-   v3f p, pd;
-   int last_valid=0;
-
-   float total_length = 0.0f,
-         travel_length = 0.0;
-
-   v3f last;
-   v3_copy( h[0], last );
-   for( int it=0; it<128; it ++ ){
-      t = (float)(it+1) * (1.0f/128.0f);
-      eval_bezier3( h[0], h[1], h[2], t, p );
-      total_length += v3_dist( p, last );
-      v3_copy( p, last );
-   }
-
-   float patch_size = 4.0f,
-         patch_count = ceilf( total_length / patch_size );
-
-   t = 0.0f;
-   v3_copy( h[0], last );
-
-   for( int it=0; it<128; it ++ ){
-      float const k_sample_dist = 0.0025f,
-                  k_line_width = 1.5f;
-
-      eval_bezier3( h[0], h[1], h[2], t, p );
-      eval_bezier3( h[0], h[1], h[2], t+k_sample_dist, pd );
-
-      travel_length += v3_dist( p, last );
-
-      float mod = k_sample_dist / v3_dist( p, pd );
-
-      v3f v0,up, right;
-
-      v3_muls( n0, -(1.0f-t), up );
-      v3_muladds( up, n2, -t, up );
-      v3_normalize( up );
-
-      v3_sub( pd,p,v0 );
-      v3_cross( up, v0, right );
-      v3_normalize( right );
-
-      float cur_x = (1.0f-t)*h[0][3] + t*h[2][3];
-      
-      v3f sc, sa, sb, down;
-      v3_muladds( p, right, cur_x * k_line_width, sc );
-      v3_muladds( sc, up, 1.5f, sc );
-      v3_muladds( sc, right, k_line_width*0.95f, sa );
-      v3_muladds( sc, right, 0.0f, sb );
-      v3_muls( up, -1.0f, down );
-      
-      ray_hit ha, hb;
-      ha.dist = 8.0f;
-      hb.dist = 8.0f;
-
-      int resa = ray_world( world, sa, down, &ha, k_material_flag_ghosts ),
-          resb = ray_world( world, sb, down, &hb, k_material_flag_ghosts );
-
-      if( resa && resb ){
-         struct world_surface *surfa = ray_hit_surface( world, &ha ),
-                              *surfb = ray_hit_surface( world, &hb );
-
-         if( (surfa->info.flags & k_material_flag_skate_target) &&
-             (surfb->info.flags & k_material_flag_skate_target) )
-         {
-            scene_vert va, vb;
-
-            float gap = vg_fractf(cur_x*0.5f)*0.02f;
-            
-            v3_muladds( ha.pos, up, 0.06f+gap, va.co );
-            v3_muladds( hb.pos, up, 0.06f+gap, vb.co );
-
-            scene_vert_pack_norm( &va, up, 0.0f );
-            scene_vert_pack_norm( &vb, up, 0.0f );
-
-            float t1 = (travel_length / total_length) * patch_count;
-            va.uv[0] = t1;
-            va.uv[1] = 0.0f;
-            vb.uv[0] = t1;
-            vb.uv[1] = 1.0f;
-
-            scene_push_vert( scene, &va );
-            scene_push_vert( scene, &vb );
-
-            if( last_valid ){
-               /* Connect them with triangles */
-               scene_push_tri( scene, (u32[3]){ 
-                     last_valid+0-2, last_valid+1-2, last_valid+2-2} );
-               scene_push_tri( scene, (u32[3]){ 
-                     last_valid+1-2, last_valid+3-2, last_valid+2-2} );
-            }
-            
-            last_valid = scene->vertex_count;
-         }
-         else
-            last_valid = 0;
-      }
-      else
-         last_valid = 0;
-
-      if( t == 1.0f )
-         return;
-
-      t += 1.0f*mod;
-      if( t > 1.0f )
-         t = 1.0f;
-
-      v3_copy( p, last );
-   }
-}
-
-static void world_routes_gen_meshes( world_instance *world, u32 route_id, 
-                                        scene_context *sc )
-{
-   ent_route *route = mdl_arritm( &world->ent_route, route_id );
-   u8 colour[4];
-   colour[0] = route->colour[0] * 255.0f;
-   colour[1] = route->colour[1] * 255.0f;
-   colour[2] = route->colour[2] * 255.0f;
-   colour[3] = route->colour[3] * 255.0f;
-
-   u32 last_valid = 0;
-
-   for( int i=0; i<route->checkpoints_count; i++ ){
-      int i0 = route->checkpoints_start+i,
-          i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
-
-      ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
-                     *c1 = mdl_arritm(&world->ent_checkpoint, i1);
-
-      ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
-      start_gate = mdl_arritm( &world->ent_gate, start_gate->target );
-
-      ent_gate *end_gate = mdl_arritm( &world->ent_gate, c1->gate_index ),
-               *collector = mdl_arritm( &world->ent_gate, end_gate->target );
-
-      v4f p[3];
-
-      v3_add( (v3f){0.0f,0.1f,0.0f}, start_gate->co[0], p[0] );
-      p[0][3]  = start_gate->ref_count;
-      p[0][3] -= (float)start_gate->route_count * 0.5f;
-      start_gate->ref_count ++;
-
-      if( !c0->path_count )
-         continue;
-
-      /* this is so that we get nice flow through the gates */
-      v3f temp_alignments[2];
-      ent_gate *both[] = { start_gate, end_gate };
-
-      for( int j=0; j<2; j++ ){
-         int pi = c0->path_start + ((j==1)? c0->path_count-1: 0);
-
-         ent_path_index *index = mdl_arritm( &world->ent_path_index, pi );
-         ent_route_node *rn = mdl_arritm( &world->ent_route_node,
-                                          index->index );
-         v3f v0;
-         v3_sub( rn->co, both[j]->co[0], v0 );
-         float d = v3_dot( v0, both[j]->to_world[2] );
-
-         v3_muladds( both[j]->co[0], both[j]->to_world[2], d, 
-                     temp_alignments[j] );
-         v3_add( (v3f){0.0f,0.1f,0.0f}, temp_alignments[j], temp_alignments[j]);
-      }
-
-
-      for( int j=0; j<c0->path_count; j ++ ){
-         ent_path_index *index = mdl_arritm( &world->ent_path_index, 
-                                             c0->path_start+j );
-         ent_route_node *rn = mdl_arritm( &world->ent_route_node,
-                                          index->index );
-         if( j==0 || j==c0->path_count-1 )
-            if( j == 0 )
-               v3_copy( temp_alignments[0], p[1] );
-            else
-               v3_copy( temp_alignments[1], p[1] );
-         else
-            v3_copy( rn->co, p[1] );
-
-         p[1][3] = rn->ref_count;
-         p[1][3] -= (float)rn->ref_total * 0.5f;
-         rn->ref_count ++;
-
-         if( j+1 < c0->path_count ){
-            index = mdl_arritm( &world->ent_path_index, 
-                                c0->path_start+j+1 );
-            rn = mdl_arritm( &world->ent_route_node, index->index );
-
-            if( j+1 == c0->path_count-1 )
-               v3_lerp( p[1], temp_alignments[1], 0.5f, p[2] );
-            else
-               v3_lerp( p[1], rn->co, 0.5f, p[2] );
-
-            p[2][3] = rn->ref_count;
-            p[2][3] -= (float)rn->ref_total * 0.5f;
-         }
-         else{
-            v3_copy( end_gate->co[0], p[2] );
-            v3_add( (v3f){0.0f,0.1f,0.0f}, p[2], p[2] );
-            p[2][3] = collector->ref_count;
-
-            if( i == route->checkpoints_count-1)
-               p[2][3] -= 1.0f;
-
-            p[2][3] -= (float)collector->route_count * 0.5f;
-            //collector->ref_count ++;
-         }
-
-         /* p0,p1,p2 bezier patch is complete
-          * --------------------------------------*/
-         v3f surf0, surf2, n0, n2;
-
-         if( bh_closest_point( world->geo_bh, p[0], surf0, 5.0f ) == -1 )
-            v3_add( (v3f){0.0f,-0.1f,0.0f}, p[0], surf0 );
-
-         if( bh_closest_point( world->geo_bh, p[2], surf2, 5.0f ) == -1 )
-            v3_add( (v3f){0.0f,-0.1f,0.0f}, p[2], surf2 );
-
-         v3_sub( surf0, p[0], n0 );
-         v3_sub( surf2, p[2], n2 );
-         v3_normalize( n0 );
-         v3_normalize( n2 );
-
-         world_routes_place_curve( world, route, p, n0, n2, sc );
-
-         /* --- */
-         v4_copy( p[2], p[0] );
-      }
-   }
-
-   scene_copy_slice( sc, &route->sm );
-}
-
-struct world_surface *world_tri_index_surface( world_instance *world, 
-                                                 u32 index );
-
-/* 
- * Create the strips of colour that run through the world along course paths
- */
-void world_gen_routes_generate( u32 instance_id )
-{
-   world_instance *world = &world_static.instances[ instance_id ];
-   vg_info( "Generating route meshes\n" );
-   vg_async_stall();
-
-   vg_async_item *call_scene = scene_alloc_async( &world->scene_lines, 
-                                                  &world->mesh_route_lines,
-                                                  200000, 300000 );
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-      gate->ref_count = 0;
-      gate->route_count = 0;
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route_node); i++ ){
-      ent_route_node *rn = mdl_arritm( &world->ent_route_node, i );
-      rn->ref_count = 0;
-      rn->ref_total = 0;
-   }
-
-   for( u32 k=0; k<mdl_arrcount(&world->ent_route); k++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, k );
-
-      for( int i=0; i<route->checkpoints_count; i++ ){
-         int i0 = route->checkpoints_start+i,
-             i1 = route->checkpoints_start+((i+1)%route->checkpoints_count);
-
-         ent_checkpoint *c0 = mdl_arritm(&world->ent_checkpoint, i0),
-                        *c1 = mdl_arritm(&world->ent_checkpoint, i1);
-
-         ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
-         start_gate = mdl_arritm( &world->ent_gate, start_gate->target );
-         start_gate->route_count ++;
-
-         if( !c0->path_count )
-            continue;
-
-         for( int j=0; j<c0->path_count; j ++ ){
-            ent_path_index *index = mdl_arritm( &world->ent_path_index, 
-                                                c0->path_start+j );
-            ent_route_node *rn = mdl_arritm( &world->ent_route_node,
-                                             index->index );
-            rn->ref_total ++;
-         }
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      world_routes_gen_meshes( world, i, &world->scene_lines );
-   }
-
-   vg_async_dispatch( call_scene, async_scene_upload );
-   world_routes_clear( world );
-}
-
-/* load all routes from model header */
-void world_gen_routes_ent_init( world_instance *world )
-{
-   vg_info( "Initializing routes\n" );
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-      for( u32 j=0; j<4; j++ ){
-         gate->routes[j] = 0xffff;
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm(&world->ent_route,i);
-      mdl_transform_m4x3( &route->anon.transform, route->board_transform );
-
-      route->flags = 0x00;
-      route->best_laptime = 0.0;
-      route->ui_stopper = 0.0f;
-      route->ui_residual = 0.0f;
-      
-      if( mdl_arrcount(&world->ent_region) )
-         route->flags |= k_ent_route_flag_out_of_zone;
-
-      route->anon.official_track_id = 0xffffffff;
-      for( u32 j=0; j<VG_ARRAY_LEN(track_infos); j ++ ){
-         if( !strcmp(track_infos[j].name, 
-                     mdl_pstr(&world->meta,route->pstr_name))){
-            route->anon.official_track_id = j;
-         }
-      }
-
-      for( u32 j=0; j<route->checkpoints_count; j++ ){
-         u32 id = route->checkpoints_start + j;
-         ent_checkpoint *cp = mdl_arritm(&world->ent_checkpoint,id);
-         
-         ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
-
-         for( u32 k=0; k<4; k++ ){
-            if( gate->routes[k] == 0xffff ){
-               gate->routes[k] = i;
-               break;
-            }
-         }
-
-         if( (gate->flags & k_ent_gate_linked) &
-            !(gate->flags & k_ent_gate_nonlocal) ){
-            gate = mdl_arritm(&world->ent_gate, gate->target );
-
-            for( u32 k=0; k<4; k++ ){
-               if( gate->routes[k] == i ){
-                  vg_error( "already assigned route to gate\n" );
-                  break;
-               }
-               if( gate->routes[k] == 0xffff ){
-                  gate->routes[k] = i;
-                  break;
-               }
-            }
-         }
-      }
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
-      ent_gate *gate = mdl_arritm( &world->ent_gate, i );
-   }
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_checkpoint); i++ ){
-      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, i );
-      cp->best_time = 0.0;
-   }
-
-   world_routes_clear( world );
-}
-
-void world_routes_recv_scoreboard( world_instance *world, 
-                                   vg_msg *body, u32 route_id,
-                                   enum request_status status )
-{
-   if( route_id >= mdl_arrcount( &world->ent_route ) ){
-      vg_error( "Scoreboard route_id out of range (%u)\n", route_id );
-      return;
-   }
-
-   struct leaderboard_cache *board = &world->leaderboard_cache[ route_id ];
-   board->status = status;
-
-   if( body == NULL ){
-      board->data_len = 0;
-      return;
-   }
-
-   if( body->max > NETWORK_REQUEST_MAX ){
-      vg_error( "Scoreboard leaderboard too big (%u>%u)\n", body->max,
-                NETWORK_REQUEST_MAX );
-      return;
-   }
-
-   memcpy( board->data, body->buf, body->max );
-   board->data_len = body->max;
-}
-
-/* 
- * -----------------------------------------------------------------------------
- *                                    Events
- * -----------------------------------------------------------------------------
- */
-
-void world_routes_init(void)
-{
-   world_static.current_run_version = 200;
-   world_static.time = 300.0;
-   world_static.last_use = 0.0;
-}
-
-void world_routes_update( world_instance *world )
-{
-   world_static.time += vg.time_delta;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-      
-      int target = route->active_checkpoint == 0xffff? 0: 1;
-      route->factive = vg_lerpf( route->factive, target, 
-                                 0.6f*vg.time_frame_delta );
-   }
-
-   for( u32 i=0; i<world_render.text_particle_count; i++ ){
-      struct text_particle *particle = &world_render.text_particles[i];
-      //rb_object_debug( &particle->obj, VG__RED );
-   }
-}
-
-void world_routes_fixedupdate( world_instance *world )
-{
-   rb_solver_reset();
-
-   rigidbody _null = {0};
-   _null.inv_mass = 0.0f;
-   m3x3_zero( _null.iI );
-
-   for( u32 i=0; i<world_render.text_particle_count; i++ ){
-      struct text_particle *particle = &world_render.text_particles[i];
-
-      if( rb_global_has_space() ){
-         rb_ct *buf = rb_global_buffer();
-
-         int l = rb_sphere__scene( particle->rb.to_world,
-                                   particle->radius,
-                                   NULL, world->geo_bh, buf,
-                                   k_material_flag_ghosts );
-
-         for( int j=0; j<l; j++ ){
-            buf[j].rba = &particle->rb;
-            buf[j].rbb = &_null;
-         }
-
-         rb_contact_count += l;
-      }
-   }
-
-   rb_presolve_contacts( rb_contact_buffer, 
-                         vg.time_fixed_delta, rb_contact_count );
-
-   for( int i=0; i<rb_contact_count; i++ ){
-      rb_contact_restitution( rb_contact_buffer+i, vg_randf64(&vg.rand) );
-   }
-
-   for( int i=0; i<6; i++ ){
-      rb_solve_contacts( rb_contact_buffer, rb_contact_count );
-   }
-
-   for( u32 i=0; i<world_render.text_particle_count; i++ ){
-      struct text_particle *particle = &world_render.text_particles[i];
-      rb_iter( &particle->rb );
-   }
-
-   for( u32 i=0; i<world_render.text_particle_count; i++ ){
-      struct text_particle *particle = &world_render.text_particles[i];
-      rb_update_matrices( &particle->rb );
-   }
-}
-
-void bind_terrain_noise(void);
-void world_bind_light_array( world_instance *world,
-                             GLuint shader, GLuint location, 
-                             int slot );
-void world_bind_light_index( world_instance *world,
-                             GLuint shader, GLuint location, 
-                             int slot );
-
-void world_routes_update_timer_texts( world_instance *world )
-{
-   world_render.timer_text_count = 0;
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-
-      if( route->active_checkpoint != 0xffff ){
-         u32 next = route->active_checkpoint+1;
-             next = next % route->checkpoints_count;
-             next += route->checkpoints_start;
-
-         ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, next );
-         ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
-         ent_gate *dest = mdl_arritm( &world->ent_gate, gate->target );
-         
-         u32 j=0;
-         for( ; j<4; j++ ){
-            if( dest->routes[j] == i ){
-               break;
-            }
-         }
-
-         float h0 = 0.8f,
-               h1 = 1.2f,
-               depth = 0.4f,
-               size = 0.4f;
-
-         struct timer_text *text = 
-            &world_render.timer_texts[ world_render.timer_text_count ++ ];
-
-         text->gate = gate;
-         text->route = route;
-
-         vg_str str;
-         vg_strnull( &str, text->text, sizeof(text->text) );
-
-         if( route->valid_checkpoints >= route->checkpoints_count )
-         {
-            double lap_time = world_static.time - route->timing_base,
-                   time_centiseconds = lap_time * 100.0;
-
-            if( time_centiseconds > (float)0xfffe ) time_centiseconds = 0.0;
-
-            u16 centiseconds = time_centiseconds,
-                seconds      = centiseconds / 100,
-                minutes      = seconds / 60;
-
-            centiseconds %= 100;
-            seconds     %= 60;
-            minutes     %= 60;
-
-            if( minutes > 9 ) 
-               minutes = 9;
-            
-            if( minutes )
-            {
-               vg_strcati32r( &str, minutes, 1, ' ' );
-               vg_strcatch( &str, ':' );
-            }
-            
-            if( seconds >= 10 || minutes )
-            {
-               vg_strcati32r( &str, seconds, 2, '0' );
-            }
-            else
-            {
-               vg_strcati32r( &str, seconds, 1, '0' );
-            }
-
-            vg_strcatch( &str, '.' );
-            vg_strcati32r( &str, centiseconds, 1, '0' );
-         }
-         else
-         {
-            vg_strcati32r( &str, route->valid_checkpoints, 1, ' ' );
-            vg_strcatch( &str, '/' );
-            vg_strcati32r( &str, route->checkpoints_count + 1, 1, ' ' );
-         }
-   
-         gui_font3d.font = &gui.font;
-         float align_r  = font3d_string_width( 0, text->text );
-               align_r *= size;
-
-         v3f positions[] = {
-            { -0.92f, h0, depth },
-            {  0.92f - align_r, h0, depth },
-            { -0.92f, h1, depth },
-            {  0.92f - align_r, h1, depth },
-         };
-
-         if( dest->route_count == 1 ){
-            positions[0][0] = -align_r*0.5f;
-            positions[0][1] = h1;
-         }
-
-         m4x3f mmdl;
-         ent_gate_get_mdl_mtx( gate, mmdl );
-
-         m3x3_copy( mmdl, text->transform );
-         float ratio = v3_length(text->transform[0]) / 
-                        v3_length(text->transform[1]);
-
-         m3x3_scale( text->transform, (v3f){ size, size*ratio, 0.1f } );
-         m4x3_mulv( mmdl, positions[j], text->transform[3] );
-      }
-   }
-}
-
-void world_routes_fracture( world_instance *world, ent_gate *gate,
-                            v3f imp_co, v3f imp_v )
-{
-   world_render.text_particle_count = 0;
-   
-   for( u32 i=0; i<world_render.timer_text_count; i++ ){
-      struct timer_text *text = &world_render.timer_texts[i];
-
-      if( text->gate != gate ) continue;
-
-      m4x3f transform;
-      m4x3_mul( gate->transport, text->transform, transform );
-
-      v3f co, s;
-      v4f q;
-      m4x3_decompose( transform, co, q, s );
-
-      v3f offset;
-      v3_zero( offset );
-
-      v4f colour;
-      float brightness = 0.3f + world->ub_lighting.g_day_phase;
-      v3_muls( text->route->colour, brightness, colour );
-      colour[3] = 1.0f-text->route->factive;
-
-      for( u32 j=0;; j++ ){
-         char c = text->text[j];
-         if( !c ) break;
-
-         ent_glyph *glyph = font3d_glyph( &gui.font, 0, c );
-         if( !glyph ) continue;
-
-         if( c >= (u32)'0' && c <= (u32)'9' && glyph->indice_count ){
-            struct text_particle *particle = 
-               &world_render.text_particles[world_render.text_particle_count++];
-
-            particle->glyph = glyph;
-            v4_copy( colour, particle->colour );
-
-            v3f origin;
-            v2_muls( glyph->size, 0.5f, origin );
-            origin[2] = -0.5f;
-
-            v3f world_co;
-
-            v3_add( offset, origin, world_co );
-            m4x3_mulv( transform, world_co, world_co );
-
-
-            m3x3_identity( particle->mlocal );
-            m3x3_scale( particle->mlocal, s );
-            origin[2] *= s[2];
-            v3_muls( origin, -1.0f, particle->mlocal[3] );
-
-            v3_copy( world_co, particle->rb.co );
-            v3_muls( imp_v, 1.0f+vg_randf64(&vg.rand), particle->rb.v );
-            particle->rb.v[1] += 2.0f;
-
-            v4_copy( q, particle->rb.q );
-            particle->rb.w[0] = vg_randf64(&vg.rand)*2.0f-1.0f;
-            particle->rb.w[1] = vg_randf64(&vg.rand)*2.0f-1.0f;
-            particle->rb.w[2] = vg_randf64(&vg.rand)*2.0f-1.0f;
-
-            f32 r = vg_maxf( s[0]*glyph->size[0], s[1]*glyph->size[1] )*0.5f;
-            particle->radius = r*0.6f;
-            rb_setbody_sphere( &particle->rb, particle->radius, 1.0f, 1.0f );
-         }
-         offset[0] += glyph->size[0];
-      }
-   }
-}
-
-static void render_gate_markers( m4x3f world_mmdl, int run_id, ent_gate *gate ){
-   for( u32 j=0; j<4; j++ ){
-      if( gate->routes[j] == run_id ){
-         m4x3f mmdl;
-         m4x3_copy( gate->to_world, mmdl );
-         m3x3_scale( mmdl, (v3f){ gate->dimensions[0], 
-                                  gate->dimensions[1], 1.0f } );
-
-         m4x3_mul( world_mmdl, mmdl, mmdl );
-         shader_model_gate_uMdl( mmdl );
-         mdl_draw_submesh( &world_gates.sm_marker[j] );
-         break;
-      }
-   }
-}
-
-void render_world_routes( world_instance *world, 
-                          world_instance *host_world,
-                          m4x3f mmdl, vg_camera *cam, 
-                          int viewing_from_gate, int viewing_from_hub )
-{
-   shader_scene_route_use();
-   shader_scene_route_uTexGarbage(0);
-   world_link_lighting_ub( host_world, _shader_scene_route.id );
-   world_bind_position_texture( host_world, _shader_scene_route.id, 
-                        _uniform_scene_route_g_world_depth, 2 );
-   world_bind_light_array( host_world, _shader_scene_route.id,
-                        _uniform_scene_route_uLightsArray, 3 );
-   world_bind_light_index( host_world, _shader_scene_route.id,
-                                 _uniform_scene_route_uLightsIndex, 4 );
-   bind_terrain_noise();
-
-   shader_scene_route_uPv( cam->mtx.pv );
-
-   if( viewing_from_hub ){
-      m4x4f m4mdl, pvm;
-      m4x3_expand( mmdl, m4mdl );
-      m4x4_mul( cam->mtx_prev.pv, m4mdl, pvm );
-      shader_scene_route_uMdl( mmdl );
-      shader_scene_route_uPvmPrev( pvm );
-
-      m3x3f mnormal;
-      m3x3_inv( mmdl, mnormal );
-      m3x3_transpose( mnormal, mnormal );
-      v3_normalize( mnormal[0] );
-      v3_normalize( mnormal[1] );
-      v3_normalize( mnormal[2] );
-      shader_scene_route_uNormalMtx( mnormal );
-   }
-   else{
-      shader_scene_route_uMdl( mmdl );
-      shader_scene_route_uPvmPrev( cam->mtx_prev.pv );
-      m3x3f ident;
-      m3x3_identity( ident );
-      shader_scene_route_uNormalMtx( ident );
-   }
-
-   shader_scene_route_uCamera( cam->transform[3] );
-
-   mesh_bind( &world->mesh_route_lines );
-
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-      ent_route *route = mdl_arritm( &world->ent_route, i );
-
-      f32 t = viewing_from_hub? 1.0f: route->factive;
-
-      v4f colour;
-      v3_lerp( (v3f){0.7f,0.7f,0.7f}, route->colour, t, colour );
-      colour[3] = t*0.2f;
-
-      shader_scene_route_uColour( colour );
-      mdl_draw_submesh( &route->sm );
-   }
-
-   /* timers
-    * ---------------------------------------------------- */
-   if( !viewing_from_gate && !viewing_from_hub ){
-      font3d_bind( &gui.font, k_font_shader_default, 0, world, cam );
-
-      for( u32 i=0; i<world_render.timer_text_count; i++ ){
-         struct timer_text *text = &world_render.timer_texts[i];
-
-         v4f colour;
-         float brightness = 0.3f + world->ub_lighting.g_day_phase;
-         v3_muls( text->route->colour, brightness, colour );
-         colour[3] = 1.0f-text->route->factive;
-
-         shader_model_font_uColour( colour );
-         font3d_simple_draw( 0, text->text, cam, text->transform );
-      }
-
-      shader_model_font_uOffset( (v4f){0.0f,0.0f,0.0f,1.0f} );
-
-      for( u32 i=0; i<world_render.text_particle_count; i++ ){
-         struct text_particle *particle = &world_render.text_particles[i];
-
-         m4x4f prev_mtx;
-
-         m4x3_expand( particle->mdl, prev_mtx );
-         m4x4_mul( cam->mtx_prev.pv, prev_mtx, prev_mtx );
-
-         shader_model_font_uPvmPrev( prev_mtx );
-
-         v4f q;
-         m4x3f model;
-         rb_extrapolate( &particle->rb, model[3], q );
-         q_m3x3( q, model );
-
-         m4x3_mul( model, particle->mlocal, particle->mdl );
-         shader_model_font_uMdl( particle->mdl );
-         shader_model_font_uColour( particle->colour );
-
-         mesh_drawn( particle->glyph->indice_start, 
-                     particle->glyph->indice_count );
-      }
-   }
-
-   /* gate markers 
-    * ---------------------------------------------------- */
-
-   shader_model_gate_use();
-   shader_model_gate_uPv( cam->mtx.pv );
-   shader_model_gate_uCam( cam->pos );
-   shader_model_gate_uTime( vg.time*0.25f );
-   shader_model_gate_uInvRes( (v2f){
-         1.0f / (float)vg.window_x,
-         1.0f / (float)vg.window_y });
-
-   mesh_bind( &world_gates.mesh );
-
-   /* skip writing into the motion vectors for this */
-   glDrawBuffers( 1, (GLenum[]){ GL_COLOR_ATTACHMENT0 } );
-   glDisable( GL_CULL_FACE );
-
-   if( viewing_from_hub ){
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-
-         v4f colour;
-         v3_muls( route->colour, 1.6666f, colour );
-         colour[3] = 0.0f;
-
-         shader_model_gate_uColour( colour );
-
-         for( u32 j=0; j<mdl_arrcount(&world->ent_gate); j ++ ){
-            ent_gate *gate = mdl_arritm( &world->ent_gate, j );
-            if( !(gate->flags & k_ent_gate_nonlocal) )
-               render_gate_markers( mmdl, i, gate );
-         }
-      }
-   }
-   else{
-      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-
-         if( route->active_checkpoint != 0xffff ){
-            v4f colour;
-            float brightness = 0.3f + world->ub_lighting.g_day_phase;
-            v3_muls( route->colour, brightness, colour );
-            colour[3] = 1.0f-route->factive;
-
-            shader_model_gate_uColour( colour );
-
-            u32 next = route->active_checkpoint+1+viewing_from_gate;
-                next = next % route->checkpoints_count;
-                next += route->checkpoints_start;
-
-            ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, next );
-            ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
-            render_gate_markers( mmdl, i, gate );
-         }
-      }
-   }
-   glEnable( GL_CULL_FACE );
-   glDrawBuffers( 2, (GLenum[]){ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 } );
-}
diff --git a/world_routes.h b/world_routes.h
deleted file mode 100644 (file)
index 621c28a..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-#include "vg/vg_camera.h"
-#include "vg/vg_msg.h"
-#include "world.h"
-#include "network_msg.h"
-
-void world_routes_init(void);
-void world_routes_fracture( world_instance *world, ent_gate *gate,
-                               v3f imp_co, v3f imp_v );
-void world_routes_activate_entry_gate( world_instance *world, 
-                                          ent_gate *rg );
-void render_world_routes( world_instance *world, 
-                          world_instance *host_world,
-                          m4x3f mmdl, vg_camera *cam, 
-                          int viewing_from_gate, int viewing_from_hub );
-
-void world_gen_routes_ent_init( world_instance *world );
-void world_gen_routes_generate( u32 instance_id );
-void world_routes_update_timer_texts( world_instance *world );
-void world_routes_update( world_instance *world );
-void world_routes_fixedupdate( world_instance *world );
-void world_routes_clear( world_instance *world );
-void world_routes_recv_scoreboard( world_instance *world, 
-                                   vg_msg *body, u32 route_id,
-                                   enum request_status status );
diff --git a/world_routes_ui.c b/world_routes_ui.c
deleted file mode 100644 (file)
index 0afbeae..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-#include "skaterift.h"
-#include "world_routes_ui.h"
-#include "world_routes.h"
-#include "player.h"
-
-static u32 v4_rgba( v4f colour ){
-   u32 r = vg_minf(1.0f,colour[0])*255.0f,
-       g = vg_minf(1.0f,colour[1])*255.0f,
-       b = vg_minf(1.0f,colour[2])*255.0f,
-       a = vg_minf(1.0f,colour[3])*255.0f;
-
-   return r | (g<<8) | (b<<16) | (a<<24);
-}
-
-static void ent_route_imgui( ui_context *ctx, 
-                             world_instance *world, ent_route *route, 
-                             ui_point inout_cursor ){
-   if( route->flags & k_ent_route_flag_out_of_zone )
-      return;
-
-   u32 last_version=0;
-   f64 last_time = 0.0;
-   ent_checkpoint *last_cp = NULL;
-
-   u32 valid_sections=0;
-
-   struct time_block{
-      f32 length, best;
-      int clean;
-   }
-   blocks[ route->checkpoints_count ];
-
-   for( u32 i=0; i<route->checkpoints_count; i++ ){
-      u32 cpid  = i+route->active_checkpoint+1;
-          cpid %= route->checkpoints_count;
-          cpid += route->checkpoints_start;
-
-      ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, cpid );
-      ent_gate *rg = mdl_arritm( &world->ent_gate, cp->gate_index );
-                rg = mdl_arritm( &world->ent_gate, rg->target );
-
-      if( last_version+1 == rg->timing_version ) {
-         struct time_block *block = &blocks[ valid_sections ++ ];
-         block->clean  = (rg->flags & k_ent_gate_clean_pass)? 1: 0;
-         block->length = rg->timing_time - last_time;
-         block->best = last_cp? last_cp->best_time: 0.0f;
-      }
-      else valid_sections = 0;
-
-      last_version = rg->timing_version;
-      last_time = rg->timing_time;
-      last_cp = cp;
-   }
-
-   if( last_version+1 == world_static.current_run_version ){
-      struct time_block *block = &blocks[ valid_sections ++ ];
-      block->clean = localplayer.rewinded_since_last_gate? 0: 1;
-      block->length = world_static.time - last_time;
-      block->best = last_cp->best_time;
-   }
-   else 
-      valid_sections = 0;
-
-   u32 colour = v4_rgba( route->colour ) | 0xff000000;
-
-   ui_px x = 0,
-         h = route->factive * 16.0f,
-         base = inout_cursor[0];//(f32)vg.window_x*0.5f - route->ui_stopper;
-
-   if( route->ui_residual > 0.0f )
-   {
-      ui_px w = route->ui_residual_block_w,
-            total = w + 4;
-
-      f32 t = vg_smoothstepf(1.0f-route->ui_residual);
-      
-      x -= (f32)total * t;
-
-      ui_rect rect = { base+x, inout_cursor[1], w, h };
-
-      v4f fadecolour;
-      v4_copy( route->colour, fadecolour );
-      fadecolour[3] *= route->ui_residual;
-
-      ui_fill( ctx, rect, v4_rgba(fadecolour) );
-
-      x += total;
-   }
-
-   int got_first = 0;
-
-   for( u32 i=0; i<valid_sections; i ++ )
-   {
-      struct time_block *block = &blocks[ i ];
-      ui_px w = 20 + (block->length * 6.0f);
-      ui_rect rect = { base+x, inout_cursor[1], w, h };
-      ui_fill( ctx, rect, colour );
-   
-      if( block->clean )
-         ui_outline( ctx, rect, 1, 0xff00ffff, 0 );
-
-      if( block->best != 0.0f )
-      {
-         char buf[32];
-         vg_str str;
-         vg_strnull( &str, buf, 32 );
-         
-         f32 diff = block->length - block->best,
-             as = fabsf(diff),
-             s  = floorf( as ),
-             ds = floorf( vg_fractf( as ) * 10.0f );
-
-         if( (block->best != 0.0f) && (fabsf(diff) > 0.001f) )
-         {
-            if( diff > 0.0f )
-               vg_strcatch( &str, '+' );
-            else
-               vg_strcatch( &str, '-' );
-
-            vg_strcati32( &str, s );
-            vg_strcatch( &str, '.' );
-            vg_strcati32( &str, ds );
-
-            ui_text( ctx, rect, buf, 1, k_ui_align_middle_center, 0 );
-         }
-      }
-
-      x += w + 4;
-
-      if( !got_first ){
-         route->ui_first_block_width = w;
-         got_first = 1;
-      }
-   }
-
-   for( u32 i=0; i<route->checkpoints_count-valid_sections; i++ )
-   {
-      struct time_block *block = &blocks[ i ];
-
-      ui_px w = 20;
-      ui_rect rect = { base+x, inout_cursor[1], w, h };
-      ui_outline( ctx, rect, -1, colour, 0 );
-      x += w + 4;
-
-      if( !got_first )
-      {
-         route->ui_first_block_width = w;
-         got_first = 1;
-      }
-   }
-
-   inout_cursor[1] += h + 4;
-
-   vg_slewf( &route->ui_residual, 0.0f, vg.time_frame_delta );
-   route->ui_stopper = vg_lerpf( route->ui_stopper, (f32)x*0.5f, 
-                                 vg.time_frame_delta );
-}
-
-void world_routes_imgui( ui_context *ctx, world_instance *world )
-{
-   if( skaterift.activity == k_skaterift_menu ) return;
-
-   ui_point cursor = { 4, 4 };
-   for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ )
-   {
-      ent_route_imgui( ctx, world, mdl_arritm( &world->ent_route, i ), cursor );
-   }
-}
diff --git a/world_routes_ui.h b/world_routes_ui.h
deleted file mode 100644 (file)
index 70c0fcd..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#pragma once
-#include "world_routes.h"
-
-struct route_ui{};
-void world_routes_imgui( ui_context *ctx, world_instance *world );
diff --git a/world_sfd.c b/world_sfd.c
deleted file mode 100644 (file)
index 6473d63..0000000
+++ /dev/null
@@ -1,362 +0,0 @@
-#ifndef SFD_C
-#define SFD_C
-
-#include "world_sfd.h"
-#include "shaders/scene_scoretext.h"
-#include "shaders/scene_vertex_blend.h"
-#include "network.h"
-#include "entity.h"
-#include "network_common.h"
-#include "world_routes.h"
-
-struct world_sfd world_sfd;
-
-static f32 sfd_encode_glyph( char c ){
-   int value = 0;
-   if( c >= 'a' && c <= 'z' )
-      value = c-'a'+11;
-   else if( c >= '0' && c <= '9' )
-      value = c-'0'+1;
-   else if( c >= 'A' && c <= 'Z' )
-      value = c-'A'+11;
-   else if( c >= '\x01' && c <= '\x01'+10 )
-      value = 63-c;
-   else{
-      int base = 11+26;
-
-      switch( c ){
-         case '!': value=base+0; break;
-         case '?': value=base+1; break;
-         case ',': value=base+2; break;
-         case '.': value=base+3; break;
-         case '#': value=base+4; break;
-         case '$': value=base+5; break;
-         case '%': value=base+6; break;
-         case '*': value=base+7; break;
-         case '+': value=base+8; break;
-         case '-': value=base+9; break;
-         case '/': value=base+10; break;
-         case ':': value=base+11; break;
-         default: value=0; break;
-      }
-   }
-
-   return (float)value;
-}
-
-static void sfd_clear( u32 row ){
-   u32 row_h = world_sfd.h -1 -row;
-   for( int i=0; i<world_sfd.w; i++ ){
-      u32 idx = (world_sfd.w*row_h + i) * 2;
-      world_sfd.buffer[idx] = 0.0f;
-   }
-}
-
-void sfd_encode( v2i co, const char *str, enum world_sfd_align align )
-{
-   i32 row_h = world_sfd.h -1 -co[1];
-   i32 offset_x = 0;
-
-   i32 w = VG_MIN( strlen(str), world_sfd.w );
-   if( align == k_world_sfd_center )
-      offset_x = (world_sfd.w - w) / 2;
-   else if( align == k_world_sfd_right )
-      offset_x = world_sfd.w - w;
-   else
-      offset_x = co[0];
-
-   for( i32 i=0; i<world_sfd.w; i++ ){
-      i32 u = i + offset_x,
-          idx = (world_sfd.w*row_h + u) * 2;
-
-      if( (u >= world_sfd.w) || (u < 0) )
-         continue;
-
-      if( !str[i] ) 
-         return;
-
-      world_sfd.buffer[idx] = sfd_encode_glyph( str[i] );
-   }
-}
-
-void world_sfd_compile_scores( struct leaderboard_cache *board,
-                               const char *title )
-{
-   for( u32 i=0; i<13; i++ )
-      sfd_clear(i);
-
-   sfd_encode( (v2i){0,0}, title, k_world_sfd_left );
-
-   if( !board ){
-      sfd_encode( (v2i){-1,4}, "Error out of range", k_world_sfd_center );
-      return;
-   }
-
-   if( !network_connected() ){
-      sfd_encode( (v2i){-1,0}, "Offline", k_world_sfd_right );
-      return;
-   }
-
-   if( board->status == k_request_status_not_found ){
-      sfd_encode( (v2i){-1,4}, "No records", k_world_sfd_center );
-      return;
-   }
-
-   if( board->status != k_request_status_ok ){
-      char buf[32];
-      vg_str s;
-      vg_strnull( &s, buf, 32 );
-      vg_strcat( &s, "Error: " );
-      vg_strcati32( &s, board->status );
-      sfd_encode( (v2i){-1,4}, buf, k_world_sfd_center );
-      return;
-   }
-
-   vg_msg body;
-   vg_msg_init( &body, board->data, board->data_len );
-
-   const char *alias = "rows";
-
-   if( world_sfd.view_weekly ){
-      alias = "rows_weekly";
-      sfd_encode( (v2i){-1,0}, "Weekly", k_world_sfd_right );
-   }
-   else {
-      sfd_encode( (v2i){-1,0}, "All-Time", k_world_sfd_right );
-   }
-
-   u32 l = 1;
-   if( vg_msg_seekframe( &body, alias ) ){
-      while( vg_msg_seekframe( &body, NULL ) ){
-         /* name */
-         const char *username = vg_msg_getkvstr( &body, "username" );
-
-         char buf[100];
-         vg_str str;
-         vg_strnull( &str, buf, 100 );
-         vg_strcati32( &str, l );
-         vg_strcat( &str, " " );
-
-         if( username )
-            vg_strcat( &str, username );
-         else
-            vg_strcat( &str, "??????" );
-
-         sfd_encode( (v2i){0,l}, str.buffer, k_world_sfd_left );
-
-         /* time */
-         vg_strnull( &str, buf, 100 );
-         
-         u32 centiseconds;
-         vg_msg_getkvintg( &body, "time", k_vg_msg_i32, &centiseconds, NULL );
-
-         i32 seconds      = centiseconds / 100,
-             minutes      = seconds / 60;
-
-         centiseconds %= 100;
-         seconds     %= 60;
-         minutes     %= 60;
-         if( minutes > 9 ) vg_strcat( &str, "?" );
-         else vg_strcati32( &str, minutes );
-         vg_strcat( &str, ":" );
-         vg_strcati32r( &str, seconds, 2, '0' );
-         vg_strcat( &str, "." );
-         vg_strcati32r( &str, centiseconds, 2, '0' );
-         sfd_encode( (v2i){-1,l}, str.buffer, k_world_sfd_right );
-         l ++;
-
-         vg_msg_skip_frame( &body );
-      }
-   }
-   else {
-      sfd_encode( (v2i){-1,4}, "No records", k_world_sfd_center );
-   }
-}
-
-void world_sfd_compile_active_scores(void)
-{
-   world_instance *world = world_current_instance();
-   
-   struct leaderboard_cache *board = NULL;
-   const char *name = "Out of range";
-
-   if( world_sfd.active_route_board < mdl_arrcount( &world->ent_route ) ){
-      board = &world->leaderboard_cache[ world_sfd.active_route_board ];
-      ent_route *route = mdl_arritm( &world->ent_route, 
-                                     world_sfd.active_route_board );
-      name = mdl_pstr( &world->meta, route->pstr_name );
-   }
-         
-   world_sfd_compile_scores( board, name );
-}
-
-void world_sfd_update( world_instance *world, v3f pos )
-{
-   if( mdl_arrcount( &world->ent_route ) ){
-      u32 closest = 0;
-      float min_dist = INFINITY;
-
-      for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
-         ent_route *route = mdl_arritm( &world->ent_route, i );
-         float dist = v3_dist2( route->board_transform[3], pos );
-
-         if( dist < min_dist ){
-            min_dist = dist;
-            closest = i;
-         }
-      }
-
-      struct leaderboard_cache *board = &world->leaderboard_cache[ closest ];
-
-      /* request new board if cache expires */
-      if( network_connected() ){
-         f64 delta = vg.time_real - board->cache_time;
-         if( (delta > 45.0) || (board->cache_time == 0.0) ){
-            board->cache_time = vg.time_real;
-            ent_route *route = mdl_arritm( &world->ent_route, closest );
-            addon_reg *world_reg = 
-               world_static.instance_addons[ world - world_static.instances ];
-
-            char mod_uid[ ADDON_UID_MAX ];
-            addon_alias_uid( &world_reg->alias, mod_uid );
-
-            network_request_scoreboard( 
-                  mod_uid, 
-                  mdl_pstr( &world->meta, route->pstr_name ),
-                  NETWORK_LEADERBOARD_ALLTIME_AND_CURRENT_WEEK, closest );
-         }
-      }
-
-      /* compile board text if we changed. */
-      if( world_sfd.active_route_board != closest ){
-         world_sfd_compile_active_scores();
-      }
-
-      world_sfd.active_route_board = closest;
-   }
-
-   for( int i=0; i<world_sfd.w*world_sfd.h; i++ ){
-      float *target = &world_sfd.buffer[i*2+0],
-            *cur =    &world_sfd.buffer[i*2+1];
-      
-      float const rate = vg.time_delta * 25.2313131414f;
-      float d1 = *target-*cur;
-      
-      if( fabsf(d1) > rate ){
-         *cur += rate;
-         if( *cur > 49.0f )
-            *cur -= 49.0f;
-      }
-      else
-         *cur = *target;
-   }
-}
-
-void bind_terrain_noise(void);
-void sfd_render( world_instance *world, vg_camera *cam, m4x3f transform )
-{
-   mesh_bind( &world_sfd.mesh_display );
-   shader_scene_scoretext_use();
-   shader_scene_scoretext_uTexMain(1);
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_scoretext );
-
-   bind_terrain_noise();
-
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_sfd.tex_scoretex );
-
-   m4x4f pvm_prev;
-   m4x3_expand( transform, pvm_prev );
-   m4x4_mul( cam->mtx_prev.pv, pvm_prev, pvm_prev );
-
-   shader_scene_scoretext_uPv( cam->mtx.pv );
-   shader_scene_scoretext_uPvmPrev( pvm_prev );
-   shader_scene_scoretext_uMdl( transform );
-   shader_scene_scoretext_uCamera( cam->transform[3] );
-
-   for( int y=0;y<world_sfd.h; y++ ){
-      for( int x=0; x<world_sfd.w; x++ ){
-         float value = world_sfd.buffer[(y*world_sfd.w+x)*2+1];
-         shader_scene_scoretext_uInfo( (v3f){ x,y, value } );
-         mesh_draw( &world_sfd.mesh_display );
-      }
-   }
-
-   shader_scene_vertex_blend_use();
-   shader_scene_vertex_blend_uTexGarbage(0);
-   shader_scene_vertex_blend_uTexGradients(1);
-   WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_vertex_blend );
-
-   bind_terrain_noise();
-   glActiveTexture( GL_TEXTURE1 );
-   glBindTexture( GL_TEXTURE_2D, world_sfd.tex_scoretex );
-
-   shader_scene_vertex_blend_uPv( cam->mtx.pv );
-   shader_scene_vertex_blend_uPvmPrev( pvm_prev );
-   shader_scene_vertex_blend_uMdl( transform );
-   shader_scene_vertex_blend_uCamera( cam->transform[3] );
-   
-   mesh_bind( &world_sfd.mesh_base );
-   mdl_draw_submesh( &world_sfd.sm_base );
-}
-
-void world_sfd_init(void)
-{
-   vg_info( "world_sfd_init\n" );
-   vg_linear_clear( vg_mem.scratch );
-
-   mdl_context mscoreboard;
-   mdl_open( &mscoreboard, "models/rs_scoretext.mdl", vg_mem.scratch );
-   mdl_load_metadata_block( &mscoreboard, vg_mem.scratch );
-   mdl_async_load_glmesh( &mscoreboard, &world_sfd.mesh_base, NULL );
-
-   mdl_load_mesh_block( &mscoreboard, vg_mem.scratch );
-
-   scene_context *scene = &world_sfd.scene;
-   vg_async_item *call = scene_alloc_async( scene, &world_sfd.mesh_display,
-                                            3000, 8000 );
-
-
-   mdl_mesh *m_backer = mdl_find_mesh( &mscoreboard, "backer" ),
-            *m_card   = mdl_find_mesh( &mscoreboard, "score_card" );
-
-   mdl_submesh 
-      *sm_backer = mdl_arritm( &mscoreboard.submeshs, m_backer->submesh_start ),
-      *sm_card   = mdl_arritm( &mscoreboard.submeshs, m_card->submesh_start );
-   world_sfd.sm_base = *sm_backer;
-
-   m4x3f identity;
-   m4x3_identity( identity );
-
-   for( int i=0;i<4;i++ ){
-      u32 vert_start = scene->vertex_count;
-      scene_add_mdl_submesh( scene, &mscoreboard, sm_card, identity );
-
-      for( int j=0; j<sm_card->vertex_count; j++ ){
-         scene_vert *vert = &scene->arrvertices[ vert_start+j ];
-
-         float const k_glyph_uvw = 1.0f/64.0f;
-         vert->uv[0] -= k_glyph_uvw * (float)(i-1);
-         vert->norm[3] = i*42;
-      }
-   }
-
-   vg_async_dispatch( call, async_scene_upload );
-   vg_tex2d_load_qoi_async_file( "textures/scoretext.qoi", 
-                                 VG_TEX2D_CLAMP|VG_TEX2D_NEAREST,
-                                 &world_sfd.tex_scoretex );
-
-   mdl_close( &mscoreboard );
-
-   int w = 27,
-       h = 13;
-
-   world_sfd.w = w;
-   world_sfd.h = h;
-   world_sfd.buffer = vg_linear_alloc( vg_mem.rtmemory, 2*w*h*sizeof(float) );
-
-   for( int i=0; i<w*h*2; i++ )
-      world_sfd.buffer[i] = 0.0f;
-}
-
-#endif /* WORLD_SFD_C */
diff --git a/world_sfd.h b/world_sfd.h
deleted file mode 100644 (file)
index e79fbbc..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2021-2024 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-#pragma once
-#include "world.h"
-#include "world_routes.h"
-#include "scene.h"
-
-struct world_sfd{
-   GLuint tex_scoretex;
-
-   glmesh mesh_base, mesh_display;
-   mdl_submesh sm_base;
-
-   u32 active_route_board;
-   scene_context scene;
-
-   u32 view_weekly;
-
-   u32 w, h;
-   float *buffer;
-}
-extern world_sfd;
-void world_sfd_init(void);
-
-enum world_sfd_align {
-   k_world_sfd_left,
-   k_world_sfd_right,
-   k_world_sfd_center
-};
-
-void sfd_encode( v2i co, const char *str, enum world_sfd_align align );
-void world_sfd_update( world_instance *world, v3f pos );
-void sfd_render( world_instance *world, vg_camera *cam, m4x3f transform );
-void world_sfd_compile_scores( struct leaderboard_cache *leaderboard,
-                               const char *title );
-void world_sfd_compile_active_scores(void);
diff --git a/world_volumes.c b/world_volumes.c
deleted file mode 100644 (file)
index b74e493..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-#include "world_volumes.h"
-
-void world_volumes_update( world_instance *world, v3f pos )
-{
-   /* filter and check the existing ones */
-   u32 j=0;
-   for( u32 i=0; i<world_static.active_trigger_volume_count; i++ ){
-      i32 idx = world_static.active_trigger_volumes[i];
-      ent_volume *volume = mdl_arritm( &world->ent_volume, idx );
-
-      v3f local;
-      m4x3_mulv( volume->to_local, pos, local );
-      if( (fabsf(local[0]) <= 1.0f) &&
-          (fabsf(local[1]) <= 1.0f) &&
-          (fabsf(local[2]) <= 1.0f) )
-      {
-         world_static.active_trigger_volumes[ j ++ ] = idx;
-         boxf cube = {{-1.0f,-1.0f,-1.0f},{1.0f,1.0f,1.0f}};
-         vg_line_boxf_transformed( volume->to_world, cube, 0xff00ccff );
-      }
-      else{
-         /* 
-          * LEGACY BEHAVIOUR: < v104 does not have leave events
-          */
-         if( world->meta.info.version >= 104 ){
-            ent_call basecall;
-            basecall.function = k_ent_function_trigger_leave;
-            basecall.id = mdl_entity_id( k_ent_volume, idx );
-            basecall.data = NULL;
-
-            entity_call( world, &basecall );
-         }
-      }
-   }
-   world_static.active_trigger_volume_count = j;
-
-   static float random_accum = 0.0f;
-   random_accum += vg.time_delta;
-
-   u32 random_ticks = 0;
-
-   while( random_accum > 0.1f ){
-      random_accum -= 0.1f;
-      random_ticks ++;
-   }
-
-   float radius = 32.0f;
-
-   bh_iter it;
-   bh_iter_init_range( 0, &it, pos, radius );
-   i32 idx;
-
-   while( bh_next( world->entity_bh, &it, &idx ) ){
-      u32 id    = world->entity_list[ idx ],
-          type  = mdl_entity_id_type( id ),
-          index = mdl_entity_id_id( id );
-
-      if( type != k_ent_volume ) continue;
-
-      ent_volume *volume = mdl_arritm( &world->ent_volume, index );
-      boxf cube = {{-1.0f,-1.0f,-1.0f},{1.0f,1.0f,1.0f}};
-      
-      if( volume->flags & k_ent_volume_flag_particles ){
-         vg_line_boxf_transformed( volume->to_world, cube, 0xff00c0ff );
-
-         for( int j=0; j<random_ticks; j++ ){
-            ent_call basecall;
-            basecall.id = id;
-            basecall.data = NULL;
-            basecall.function = 0;
-
-            entity_call( world, &basecall );
-         }
-      }
-      else{
-         for( u32 i=0; i<world_static.active_trigger_volume_count; i++ )
-            if( world_static.active_trigger_volumes[i] == index )
-               goto next_volume;
-
-         if( world_static.active_trigger_volume_count > 
-               VG_ARRAY_LEN(world_static.active_trigger_volumes) ) continue;
-
-         v3f local;
-         m4x3_mulv( volume->to_local, pos, local );
-
-         if( (fabsf(local[0]) <= 1.0f) &&
-             (fabsf(local[1]) <= 1.0f) &&
-             (fabsf(local[2]) <= 1.0f) ){
-            ent_call basecall;
-            basecall.function = 0;
-            basecall.id = id;
-            basecall.data = NULL;
-
-            entity_call( world, &basecall );
-            world_static.active_trigger_volumes[ 
-               world_static.active_trigger_volume_count ++ ] = index;
-         }
-         else
-            vg_line_boxf_transformed( volume->to_world, cube, 0xffcccccc );
-      }
-next_volume:;
-   }
-}
diff --git a/world_volumes.h b/world_volumes.h
deleted file mode 100644 (file)
index 2d84e9e..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#pragma once
-#include "world.h"
-#include "vg/vg_bvh.h"
-
-void world_volumes_update( world_instance *world, v3f pos );
diff --git a/world_water.c b/world_water.c
deleted file mode 100644 (file)
index 3d95f09..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#include "world_water.h"
-#include "world_render.h"
-#include "render.h"
-#include "shaders/scene_water.h"
-#include "shaders/scene_water_fast.h"
-#include "scene.h"
-#include "player.h"
-#include "player_walk.h"
-#include "player_dead.h"
-
-struct world_water world_water;
-
-void world_water_init(void)
-{
-   vg_info( "world_water_init\n" );
-
-   vg_tex2d_load_qoi_async_file( "textures/water_surf.qoi",
-                                 VG_TEX2D_LINEAR|VG_TEX2D_REPEAT,
-                                 &world_water.tex_water_surf );
-
-   vg_success( "done\n" );
-}
-
-void water_set_surface( world_instance *world, float height )
-{
-   world->water.height = height;
-   v4_copy( (v4f){ 0.0f, 1.0f, 0.0f, height }, world->water.plane );
-}
-
-void world_link_lighting_ub( world_instance *world, GLuint shader );
-void world_bind_position_texture( world_instance *world, 
-                                     GLuint shader, GLuint location,
-                                     int slot );
-void world_bind_light_array( world_instance *world,
-                                GLuint shader, GLuint location, 
-                                int slot );
-void world_bind_light_index( world_instance *world,
-                                       GLuint shader, GLuint location, 
-                                       int slot );
-
-/*
- * Does not write motion vectors
- */
-void render_water_texture( world_instance *world, vg_camera *cam )
-{
-   if( !world->water.enabled || (vg.quality_profile == k_quality_profile_low) )
-      return;
-
-   /* Draw reflection buffa */
-   vg_framebuffer_bind( g_render.fb_water_reflection, k_render_scale );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-
-   /* 
-    * Create flipped view matrix. Don't care about motion vectors
-    */
-   float cam_height = cam->transform[3][1] - world->water.height;
-
-   vg_camera water_cam;
-   water_cam.farz = cam->farz;
-   water_cam.nearz = cam->nearz;
-   v3_copy( cam->transform[3], water_cam.transform[3] );
-   water_cam.transform[3][1] -= 2.0f * cam_height;
-
-   m3x3f flip;
-   m3x3_identity( flip );
-   flip[1][1] = -1.0f;
-   m3x3_mul( flip, cam->transform, water_cam.transform );
-
-   vg_camera_update_view( &water_cam );
-
-   /* 
-    * Create clipped projection 
-    */
-   v4f clippa = { 0.0f, 1.0f, 0.0f, world->water.height-0.1f };
-   m4x3_mulp( water_cam.transform_inverse, clippa, clippa );
-   clippa[3] *= -1.0f;
-
-   m4x4_copy( cam->mtx.p, water_cam.mtx.p );
-   m4x4_clip_projection( water_cam.mtx.p, clippa );
-
-   vg_camera_finalize( &water_cam );
-
-   /*
-    * Draw world
-    */
-   glEnable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-   glCullFace( GL_FRONT );
-   render_world( world, &water_cam, 0, 1, 0, 1 );
-   glCullFace( GL_BACK );
-   
-   /*
-    * Create beneath view matrix
-    */
-   vg_camera beneath_cam;
-   vg_framebuffer_bind( g_render.fb_water_beneath, k_render_scale );
-   glClearColor( 1.0f, 0.0f, 0.0f, 0.0f );
-   glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );
-
-   m4x3_copy( cam->transform, beneath_cam.transform );
-   vg_camera_update_view( &beneath_cam );
-
-   float bias = -(cam->transform[3][1]-world->water.height)*0.1f;
-
-   v4f clippb = { 0.0f, -1.0f, 0.0f, -(world->water.height) + bias };
-   m4x3_mulp( beneath_cam.transform_inverse, clippb, clippb );
-   clippb[3] *= -1.0f;
-
-   m4x4_copy( cam->mtx.p, beneath_cam.mtx.p );
-   m4x4_clip_projection( beneath_cam.mtx.p, clippb );
-   vg_camera_finalize( &beneath_cam );
-
-   glEnable( GL_DEPTH_TEST );
-   glDisable( GL_BLEND );
-   render_world_depth( world, &beneath_cam );
-   //glViewport( 0,0, g_render_x, g_render_y );
-}
-
-void render_water_surface( world_instance *world, vg_camera *cam )
-{
-   if( !world->water.enabled )
-      return;
-
-   if( vg.quality_profile == k_quality_profile_high )
-   {
-      /* Draw surface */
-      shader_scene_water_use();
-      
-      vg_framebuffer_bind_texture( g_render.fb_water_reflection, 0, 0 );
-      shader_scene_water_uTexMain( 0 );
-   
-      glActiveTexture( GL_TEXTURE1 );
-      glBindTexture( GL_TEXTURE_2D, world_water.tex_water_surf );
-      shader_scene_water_uTexDudv( 1 );
-      
-      shader_scene_water_uInvRes( (v2f){
-            1.0f / (float)vg.window_x,
-            1.0f / (float)vg.window_y });
-
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_water );
-
-      vg_framebuffer_bind_texture( g_render.fb_water_beneath, 0, 5 );
-      shader_scene_water_uTexBack( 5 );
-      shader_scene_water_uTime( world_static.time );
-      shader_scene_water_uCamera( cam->transform[3] );
-      shader_scene_water_uSurfaceY( world->water.height );
-
-      shader_scene_water_uPv( cam->mtx.pv );
-      shader_scene_water_uPvmPrev( cam->mtx_prev.pv );
-
-      m4x3f full;
-      m4x3_identity( full );
-      shader_scene_water_uMdl( full );
-
-      glEnable(GL_BLEND);
-      glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
-      glBlendEquation(GL_FUNC_ADD);
-
-      mesh_bind( &world->mesh_no_collide );
-
-      for( int i=0; i<world->surface_count; i++ )
-      {
-         struct world_surface *mat = &world->surfaces[i];
-         struct shader_props_water *props = mat->info.props.compiled;
-
-         if( mat->info.shader == k_shader_water )
-         {
-            shader_scene_water_uShoreColour( props->shore_colour );
-            shader_scene_water_uOceanColour( props->deep_colour );
-            shader_scene_water_uFresnel( props->fresnel );
-            shader_scene_water_uWaterScale( props->water_sale );
-            shader_scene_water_uWaveSpeed( props->wave_speed );
-
-            mdl_draw_submesh( &mat->sm_no_collide );
-         }
-      }
-
-      glDisable(GL_BLEND);
-   }
-   else if( (vg.quality_profile == k_quality_profile_low) ||
-            (vg.quality_profile == k_quality_profile_min) )
-   {
-      shader_scene_water_fast_use();
-
-      glActiveTexture( GL_TEXTURE1 );
-      glBindTexture( GL_TEXTURE_2D, world_water.tex_water_surf );
-      shader_scene_water_fast_uTexDudv( 1 );
-
-      shader_scene_water_fast_uTime( world_static.time );
-      shader_scene_water_fast_uCamera( cam->transform[3] );
-      shader_scene_water_fast_uSurfaceY( world->water.height );
-
-      WORLD_BIND_LIGHT_BUFFERS_UB0_TEX234( world, scene_water_fast );
-
-      m4x3f full;
-      m4x3_identity( full );
-      shader_scene_water_fast_uMdl( full );
-      shader_scene_water_fast_uPv( cam->mtx.pv );
-      shader_scene_water_fast_uPvmPrev( cam->mtx_prev.pv );
-
-      glEnable(GL_BLEND);
-      glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
-      glBlendEquation(GL_FUNC_ADD);
-
-      mesh_bind( &world->mesh_no_collide );
-
-      for( int i=0; i<world->surface_count; i++ )
-      {
-         struct world_surface *mat = &world->surfaces[i];
-         struct shader_props_water *props = mat->info.props.compiled;
-
-         if( mat->info.shader == k_shader_water )
-         {
-            shader_scene_water_fast_uShoreColour( props->shore_colour );
-            shader_scene_water_fast_uOceanColour( props->deep_colour );
-
-            mdl_draw_submesh( &mat->sm_no_collide );
-         }
-      }
-
-      glDisable(GL_BLEND);
-   }
-}
-
-static void world_water_drown(void)
-{
-   if( localplayer.drowned ) return;
-   player__networked_sfx( k_player_subsystem_walk, 32, 
-                          k_player_walk_soundeffect_splash,
-                          localplayer.rb.co, 1.0f );
-   vg_info( "player fell of due to walking into walker\n" );
-   localplayer.drowned = 1;
-   player__dead_transition( k_player_die_type_generic );
-}
-
-bool world_water_player_safe( world_instance *world, f32 allowance )
-{
-   if( !world->water.enabled ) return 1;
-   if( world->info.flags & 0x2 ) return 1;
-
-   if( localplayer.rb.co[1]+allowance < world->water.height )
-   {
-      world_water_drown();
-      return 0;
-   }
-
-   return 1;
-}
-
-entity_call_result ent_water_call( world_instance *world, ent_call *call )
-{
-   if( call->function == 0 )
-   {
-      world_water_drown();
-      return k_entity_call_result_OK;
-   }
-
-   return k_entity_call_result_unhandled;
-}
diff --git a/world_water.h b/world_water.h
deleted file mode 100644 (file)
index 7ff9af5..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2021-2023 Mt.ZERO Software, Harry Godden - All Rights Reserved
- */
-
-#pragma once
-#include "world.h"
-
-struct world_water{
-   GLuint tex_water_surf;
-}
-extern world_water;
-void world_water_init(void);
-
-void water_set_surface( world_instance *world, f32 height );
-void render_water_texture( world_instance *world, vg_camera *cam );
-void render_water_surface( world_instance *world, vg_camera *cam );
-entity_call_result ent_water_call( world_instance *world, ent_call *call );
-bool world_water_player_safe( world_instance *world, f32 allowance );