By Casey Muratori
I should be back on a normal schedule next week, so Witness Wednesday Proper will resume next Wednesday. I’d like to do a Q&A about the Lister Panel, if people are up for it, just so that beginners can take a look at the actual code as it is used in the editor and ask questions about why things are the way they are. Sometimes the answer will be, “because I was in a hurry” :) But hopefully I can answer most questions in a way that gives additional insight on why it was a good idea to do things roughly the way they were done in the code.
Here is the complete ~2000 line source file that implements everything in the Lister Panel, along with its header file. Both have been lightly redacted to remove any filters that might give away aspects of The Witness’s gameplay for people who want to remain spoiler-free, so there should be absolutely no spoiler risk in reading the code.
Keep in mind that this code is a mixture of styles (Jon’s and mine), because even though I wrote all the code, I adopted Jon’s conventions in many places, just to make it easier for him to maintain. This is also editor-only code, which means it is not meant to be used in a released product, and thus the emphasis is entirely on keeping implementation costs low, and not on ancillary things like robustness that might be more of a priority in code meant for end users.
So, if you are in the mood, please spend some time looking over the files and send questions. I will collect them all and answer them next week!
lister_panel.h:
struct Entity_Panel;
struct Movement_Panel;
struct Materials_Panel;
struct Entity;
 
#include "panel.h"
 
/* TODO(casey): Currently, specials are handled pretty sloppily, in that every special is checked for every entity, both in terms of filtering and in terms of button display. What you really want to do is just do a single pass over the specials and build two lists, one that's the filter list and one that's the button list, and then you just use those. I haven't done that yet but it's clearly the right thing to do to avoid O(mn), it just isn't a problem right now because m is small. But once you have 200 filter types in there or something, it could get bad. */
 
enum Lister_Filter_Mode
{
LISTER_FILTER_MODE_OFF,
LISTER_FILTER_MODE_POSITIVE,
LISTER_FILTER_MODE_NEGATIVE,
 
LISTER_FILTER_MODE_COUNT,
};
 
enum Lister_Type_Mode
{
LISTER_TYPE_MODE_LIST,
LISTER_TYPE_MODE_ENTITY_PANEL,
LISTER_TYPE_MODE_SELECTION,
 
LISTER_TYPE_MODE_COUNT,
};
 
struct Visor_Settings
{
bool enabled;
float scale;
int unsigned color[8];
};
 
struct Lister_Panel_Settings
{
// TODO(casey): These are only here because the variable service stuff doesn't
// really work the way we would like.
char const *long_name_reader;
char const *short_name_reader;
char const *description_reader;
 
char long_name[32];
char short_name[8];
char description[256];
 
bool pull_groups_in;
bool show_hierarchically;
bool show_when_collapsed;
bool update_automatically;
 
bool show_groups_first;
bool show_selected_first;
 
Lister_Type_Mode type_mode;
 
float max_distance_from_cursor;
float max_distance_from_camera;
float max_distance_from_focus;
 
// TODO(casey): These really want to be fixed sizes, because it
// makes life so much simpler for copying things stress-free.
// But technically the include_type size is not known at
// compile time, because the compiler is stupid (it actually
// should know :( ) So there are simply startup asserts,
// and they will catch a problem if the sizes grow too large,
// and you can just bump these numbers at that point.
bool include_type[256];
int special[256];
bool special_button[256];
Visor_Settings visors[256];
};
 
struct Lister_Panel : public Panel {
Lister_Panel();
~Lister_Panel();
 
void draw_3d_im_stuff(const Viewpoint * vp);
void draw_2d_stuff();
void update();
bool can_collapse(void) {return(true);}
 
bool show_display_section;
bool show_included_types_section;
bool show_spatial_filters_section;
bool show_special_filters_section;
bool show_visors_section;
 
float visor_brightness;
 
float included_types_scroll;
float special_filters_scroll;
float visors_scroll;
float entities_scroll;
 
int current_settings_index;
Lister_Panel_Settings settings[24];
 
Auto_Array<int> viewing_ids;
Hash_Table expanded;
};
lister_panel.cpp:
#include "client.h"
 
#if ALLOW_DEVELOPER
 
#include "panel.h"
#include "font.h"
#include "control.h"
 
#include "mprintf.h"
 
#include "world_panel.h"
#include "device_context.h"
#include "game_resource_manager.h"
#include "rendering.h"
#include "movement_panel.h"
#include "particle_manager.h"
#include "entity_manager.h"
#include "lister_panel.h"
#include "entity.h"
#include "entity_panel.h"
#include "undo_panel.h"
 
#include "portable.h"
#include "ui_manager.h"
#include "editors.h"
#include "panel_utilities.h"
#include "bounding_volumes.h"
 
#include "entities/group.h"
[REDACTED]
#include "entities/grass.h"
#include "entities/door.h"
[REDACTED]
#include "entities/light_probe.h"
#include "grass_planting.h"
#include "raycast_result.h"
 
#include "token.h"
#include "mesh.h"
 
#include <float.h>
 
#define EDITOR_PANEL_NAME "lister"
 
Shader *get_shader(const char *name); // NOTE(casey): From rendering.cpp
 
// NOTE(casey): I don't use FLT_MAX in here because
// I was worried that FLT_MAX wouldn't persist properly, potentially,
// since it's written in text and all that, who knows... didn't
// wnat to risk it!
#define PSEUDO_FLT_MAX 100000.0f
 
// TODO(casey): No squaring function??
#define square(X) ((X)*(X))
 
// NOTE(casey): Filters don't all apply to everything, so
// NOT_APPLICABLE is their way of returning "I don't look at these
// types of entities"
#define NOT_APPLICABLE - 1
 
// TODO(casey): We need a centralized fast lookup for selection!
static Hash_Table *selected_hash;
 
//
// NOTE(casey): Special Filters
//
 
#define SPECIAL_FILTER(name) void name(int param, Entity *e, bool op, bool new_value, int *result)
typedef SPECIAL_FILTER(Special_Filter);
 
static SPECIAL_FILTER(filter_is_killed)
{
*result = (e->portable_flags & PORTABLE_KILLED);
}
 
static SPECIAL_FILTER(filter_hidden)
{
if(op)
{
change_visibility(e, !new_value);
}
 
*result = (e->runtime_only_flags & ER_HIDDEN);
}
 
static SPECIAL_FILTER(filter_inactive)
{
*result = (e->runtime_only_flags & ER_MY_LAYER_IS_INACTIVE);
}
 
static SPECIAL_FILTER(filter_groups_must_be_layers)
{
// TODO(casey): Allow this to be toggled interactively?
 
if(e->portable_type == &ptype_Group)
{
Entity_Group *group = (Entity_Group *)e;
 
if(op)
{
group->is_a_layer = new_value;
note_change(e);
}
 
*result = group->is_a_layer;
}
}
 
[REDACTED]
 
static SPECIAL_FILTER(filter_selected)
{
assert(selected_hash);
 
if(op)
{
if(new_value)
{
if(!selected_hash->find(e))
{
selected_hash->add(e, e);
add_to_selection(e->portable_id);
}
}
else
{
if(selected_hash->find(e))
{
selected_hash->remove(e);
// TODO(casey): WTF is this called toggle selection, when apparently it only clears the selection?
toggle_selection(e->portable_id);
}
}
}
 
*result = (selected_hash->find(e) != 0);
}
 
static SPECIAL_FILTER(filter_entity_flag)
{
if(op)
{
if(new_value)
{
e->entity_flags |= param;
}
else
{
e->entity_flags &= ~param;
}
note_change(e);
}
 
*result = (e->entity_flags & param);
}
 
static SPECIAL_FILTER(filter_group_current_visibility)
{
if(e->portable_type == &ptype_Group)
{
Entity_Group *group = (Entity_Group *)e;
 
if(op)
{
change_group_visibility(group, new_value);
}
 
*result = (group->current_group_visibility);
}
}
 
static SPECIAL_FILTER(filter_group_initial_visibility)
{
if(e->portable_type == &ptype_Group)
{
Entity_Group *group = (Entity_Group *)e;
 
if(op)
{
group->initial_group_visibility = new_value;
note_change(e);
}
 
*result = (group->initial_group_visibility);
}
}
 
static SPECIAL_FILTER(filter_door_slides_open)
{
if(e->portable_type == &ptype_Door)
{
Entity_Door *door = (Entity_Door *)e;
*result = (door->slides_open != 0);
}
}
 
static SPECIAL_FILTER(filter_mounted)
{
*result = (e->mount_parent_id != 0);
}
 
static SPECIAL_FILTER(filter_animated)
{
*result = (e->animator && e->mesh && e->mesh->name);
}
 
static SPECIAL_FILTER(filter_door_matches_closed)
{
if(e->portable_type == &ptype_Door)
{
Entity_Door *door = (Entity_Door *)e;
 
if(e->mount_parent_id == 0)
{
*result = ((door->pos_when_closed == door->position) && (door->ori_when_closed == door->orientation));
}
else
{
*result = ((door->mount_pos_when_closed == door->mount_position) && (door->mount_ori_when_closed == door->mount_orientation));
}
}
}
 
[REDACTED]
 
struct Special_Filter_Entry
{
char *save_name;
char *short_name;
char *positive_name;
char *negated_name;
Special_Filter *handler;
int param;
};
 
Special_Filter_Entry special_filters[] =
{
//
// NOTE(casey): Transient flags
//
{"filter_is_killed", "ki", "killed", "not killed", filter_is_killed},
 
{"filter_selected", "se", "selected", "not selected", filter_selected},
{"filter_hidden", "hi", "hidden", "not hidden", filter_hidden},
{"filter_inactive", "ia", "inactive", "active", filter_inactive},
 
//
// NOTE(casey): Persistent flags
//
{"filter_immobile", "im", "immobile", "mobile", filter_entity_flag, ENTITY_IMMOBILE},
{"filter_do_not_collide", "co", "doesn't collide", "collides", filter_entity_flag, ENTITY_DO_NOT_COLLIDE},
{"filter_do_not_cluster", "cl", "doesn't cluster", "clusters", filter_entity_flag, ENTITY_DO_NOT_CLUSTER},
{"filter_do_not_reflect", "rf", "doesn't reflect", "reflects", filter_entity_flag, ENTITY_DO_NOT_REFLECT},
 
{"filter_do_not_cast_shadows", "ca", "doesn't cast shadows", "casts shadows", filter_entity_flag, ENTITY_DO_NOT_CAST_SHADOWS},
{"filter_mount_in_editor_only", "me", "mounts only in editor", "doesn't mount only in editor", filter_entity_flag, ENTITY_MOUNT_IN_EDITOR_ONLY},
[REDACTED]
{"filter_has_slab_collision", "sc", "has slab collision", "doesn't have slab collision", filter_entity_flag, ENTITY_HAS_SLAB_COLLISION},
 
{"filter_has_ramp_collision", "rc", "has ramp collision", "doesn't have ramp collision", filter_entity_flag, ENTITY_HAS_RAMP_COLLISION},
[REDACTED]
 
{"filter_lods_to_nothing", "ln", "lods to nothing", "does not lod to nothing", filter_entity_flag, ENTITY_LODS_TO_NOTHING},
 
{"filter_rooted", "rt", "rooted", "not rooted", filter_entity_flag, ENTITY_ROOTED},
{"filter_forgiving_collision", "fc", "forgiving collision", "standard collision", filter_entity_flag, ENTITY_USES_FORGIVING_COLLISION_MODEL},
{"filter_is_invisible", "in", "invisible", "visible", filter_entity_flag, ENTITY_INVISIBLE},
 
{"filter_has_tree_collision", "tc", "has tree collision", "doesn't have tree collision", filter_entity_flag, ENTITY_HAS_TREE_COLLISION},
{"filter_has_skeleton", "sk", "has skeleton", "doesn't have skeleton", filter_entity_flag, ENTITY_HAS_SKELETON},
{"filter_has_convex_collision", "cc", "has convex collision", "doesn't have convex collision", filter_entity_flag, ENTITY_HAS_CONVEX_COLLISION},
#if 0
// NOTE(casey): Andy requested that this button be removed.
{"filter_has_occluder_geometry", "og", "has occluder geometry", "doesn't have occluder geometry", filter_entity_flag, ENTITY_HAS_OCCLUDER_GEOMETRY},
#endif
 
[REDACTED]
{"filter_lightmap_hidden", "lh", "hidden lightmap", "visible lightmap", filter_entity_flag, ENTITY_LIGHTMAP_HIDDEN},
{"filter_lightmap_static", "ls", "static lightmap", "non-static lightmap", filter_entity_flag, ENTITY_LIGHTMAP_STATIC},
{"filter_no_sun_light", "sn", "doesn't receive sunlight", "receives sunlight", filter_entity_flag, ENTITY_NO_SUN_LIGHT},
 
{"filter_lightmap_visible", "lv", "visible to lightmap", "visible to lightmap", filter_entity_flag, ENTITY_LIGHTMAP_VISIBLE},
{"filter_raycast_class1", "r1", "raycast class 1", "not raycast class 1", filter_entity_flag, ENTITY_RAYCAST_CLASS1},
{"filter_lightmap_high_priority", "hp", "high-priority lightmap", "normal priority lightmap", filter_entity_flag, ENTITY_LIGHTMAP_HIGH_PRIORITY},
{"filter_acts_as_ground", "ag", "acts as ground", "doesn't act as ground", filter_entity_flag, ENTITY_ACTS_AS_GROUND},
 
{"filter_locked", "lk", "locked", "unlocked", filter_entity_flag, ENTITY_LOCKED}, // {"filter_pose_does_not_matter", "pd", "pose doesn't matter", "pose matters", filter_entity_flag, ENTITY_POSE_DOES_NOT_MATTER},
{"filter_ignore_grass_avoidance", "ga", "ignores grass avoids", "doesn't ignore grass avoids", filter_entity_flag, ENTITY_IGNORES_GRASS_AVOIDANCE},
 
//
// NOTE(casey): Entity variables
//
{"filter_mounted", "mt", "mounted", "not mounted", filter_mounted},
{"filter_animated", "an", "animated", "not animated", filter_animated},
 
//
// NOTE(casey): Type-specific
//
[REDACTED]
 
{"filter_groups_must_be_layers", "gt", "group is layer", "group is not layer", filter_groups_must_be_layers},
{"filter_group_current_visibility", "gv", "group visible now", "group invisible now", filter_group_current_visibility},
 
#if 0
// NOTE(casey): Jon decided he did not want this button available to people,
// and that they should have to go through the entity panel to do this
// operation if they wanted to.
{"filter_group_initial_visibility", "gi", "group visible initially", "group invisible initially", filter_group_initial_visibility},
#endif
 
{"filter_door_slides_open", "ds", "door slides open", "door swings open", filter_door_slides_open},
{"filter_door_matches_closed", "dc", "door matches closed", "door differs from closed", filter_door_matches_closed},
 
[REDACTED]
};
 
//
// NOTE(casey): Visors
//
 
#define VISOR_HANDLER(name) void name(Visor_Settings &settings, Entity *e)
typedef VISOR_HANDLER(Visor_Handler);
 
inline void draw_circle(Vector3 const &p, float r, int unsigned color)
{
Vector3 e1 = globals.particle_manager->e1;
Vector3 e2 = globals.particle_manager->e2;
 
Vector3 p0 = p - r*e1 - r*e2;
Vector3 p1 = p + r*e1 - r*e2;
Vector3 p2 = p + r*e1 + r*e2;
Vector3 p3 = p - r*e1 + r*e2;
 
Vector2 uv0(0, 0);
Vector2 uv1(1, 0);
Vector2 uv2(1, 1);
Vector2 uv3(0, 1);
 
Device_Context::im_vertex(p0, uv0, color);
Device_Context::im_vertex(p1, uv1, color);
Device_Context::im_vertex(p2, uv2, color);
 
Device_Context::im_vertex(p0, uv0, color);
Device_Context::im_vertex(p2, uv2, color);
Device_Context::im_vertex(p3, uv3, color);
}
 
inline void draw_standard_marker(Vector3 const &p, int unsigned color)
{
// NOTE(casey): Grow the markers a bit based on camera distance so that when
// you zoom way out on the map, they're nice and big and you can see them clearly.
float radius = 0.01f*distance(globals.camera_position, p);
draw_circle(p, 1.5f*radius, color);
}
 
VISOR_HANDLER(visor_entity_position)
{
draw_standard_marker(e->position, argb_color(0, 1, 0, 1));
}
 
[REDACTED]
 
VISOR_HANDLER(visor_grass_planting_distribution)
{
if(e->portable_type == &ptype_Grass)
{
Entity_Grass *grass = (Entity_Grass *)e;
Grass_Source *source = grass->grass_source;
if(source)
{
int unsigned color = argb_color(0, 1, 0, 1);
{for(int inst_index = 0;
inst_index < source->instance_count;
++inst_index)
{
if(grass->fancy && inst_index == source->active_base_count)
{
color = argb_color(0, 0, 1, 1);
}
 
Grass_Instance &inst = source->instance_array[inst_index];
float radius = 0.1f*inst.scale;
 
Vector3 p = object_to_world_space(grass, inst.root_location);
 
draw_circle(p, 1.5f*radius, color);
}}
}
}
}
 
VISOR_HANDLER(visor_grass_planting_boundaries)
{
if(e->portable_type == &ptype_Grass)
{
Entity_Grass *grass = (Entity_Grass *)e;
Grass_Source *source = grass->grass_source;
if(source)
{
{for(int cell_y = 0; cell_y < (GRASS_CELL_DIMENSION + 1); ++cell_y)
{
{for(int cell_x = 0; cell_x < (GRASS_CELL_DIMENSION + 1); ++cell_x)
{
float &cell_d = source->distance_from_edge[cell_y][cell_x];
 
Vector2 cell_xy = get_xy_for_cell(*source, cell_x, cell_y);
Vector3 p = object_to_world_space(grass, Vector3(cell_xy.x, cell_xy.y, 0.0f));
 
float max_r = Max(grass->r1, grass->r2);
float gray = (cell_d / max_r);
int unsigned color = argb_color(gray, gray, gray, 1);
draw_circle(p, 0.1f, color);
}}
}}
}
}
}
 
static void thick_line(Vector3 const &from_p, Vector3 const &to_p, int unsigned color, float thickness = 0.2f)
{
Vector3 diff = to_p - from_p;
Vector3 perp = cross_product(diff, axis_up);
perp.normalize_or_zero();
 
Device_Context::im_vertex(from_p - thickness*perp, color);
Device_Context::im_vertex(to_p - thickness*perp, color);
Device_Context::im_vertex(to_p + thickness*perp, color);
 
Device_Context::im_vertex(from_p - thickness*perp, color);
Device_Context::im_vertex(to_p + thickness*perp, color);
Device_Context::im_vertex(from_p + thickness*perp, color);
}
 
static bool get_ground_point(Entity_Grass *grass, Vector3 start_p, Vector3 *result_p, float default_z)
{
bool result = false;
 
*result_p = start_p;
 
Ground_Hit ground_hit;
Raycast_Result raycast_result;
if(get_ground_hit_fancy(grass, start_p, 0, &ground_hit, &raycast_result))
{
result = true;
result_p->z = ground_hit.height;
}
else
{
result_p->z = default_z;
}
 
return(result);
}
 
VISOR_HANDLER(visor_grass_planting_area)
{
// TODO(casey): Really this is _half_ thickness
float thickness = 0.2f;
 
if(e->portable_type == &ptype_Grass)
{
Entity_Grass *grass = (Entity_Grass *)e;
 
int unsigned color = settings.color[1];
 
if(grass->is_rectangular)
{
Vector3 corner[4] =
{
Vector3(-grass->r1, -grass->r2, grass->position.z), Vector3(grass->r1, -grass->r2, grass->position.z), Vector3(grass->r1, grass->r2, grass->position.z), Vector3(-grass->r1, grass->r2, grass->position.z),
};
 
// TODO(casey): Need to do a prepass to record all the points,
// so those that miss the ground can be given a default Z
// based on ones that did hit the ground.
float default_z = grass->position.z;
 
Vector3 first_ground_p;
if(get_ground_point(grass, object_to_world_space(grass, corner[3]), &first_ground_p, default_z))
{
default_z = first_ground_p.z;
}
 
Vector3 last_p = first_ground_p;
int prev_corner_index = 3;
{for(int corner_index = 0;
corner_index <= 3;
prev_corner_index = corner_index++)
{
int step_count = 3;
{for(int step_index = 0;
step_index < step_count;
++step_index)
{
float t = (float)step_index / (float)step_count;
Vector3 c_p = (1.0f - t)*corner[prev_corner_index] + t*corner[corner_index];
Vector3 this_p;
if(get_ground_point(grass, object_to_world_space(grass, c_p), &this_p, default_z))
{
default_z = this_p.z;
}
 
thick_line(last_p, this_p, color, thickness);
last_p = this_p;
}}
}}
thick_line(last_p, first_ground_p, color, thickness);
}
else
{
// TODO(casey): Need to do a prepass to record all the points,
// so those that miss the ground can be given a default Z
// based on ones that did hit the ground.
float default_z = grass->position.z;
Vector3 first_ground_p;
if(get_ground_point(grass, object_to_world_space(grass, Vector3(grass->r1, 0, grass->position.z)), &first_ground_p, default_z))
{
default_z = first_ground_p.z;
}
 
Vector3 last_arm(1.0f, 0.0f, 0.0f);
Vector3 last_ground_p = first_ground_p;
int step_count = 16;
{for(int step_index = 1;
step_index < step_count;
++step_index)
{
float angle = TAU * ((float)step_index / (float)step_count);
 
Vector3 arm = Vector3(cos(angle), sin(angle), 0.0f);
Vector3 this_ground_p = Vector3(grass->r1*arm.x, grass->r2*arm.y, grass->position.z);
if(get_ground_point(grass, object_to_world_space(grass, this_ground_p), &this_ground_p, default_z))
{
default_z = this_ground_p.z;
}
 
Device_Context::im_vertex(last_ground_p - thickness*arm, color);
Device_Context::im_vertex(this_ground_p - thickness*arm, color);
Device_Context::im_vertex(this_ground_p + thickness*arm, color);
 
Device_Context::im_vertex(last_ground_p - thickness*arm, color);
Device_Context::im_vertex(this_ground_p + thickness*arm, color);
Device_Context::im_vertex(last_ground_p + thickness*arm, color);
 
last_ground_p = this_ground_p;
last_arm = arm;
}}
}
}
}
 
enum Planting_Curve_Type
{
PLANTING_CURVE_SCALE,
PLANTING_CURVE_DISTANCE,
};
static void draw_planting_curve(Entity_Grass *grass, Planting_Curve_Type type,
int unsigned min_color, int unsigned max_color,
float z_offset)
{
Ground_Hit ground_hit;
Raycast_Result raycast_result;
if(get_ground_hit_fancy(grass, grass->position, 0, &ground_hit, &raycast_result))
{
float mesh_radius = 1.0f;
if(grass->mesh)
{
mesh_radius = grass->mesh->sphere.radius;
}
 
Vector3 target_point;
if(grass->is_rectangular)
{
target_point = Vector3(grass->r1, grass->r2, 0);
}
else
{
target_point = Vector3(grass->r1, 0, 0);
}
 
Vector3 perp = cross_product(target_point, axis_up);
perp.normalize_or_zero();
 
int step_count = 10;
{for(int step_index = 0;
step_index < step_count;
++step_index)
{
float d0 = (float)step_index / (float)step_count;
float d1 = (float)(step_index + 1) / (float)step_count;
 
Vector3 p0 = d0*target_point;
Vector3 p1 = d1*target_point;
 
Grass_Planting_Values v0, v1;
get_grass_planting_values_at(*grass, p0, &v0);
get_grass_planting_values_at(*grass, p1, &v1);
 
Vector3 pw0 = object_to_world_space(grass, p0);
Vector3 pw1 = object_to_world_space(grass, p1);
 
pw0.z = ground_hit.height + z_offset;
pw1.z = ground_hit.height + z_offset;
 
float v0_min;
float v0_max;
float v1_min;
float v1_max;
if(type == PLANTING_CURVE_SCALE)
{
v0_min = v0.min_scale*mesh_radius;
v0_max = v0.max_scale*mesh_radius;
v1_min = v1.min_scale*mesh_radius;
v1_max = v1.max_scale*mesh_radius;
}
else
{
assert(type == PLANTING_CURVE_DISTANCE);
v0_min = 0.5f*v0.planting_distance*v0.min_scale;
v0_max = 0.5f*v0.planting_distance*v0.max_scale;
v1_min = 0.5f*v1.planting_distance*v1.min_scale;
v1_max = 0.5f*v1.planting_distance*v1.max_scale;
}
 
{for(int side = 0;
side <= 1;
++side)
{
Device_Context::im_vertex(pw0, min_color);
Device_Context::im_vertex(pw1, min_color);
Device_Context::im_vertex(pw1 + v1_min*perp, min_color);
 
Device_Context::im_vertex(pw0, min_color);
Device_Context::im_vertex(pw1 + v1_min*perp, min_color);
Device_Context::im_vertex(pw0 + v0_min*perp, min_color);
 
Device_Context::im_vertex(pw0 + v0_min*perp, max_color);
Device_Context::im_vertex(pw1 + v1_min*perp, max_color);
Device_Context::im_vertex(pw1 + v1_max*perp, max_color);
 
Device_Context::im_vertex(pw0 + v0_min*perp, max_color);
Device_Context::im_vertex(pw1 + v1_max*perp, max_color);
Device_Context::im_vertex(pw0 + v0_max*perp, max_color);
 
perp = -perp;
}}
}}
}
}
 
VISOR_HANDLER(visor_grass_planting_distance_curve)
{
if(e->portable_type == &ptype_Grass)
{
Entity_Grass *grass = (Entity_Grass *)e;
 
draw_planting_curve(grass, PLANTING_CURVE_DISTANCE, settings.color[1], settings.color[2], 1.0f);
}
}
 
VISOR_HANDLER(visor_grass_planting_scale_curve)
{
if(e->portable_type == &ptype_Grass)
{
Entity_Grass *grass = (Entity_Grass *)e;
 
draw_planting_curve(grass, PLANTING_CURVE_SCALE, settings.color[1], settings.color[2], 0.25f);
}
}
 
VISOR_HANDLER(visor_light_probe_radius)
{
if(e->portable_type == &ptype_Light_Probe)
{
Entity_Light_Probe * light_probe = (Entity_Light_Probe *)e;
 
if (light_probe->filtered) {
float radius = 16;
uint color = argb_color(1, 0, 0, 0.2);
extern void draw_sphere_cheap_im(const Vector3 & center, float radius, unsigned long color);
draw_sphere_cheap_im(light_probe->position, radius, color);
}
}
}
 
enum Visor_Flag
{
VISOR_USES_MARKER_PASS = 0x1,
VISOR_USES_SOLIDS_PASS = 0x2,
};
 
struct Visor_Entry
{
int unsigned flags;
char *save_name;
char *long_name;
Visor_Handler *handler;
};
 
Visor_Entry visors[] =
{
// NOTE(casey): Entity generic
{VISOR_USES_MARKER_PASS, "entity_position", "entity position", visor_entity_position},
 
// NOTE(casey): Grass planting
{VISOR_USES_MARKER_PASS, "grass_planting_distribution", "grass planting distribution", visor_grass_planting_distribution},
{VISOR_USES_MARKER_PASS, "grass_planting_boundaries", "grass planting boundaries", visor_grass_planting_boundaries},
{VISOR_USES_SOLIDS_PASS, "grass_planting_area", "grass planting area", visor_grass_planting_area},
{VISOR_USES_SOLIDS_PASS, "grass_planting_distance_curve", "grass distance curve", visor_grass_planting_distance_curve},
{VISOR_USES_SOLIDS_PASS, "grass_planting_scale_curve", "grass scale curve", visor_grass_planting_scale_curve},
 
// NOTE(IC): Light probes
{VISOR_USES_SOLIDS_PASS, "light_probe_radius", "light probe radius", visor_light_probe_radius},
};
 
//
//
//
 
static int compare_names(const void *b1, const void *b2)
{
// TODO(casey): WTF doesn't qsort pass a user parameter?
Lister_Panel_Settings &s = world_panel->lister_panel->settings[world_panel->lister_panel->current_settings_index];
 
Entity *e1 = *(Entity **)b1;
Entity *e2 = *(Entity **)b2;
 
if(s.show_selected_first)
{
assert(selected_hash);
bool e1_selected = selected_hash->find(e1);
bool e2_selected = selected_hash->find(e2);
if(e1_selected && !e2_selected)
{
return -1;
}
else if(!e1_selected && e2_selected)
{
return 1;
}
}
 
if(s.show_groups_first)
{
if((e1->portable_type == &ptype_Group) && (e2->portable_type != &ptype_Group))
{
return -1;
}
else if((e1->portable_type != &ptype_Group) && (e2->portable_type == &ptype_Group))
{
return 1;
}
}
 
if (e1->entity_name == NULL)
{
if (e2->entity_name == NULL)
{
return 0;
}
 
return 1;
}
else if (e2->entity_name == NULL)
{
return -1;
}
 
assert(e1->entity_name && e2->entity_name);
return stricmp(e1->entity_name, e2->entity_name);
}
 
static void draw_element(Panel_Layout &layout, Entity *e, Hash_Table *expanded_hash, Lister_Panel_Settings &current_settings)
{
assert(selected_hash);
 
layout.row(1);
 
if(expanded_hash)
{
bool original_expanded = (expanded_hash->find(e) != 0);
bool expanded = original_expanded;
layout.expansion_button(&expanded);
if(original_expanded != expanded)
{
if(expanded)
{
expanded_hash->add(e, e);
}
else
{
expanded_hash->remove(e);
}
}
}
 
char const *portable_type_name = e->portable_type->name;
 
bool highlight = false;
bool is_a_group = false;
if(e->portable_type == &ptype_Group)
{
Entity_Group *group = (Entity_Group *)e;
highlight = !(group->runtime_only_flags & ER_MY_LAYER_IS_INACTIVE);
is_a_group = true;
 
if(group->is_a_layer)
{
portable_type_name = "Layer";
}
}
else
{
highlight = (selected_hash->find(e) != 0);
}
 
Panel_Name entity_pname;
if(e->entity_name)
{
entity_pname = pnamef("%s (%s)", e->entity_name, portable_type_name);
}
else
{
entity_pname = pnamef("#%d (%s)", e->portable_id, portable_type_name);
}
entity_pname.description = "";
if (layout.push_button(entity_pname, highlight))
{
Input_Devices *input = gamelib_render->input_devices;
bool ctrl_held = input->get_keyboard_button_state(Keyboard::KEY_CTRL);
bool shift_held = input->get_keyboard_button_state(Keyboard::KEY_SHIFT);
bool alt_held = input->get_keyboard_button_state(Keyboard::KEY_ALT);
 
if(is_a_group)
{
Entity_Group *group = (Entity_Group *)e;
// TODO(casey): Can we get rid of the flag_group_as_dirty
// stuff now? The comment indicates it was in there for
// the group-zero case, but in fact it must never have
// properly handled that case, because that was the bug I
// had to fix that time with detached things not being set
// to visible, etc...
flag_group_as_dirty(group->entity_manager, group->portable_id); // Force elements rebuild; @speed. Was necessary since if we set someone's group to 0 (or whatever), the group doesn't know it yet, and we still treat him as if he were in the group!
group->update_elements();
 
if(ctrl_held)
{
// Put everything in the selection into the current group.
Entity_Manager *manager = open_entity_manager();
 
Auto_Array <Entity *> selected;
get_selection_entities(&selected);
Entity *other;
Array_Foreach(&selected, other)
{
flag_group_as_dirty(manager, other->group_id);
other->group_id = group->portable_id;
note_change("Changed Layer", other);
}
 
if(selected.items)
{
flag_group_as_dirty(manager, group->portable_id);
}
}
else if(alt_held)
{
begin_selection_change();
 
if (!shift_held) set_selection(0);
 
group->update_elements();
int id;
Array_Foreach(&group->elements, id)
{
add_to_selection(id);
}
 
end_selection_change();
}
else
{
change_group_visibility(group, !group->current_group_visibility);
}
}
else
{
if(shift_held)
{
toggle_selection(e->portable_id);
}
else if(ctrl_held)
{
Auto_Array <Entity *> temp;
temp.add(e);
Bounding_Box box;
Vector3 focus = get_center(&box, &temp);
set_camera_to_view_box(box);
}
else
{
set_selection(e->portable_id);
}
}
}
 
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
if(current_settings.special_button[special_index])
{
Special_Filter_Entry &entry = special_filters[special_index];
 
int handler_result = NOT_APPLICABLE;
entry.handler(entry.param, e, false, false, &handler_result);
 
if(handler_result == NOT_APPLICABLE)
{
layout.nub(pname(entry.short_name));
}
else
{
if(layout.push_button(pname(entry.short_name, entry.short_name, "", entry.positive_name), handler_result))
{
note_operation_name(handler_result ? entry.negated_name : entry.positive_name);
 
int ignored;
entry.handler(entry.param, e, true, !handler_result, &ignored);
}
}
}
}}
}
 
static void draw_element_recursive(Panel_Layout &layout, Entity *e, Hash_Table &viewing_present, Hash_Table &expanded, Lister_Panel_Settings &current_settings)
{
Auto_Array<Entity *> sub_entities;
 
if(e->portable_type == &ptype_Group)
{
Entity_Group *group = (Entity_Group *)e;
Foreach(int id, group->elements)
{
Entity *sub_e = group->entity_manager->find(id);
if(sub_e && viewing_present.find(sub_e))
{
sub_entities.add(sub_e);
}
}
}
 
draw_element(layout, e, sub_entities.items ? &expanded : 0, current_settings);
 
if(sub_entities.items && (expanded.find(e)))
{
layout.push_indent();
 
// TODO(casey): Sort groups first when set
qsort(sub_entities.data, sub_entities.items, sizeof(sub_entities.data[0]), compare_names);
 
Foreach(Entity *sub_e, sub_entities)
{
draw_element_recursive(layout, sub_e, viewing_present, expanded, current_settings);
}
 
layout.pop_indent();
}
}
 
static bool passes_filter(Lister_Panel_Settings &settings, Entity *e)
{
bool result = false;
 
if(e)
{
bool passes_type = settings.include_type[e->portable_type->serial_number_in_memory];
 
bool passes_special = true;
{for(int special_index = 0;
passes_special && (special_index < NV_ARRAY_SIZE(special_filters));
++special_index)
{
Special_Filter_Entry &special = special_filters[special_index];
assert(special_index < NV_ARRAY_SIZE(settings.special));
int filter_mode = settings.special[special_index];
switch(filter_mode)
{
case LISTER_FILTER_MODE_OFF:
{
// NOTE(casey): Ignored
} break;
 
case LISTER_FILTER_MODE_POSITIVE:
{
int handler_result = NOT_APPLICABLE;
special.handler(special.param, e, 0, false, &handler_result);
if(handler_result != NOT_APPLICABLE)
{
passes_special = (handler_result != 0);
}
} break;
 
case LISTER_FILTER_MODE_NEGATIVE:
{
int handler_result = NOT_APPLICABLE;
special.handler(special.param, e, 0, false, &handler_result);
if(handler_result != NOT_APPLICABLE)
{
passes_special = (handler_result == 0);
}
} break;
 
default:
{
assert(!"Unrecognized special filter mode");
} break;
}
}}
 
bool passes_distance = true;
if((settings.max_distance_from_cursor != PSEUDO_FLT_MAX) || (settings.max_distance_from_camera != PSEUDO_FLT_MAX) || (settings.max_distance_from_focus != PSEUDO_FLT_MAX))
{
Movement_Panel *mp = ::world_panel->movement_panel;
 
Vector3 sphere_center = get_sphere_center_in_worldspace(e);
float r = get_sphere_radius_in_worldspace(e);
 
float cursor_distance2 = distance_squared(sphere_center, mp->cursor_position);
float camera_distance2 = distance_squared(sphere_center, globals.camera_position);
float focus_distance2 = distance_squared(sphere_center, get_camera_focus());
 
passes_distance = ((cursor_distance2 < square(settings.max_distance_from_cursor + r)) && (camera_distance2 < square(settings.max_distance_from_camera + r)) && (focus_distance2 < square(settings.max_distance_from_focus + r)));
}
 
result = (passes_type && passes_special && passes_distance);
}
 
return(result);
}
 
static void set_all_types_to(Lister_Panel_Settings &settings, bool value)
{
{for(int type_index = 0;
type_index < shared_globals.portable_type_manager->types.items;
++type_index)
{
settings.include_type[type_index] = value;
}}
}
 
static void set_special(Lister_Panel_Settings &settings, Special_Filter *handler, int mode, bool button_on)
{
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
if(special_filters[special_index].handler == handler)
{
settings.special[special_index] = mode;
settings.special_button[special_index] = button_on;
}
}}
}
 
static void unpack_selection(Hash_Table &selected)
{
Entity_Manager *manager = open_entity_manager();
Foreach(int id, selected_portable_ids)
{
Entity *e = manager->find(id);
if(e)
{
selected.add(e, e);
}
}
}
 
Lister_Panel::Lister_Panel() : Panel(PANEL_TAG_WORLD), expanded(13729)
{
set_default_location(0, 1, 150, -6, 0, 1, 150, -6);
 
title = "Lister";
collapsed = true;
 
my_width = 300;
 
EDITOR_VAR(show_display_section, false);
EDITOR_VAR(show_included_types_section, false);
EDITOR_VAR(show_spatial_filters_section, false);
EDITOR_VAR(show_special_filters_section, false);
EDITOR_VAR(show_visors_section, false);
 
EDITOR_VAR(visor_brightness, Panel_Layout::get_default_overlay_brightness());
 
EDITOR_VAR(included_types_scroll, 0.0f);
EDITOR_VAR(special_filters_scroll, 0.0f);
EDITOR_VAR(visors_scroll, 0.0f);
EDITOR_VAR(entities_scroll, 0.0f);
 
EDITOR_VAR(current_settings_index, 0);
 
int type_count = shared_globals.portable_type_manager->types.items;
 
//
// NOTE(casey): Clear all settings to defaults
//
{for(int settings_index = 0;
settings_index < NV_ARRAY_SIZE(settings);
++settings_index)
{
Lister_Panel_Settings &this_settings = settings[settings_index];
 
sprintf(this_settings.long_name, "unused"/* , settings_index */);
sprintf(this_settings.short_name, "unu"/* , settings_index */);
sprintf(this_settings.description, "(no description)"/* , settings_index */);
 
this_settings.pull_groups_in = false;
this_settings.show_groups_first = false;
this_settings.show_selected_first = false;
this_settings.show_hierarchically = true;
this_settings.show_when_collapsed = false;
this_settings.type_mode = LISTER_TYPE_MODE_LIST;
this_settings.update_automatically = true;
 
this_settings.max_distance_from_cursor = PSEUDO_FLT_MAX;
this_settings.max_distance_from_camera = PSEUDO_FLT_MAX;
this_settings.max_distance_from_focus = PSEUDO_FLT_MAX;
 
assert(type_count <= NV_ARRAY_SIZE(this_settings.include_type));
{for(int type_index = 0;
type_index < type_count;
++type_index)
{
this_settings.include_type[type_index] = false;
}}
 
assert(NV_ARRAY_SIZE(special_filters) <= NV_ARRAY_SIZE(this_settings.special));
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
this_settings.special[special_index] = LISTER_FILTER_MODE_OFF;
this_settings.special_button[special_index] = false;
}}
 
assert(NV_ARRAY_SIZE(visors) <= NV_ARRAY_SIZE(this_settings.visors));
{for(int visor_index = 0;
visor_index < NV_ARRAY_SIZE(visors);
++visor_index)
{
Visor_Settings &v = this_settings.visors[visor_index];
v.enabled = false;
v.scale = 1.0f;
assert(NV_ARRAY_SIZE(v.color) == 8);
v.color[0] = argb_color(0, 0, 0, 1);
v.color[1] = argb_color(1, 0, 0, 1);
v.color[2] = argb_color(0, 1, 0, 1);
v.color[3] = argb_color(0, 0, 1, 1);
v.color[4] = argb_color(1, 1, 0, 1);
v.color[5] = argb_color(0, 1, 1, 1);
v.color[6] = argb_color(1, 0, 1, 1);
v.color[7] = argb_color(1, 1, 1, 1);
}}
}}
 
//
// NOTE(casey): Set a few slots to example filters that people can use right
// of the bat before they start customizing
//
{
Lister_Panel_Settings &s = settings[0];
sprintf(s.long_name, "layers");
sprintf(s.short_name, "lay");
sprintf(s.description, "emulates the old layer lister, but with proper scrolling");
s.pull_groups_in = false;
s.show_groups_first = false;
s.show_hierarchically = false;
s.show_when_collapsed = true;
s.include_type[ptype_Group.serial_number_in_memory] = true;
set_special(s, filter_groups_must_be_layers, LISTER_FILTER_MODE_POSITIVE, false);
set_special(s, filter_selected, LISTER_FILTER_MODE_OFF, true);
}
 
{
Lister_Panel_Settings &s = settings[1];
sprintf(s.long_name, "groups");
sprintf(s.short_name, "grp");
sprintf(s.description, "new-school hierarchical layer/group lister");
s.pull_groups_in = false;
s.show_groups_first = false;
s.show_hierarchically = true;
s.show_when_collapsed = true;
s.include_type[ptype_Group.serial_number_in_memory] = true;
set_special(s, filter_selected, LISTER_FILTER_MODE_OFF, true);
}
 
{
Lister_Panel_Settings &s = settings[2];
sprintf(s.long_name, "flat selected");
sprintf(s.short_name, "fsl");
sprintf(s.description, "shows selected entities as a flat list");
s.pull_groups_in = false;
s.show_groups_first = false;
s.show_hierarchically = false;
s.show_when_collapsed = true;
set_all_types_to(s, true);
set_special(s, filter_selected, LISTER_FILTER_MODE_POSITIVE, false);
set_special(s, filter_hidden, LISTER_FILTER_MODE_OFF, true);
}
 
{
Lister_Panel_Settings &s = settings[3];
sprintf(s.long_name, "selected");
sprintf(s.short_name, "sel");
sprintf(s.description, "shows selected entities within their hierarchies");
s.pull_groups_in = true;
s.show_groups_first = false;
s.show_hierarchically = true;
s.show_when_collapsed = true;
set_all_types_to(s, true);
set_special(s, filter_selected, LISTER_FILTER_MODE_POSITIVE, false);
set_special(s, filter_hidden, LISTER_FILTER_MODE_OFF, true);
}
 
[REDACTED]
 
{
Lister_Panel_Settings &s = settings[5];
sprintf(s.long_name, "by type");
sprintf(s.short_name, "typ");
sprintf(s.description, "flat list of entities of the current entity panel type");
s.pull_groups_in = false;
s.show_groups_first = false;
s.show_hierarchically = false;
s.show_when_collapsed = true;
s.type_mode = LISTER_TYPE_MODE_ENTITY_PANEL;
set_all_types_to(s, true);
set_special(s, filter_selected, LISTER_FILTER_MODE_OFF, true);
set_special(s, filter_hidden, LISTER_FILTER_MODE_OFF, true);
}
 
{
Lister_Panel_Settings &s = settings[6];
sprintf(s.long_name, "camera group");
sprintf(s.short_name, "cmg");
sprintf(s.description, "shows all groups near to the camera");
s.pull_groups_in = true;
s.show_groups_first = false;
s.show_hierarchically = true;
s.show_when_collapsed = true;
s.include_type[ptype_Group.serial_number_in_memory] = true;
s.max_distance_from_camera = 50.0f;
set_special(s, filter_selected, LISTER_FILTER_MODE_OFF, true);
set_special(s, filter_hidden, LISTER_FILTER_MODE_OFF, true);
}
 
{
Lister_Panel_Settings &s = settings[7];
sprintf(s.long_name, "camera all");
sprintf(s.short_name, "cma");
sprintf(s.description, "shows all entities near to the camera");
s.pull_groups_in = true;
s.show_groups_first = true;
s.show_hierarchically = true;
set_all_types_to(s, true);
s.max_distance_from_camera = 50.0f;
set_special(s, filter_selected, LISTER_FILTER_MODE_OFF, true);
set_special(s, filter_hidden, LISTER_FILTER_MODE_OFF, true);
}
 
//
// NOTE(casey): Now, overwrite everything with the user's preferences if they exist
//
{for(int settings_index = 0;
settings_index < NV_ARRAY_SIZE(settings);
++settings_index)
{
Lister_Panel_Settings &this_settings = settings[settings_index];
 
this_settings.long_name_reader = this_settings.long_name;
this_settings.short_name_reader = this_settings.short_name;
this_settings.description_reader = this_settings.description;
 
EDITOR_VAR_INDEXED(&this_settings.long_name_reader, "long_name", settings_index);
EDITOR_VAR_INDEXED(&this_settings.short_name_reader, "short_name", settings_index);
EDITOR_VAR_INDEXED(&this_settings.description_reader, "description", settings_index);
 
safe_copy_and_redirect(this_settings.long_name, this_settings.long_name_reader, sizeof(this_settings.long_name));
safe_copy_and_redirect(this_settings.short_name, this_settings.short_name_reader, sizeof(this_settings.short_name));
safe_copy_and_redirect(this_settings.description, this_settings.description_reader, sizeof(this_settings.description));
 
EDITOR_VAR_INDEXED(&this_settings.pull_groups_in, "pull_groups_in", settings_index);
EDITOR_VAR_INDEXED(&this_settings.show_groups_first, "show_groups_first", settings_index);
EDITOR_VAR_INDEXED(&this_settings.show_selected_first, "show_selected_first", settings_index);
EDITOR_VAR_INDEXED(&this_settings.show_hierarchically, "show_hierarchically", settings_index);
EDITOR_VAR_INDEXED(&this_settings.show_when_collapsed, "show_when_collapsed", settings_index);
EDITOR_VAR_INDEXED((int *)&this_settings.type_mode, "type_mode", settings_index);
EDITOR_VAR_INDEXED(&this_settings.update_automatically, "update_automatically", settings_index);
 
EDITOR_VAR_INDEXED(&this_settings.max_distance_from_cursor, "max_distance_from_cursor", settings_index);
EDITOR_VAR_INDEXED(&this_settings.max_distance_from_camera, "max_distance_from_camera", settings_index);
EDITOR_VAR_INDEXED(&this_settings.max_distance_from_focus, "max_distance_from_focus", settings_index);
 
{for(int type_index = 0;
type_index < type_count;
++type_index)
{
EDITOR_VAR_SUBNAME(&this_settings.include_type[type_index], "include_type", settings_index, (char *)shared_globals.portable_type_manager->types[type_index]->name);
}}
 
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
EDITOR_VAR_SUBNAME(&this_settings.special[special_index], "special", settings_index, special_filters[special_index].save_name);
EDITOR_VAR_SUBNAME(&this_settings.special_button[special_index], "special_button", settings_index, special_filters[special_index].save_name);
}}
 
{for(int visor_index = 0;
visor_index < NV_ARRAY_SIZE(visors);
++visor_index)
{
Visor_Settings &v = this_settings.visors[visor_index];
 
EDITOR_VAR_SUBNAME(&v.enabled, "visor_enabled", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.scale, "visor_scale", settings_index, visors[visor_index].save_name);
#if 0
// TODO(casey): Persist colors
EDITOR_VAR_SUBNAME(&v.color[0], "visor_color_0", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[1], "visor_color_1", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[2], "visor_color_2", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[3], "visor_color_3", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[4], "visor_color_4", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[5], "visor_color_5", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[6], "visor_color_6", settings_index, visors[visor_index].save_name);
EDITOR_VAR_SUBNAME(&v.color[7], "visor_color_7", settings_index, visors[visor_index].save_name);
#endif
}}
}}
 
EDITOR_STANDARD_PANEL_VARS;
}
 
Lister_Panel::~Lister_Panel()
{
}
 
void Lister_Panel::update()
{
Panel::update();
buttons_active = ::world_panel->buttons_active; // @Refactor: backwards data flow.
 
// NOTE(casey): Unpack selection to avoid n^2 lookups during tests
// TODO(casey): Why can't I just _clear_ the hash table?
if(selected_hash)
{
delete selected_hash;
}
selected_hash = new Hash_Table(13729);
assert(selected_hash);
unpack_selection(*selected_hash);
 
Lister_Panel_Settings &current_settings = settings[current_settings_index];
if(current_settings.type_mode == LISTER_TYPE_MODE_ENTITY_PANEL)
{
Entity_Panel *ep = ::world_panel->entity_panel;
set_all_types_to(current_settings, false);
assert(ep->current_type >= 0);
assert(ep->current_type < NV_ARRAY_SIZE(current_settings.include_type));
current_settings.include_type[ep->current_type] = true;
}
 
if(current_settings.type_mode == LISTER_TYPE_MODE_SELECTION)
{
set_all_types_to(current_settings, false);
}
 
if(current_settings.update_automatically)
{
Entity_Manager *manager = open_entity_manager();
viewing_ids.reset();
Foreach(Entity *e, manager->all_entities)
{
if(current_settings.type_mode == LISTER_TYPE_MODE_SELECTION)
{
if(selected_hash->find(e))
{
current_settings.include_type[e->portable_type->serial_number_in_memory] = true;
}
}
 
if(passes_filter(current_settings, e))
{
viewing_ids.add(e->portable_id);
}
}
}
 
}
 
void Lister_Panel::draw_3d_im_stuff(const Viewpoint * vp)
{
Lister_Panel_Settings &current_settings = settings[current_settings_index];
 
// NOTE(casey): Build the lists of active visors by type of visor pass
Auto_Array<int> marker_visors;
Auto_Array<int> solids_visors;
{for(int visor_index = 0;
visor_index < NV_ARRAY_SIZE(visors);
++visor_index)
{
if(current_settings.visors[visor_index].enabled)
{
if(visors[visor_index].flags & VISOR_USES_MARKER_PASS)
{
marker_visors.add(visor_index);
}
 
if(visors[visor_index].flags & VISOR_USES_SOLIDS_PASS)
{
solids_visors.add(visor_index);
}
}
}}
 
// NOTE(casey): Draw all visors that do textured marker triangles
if(marker_visors.items)
{
Resource_Ptr<Texture_Map> map(acquire_texture(0, "ui_circle_marker"), 0);
setup_standard_soft_z(vp, true, visor_brightness);
Device_Context::set_tex_parameter("texture_map", &*map);
 
Device_Context::im_begin();
 
Entity_Manager *manager = open_entity_manager();
Foreach(int id, viewing_ids)
{
Entity *e = manager->find(id);
if(e)
{
Foreach(int visor_index, marker_visors)
{
visors[visor_index].handler(current_settings.visors[visor_index], e);
}
}
}
 
Device_Context::im_flush();
}
 
// NOTE(casey): Draw all visors that do untextured triangles
if(solids_visors.items)
{
setup_standard_soft_z(vp, /* textured= */false);
 
Device_Context::im_begin();
 
Entity_Manager *manager = open_entity_manager();
Foreach(int id, viewing_ids)
{
Entity *e = manager->find(id);
if(e)
{
Foreach(int visor_index, solids_visors)
{
visors[visor_index].handler(current_settings.visors[visor_index], e);
}
}
}
 
Device_Context::im_flush();
}
}
 
void Lister_Panel::draw_2d_stuff()
{
Input_Devices *input = gamelib_render->input_devices;
bool ctrl_held = input->get_keyboard_button_state(Keyboard::KEY_CTRL);
 
if(collapsed)
{
my_width = 300;
}
else
{
my_width = 600;
}
 
rendering_2d_right_handed();
 
Panel_Layout layout(this, x, y, my_width, true);
 
Lister_Panel_Settings &current_settings = settings[current_settings_index];
layout.window_title(pname(current_settings.long_name));
 
int disp_index = 0;
{for(int settings_index = 0;
settings_index < NV_ARRAY_SIZE(settings);
++settings_index)
{
Lister_Panel_Settings &s = settings[settings_index];
if(!collapsed || s.show_when_collapsed)
{
if((disp_index % 6) == 0)
{
layout.row(6);
}
 
if(layout.push_button(pname(s.long_name, s.short_name, "", s.description), (current_settings_index == settings_index)))
{
if(ctrl_held)
{
s = current_settings;
}
else
{
current_settings_index = settings_index;
}
}
 
++disp_index;
}
}}
 
if(!collapsed)
{
if(layout.section_title(pname("display"), &show_display_section))
{
layout.row(1, pname("long name"));
layout.text_edit(sizeof(current_settings.long_name), current_settings.long_name);
layout.row(1, pname("short name"));
layout.text_edit(sizeof(current_settings.short_name), current_settings.short_name);
layout.row(1, pname("description"));
layout.text_edit(sizeof(current_settings.description), current_settings.description);
 
layout.row(4, pname("list options"));
layout.bool_button(pname("pull groups", "pgp", "", "any selected entity automatically includes all its parent groups in the listing"), &current_settings.pull_groups_in);
layout.bool_button(pname("groups first", "gpf", "", "list groups first, then entities, instead of everything being in alphabetical order"), &current_settings.show_groups_first);
layout.bool_button(pname("sel first", "sfr", "", "list selected items first, then everything else"), &current_settings.show_selected_first);
layout.bool_button(pname("hierarchical", "har", "", "list in hierarchical format, instead of flat"), &current_settings.show_hierarchically);
 
layout.row(2, pname("misc options"));
layout.bool_button(pname("show when collapsed", "swc", "", "show a button for this filter in the collapsed panel"), &current_settings.show_when_collapsed);
layout.bool_button(pname("auto update", "aup", "", "automatically update the contents of the lister as edits are made to the world"), &current_settings.update_automatically);
}
 
if(layout.section_title(pname("included types"), &show_included_types_section))
{
layout.row(3, pname("type from"));
layout.mutex_button(pname("list", "lst", "", "set the type inclusion manually from a list of all possible types"), (int *)&current_settings.type_mode, LISTER_TYPE_MODE_LIST);
layout.mutex_button(pname("selection", "tfs", "", "set the type inclusion automatically to the set of types in the current selection"), (int *)&current_settings.type_mode, LISTER_TYPE_MODE_SELECTION);
layout.mutex_button(pname("entity inst", "tfi", "", "set the type inclusion automatically from the current instatiation type of the entity panel"), (int *)&current_settings.type_mode, LISTER_TYPE_MODE_ENTITY_PANEL);
 
if(current_settings.type_mode == LISTER_TYPE_MODE_LIST)
{
layout.row(2, pname("preset", "pre", "", "quickly enable or disable a preset list of types"));
if(layout.push_button(pname("none", "nne", "", "include no types")))
{
set_all_types_to(current_settings, false);
}
if(layout.push_button(pname("all", "all", "", "include all types")))
{
set_all_types_to(current_settings, true);
}
 
layout.begin_scrollable_section(&included_types_scroll);
{for(int type_index = 0;
type_index < shared_globals.portable_type_manager->types.items;
++type_index)
{
if((type_index % 2) == 0)
{
layout.row(2);
}
 
layout.bool_button(pname((char *)shared_globals.portable_type_manager->types[type_index]->name), &current_settings.include_type[type_index]);
}}
layout.end_scrollable_section();
}
}
 
if(layout.section_title(pname("spatial filters"), &show_spatial_filters_section))
{
layout.float_mutex_control(pname("cursor dist", "crd", "", "sets the maximum distance from the cursor an entity can be before it is excluded from the listing"), &current_settings.max_distance_from_cursor, "10", 10.0f, "25", 25.0f, "50", 50.0f, "inf", PSEUDO_FLT_MAX);
layout.float_mutex_control(pname("camera dist", "cmd", "", "sets the maximum distance from the camera an entity can be before it is excluded from the listing"), &current_settings.max_distance_from_camera, "10", 10.0f, "25", 25.0f, "50", 50.0f, "inf", PSEUDO_FLT_MAX);
layout.float_mutex_control(pname("focus dist", "foc", "", "sets the maximum distance from the camera focus an entity can be before it is excluded from the listing"), &current_settings.max_distance_from_focus, "10", 10.0f, "25", 25.0f, "50", 50.0f, "inf", PSEUDO_FLT_MAX);
}
 
if(layout.section_title(pname("parameter filters and operations"), &show_special_filters_section))
{
layout.row(2, pname("preset", "pre", "", "quickly enable or disable a preset list of filters"));
if(layout.push_button(pname("no filters", "nfl", "", "apply no parameter filters")))
{
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
current_settings.special[special_index] = LISTER_FILTER_MODE_OFF;
}}
}
if(layout.push_button(pname("no operations", "nop", "", "show no operation columns")))
{
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
current_settings.special_button[special_index] = false;
}}
}
 
layout.begin_scrollable_section(&special_filters_scroll);
layout.begin_row_template();
layout.row_template_variable_column();
layout.row_template_fixed_column(layout.default_nub_width);
layout.row_template_variable_column();
layout.row_template_fixed_column(layout.default_nub_width);
layout.end_and_set_row_template();
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
if((special_index % 2) == 0)
{
layout.row();
}
 
Special_Filter_Entry &entry = special_filters[special_index];
int &value = current_settings.special[special_index];
if(layout.push_button((value == LISTER_FILTER_MODE_NEGATIVE) ? pname(entry.negated_name) : pname(entry.positive_name), value) != LISTER_FILTER_MODE_OFF)
{
value = ((value + 1) % LISTER_FILTER_MODE_COUNT);
}
 
layout.bool_button(pname(entry.short_name), &current_settings.special_button[special_index]);
}}
layout.clear_row_template();
layout.end_scrollable_section();
}
 
if(layout.section_title(pname("visors"), &show_visors_section))
{
layout.overlay_brightness_control(&visor_brightness);
 
layout.row(1, pname("preset", "pre", "", "quickly enable or disable a preset list of visors"));
if(layout.push_button(pname("none", "nne", "", "show no visors")))
{
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
current_settings.visors[special_index].enabled = false;
}}
}
 
layout.begin_scrollable_section(&visors_scroll);
{for(int visor_index = 0;
visor_index < NV_ARRAY_SIZE(visors);
++visor_index)
{
if((visor_index % 2) == 0)
{
layout.row(2);
}
 
Visor_Entry &entry = visors[visor_index];
layout.bool_button(pname(entry.long_name), &current_settings.visors[visor_index].enabled);
}}
layout.end_scrollable_section();
}
}
 
//
//
//
 
Entity_Manager *manager = open_entity_manager();
 
Hash_Table viewing_present(13729);
Auto_Array<Entity *> top_level;
 
// NOTE(casey): Unpack listed entities to avoid n^2 lookups during hierarchy walk,
// and determine who is at the top level
int special_states[NV_ARRAY_SIZE(current_settings.special)] = {0};
Foreach(int id, viewing_ids)
{
Entity *e = manager->find(id);
 
while(e && !viewing_present.find(e))
{
viewing_present.add(e, e);
if(!current_settings.show_hierarchically || (e->group_id == 0))
{
top_level.add(e);
}
 
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
Special_Filter_Entry &entry = special_filters[special_index];
 
int handler_result = NOT_APPLICABLE;
entry.handler(entry.param, e, 0, false, &handler_result);
 
if(handler_result != NOT_APPLICABLE)
{
special_states[special_index] |= (handler_result ? 2 : 1);
}
}}
 
if(current_settings.pull_groups_in && e->group_id)
{
e = manager->find(e->group_id);
}
else
{
e = 0;
}
}
}
 
layout.section_title(pname("listing"));
 
#if 0
layout.row(4, pname("expand", "exp", "", "quickly expand or collapse multiple entries"));
layout.push_button(pname("none", "nne", "", "collapse everything"));
layout.push_button(pname("layers", "lay", "", "expand all layer entities"));
layout.push_button(pname("groups", "grp", "", "expand all group entities"));
layout.push_button(pname("all", "all", "", "expand everything"));
#endif
 
layout.begin_row_template();
layout.row_template_variable_column();
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
if(current_settings.special_button[special_index])
{
Special_Filter_Entry &entry = special_filters[special_index];
layout.row_template_fixed_column(layout.default_nub_width);
}
}}
layout.row_template_fixed_column(layout.default_scroll_bar_width);
layout.end_and_set_row_template();
 
layout.row(0, pname("entity name", "entity name", "", ""));
{for(int special_index = 0;
special_index < NV_ARRAY_SIZE(special_filters);
++special_index)
{
if(current_settings.special_button[special_index])
{
Special_Filter_Entry &entry = special_filters[special_index];
 
bool current_state = (special_states[special_index] == 2);
if(layout.push_button(pname(entry.short_name, entry.short_name, "", entry.positive_name), current_state))
{
note_operation_name(current_state ? entry.negated_name : entry.positive_name);
Foreach(int id, viewing_ids)
{
Entity *e = manager->find(id);
if(e)
{
int ignored;
entry.handler(entry.param, e, true, !current_state, &ignored);
}
}
}
}
}}
layout.nub(pname(""));
 
layout.begin_scrollable_section(&entities_scroll, collapsed ? 600 : 300);
 
// NOTE(casey): If walking hierarchically, make sure we move everything
// to the top level that would not be reached by the in-order walk
if(current_settings.show_hierarchically)
{
Foreach(int id, viewing_ids)
{
Entity *e = manager->find(id);
if(e && (e->group_id != 0))
{
Entity *group = manager->find(e->group_id);
if(!group || !viewing_present.find(group))
{
top_level.add(e);
}
}
}
}
 
qsort(top_level.data, top_level.items, sizeof(top_level.data[0]), compare_names);
 
Foreach(Entity *e, top_level)
{
if(current_settings.show_hierarchically)
{
draw_element_recursive(layout, e, viewing_present, expanded, current_settings);
}
else
{
draw_element(layout, e, 0, current_settings);
}
}
 
layout.end_scrollable_section();
layout.clear_row_template();
 
layout.complete();
}
 
#endif // ALLOW_DEVELOPER
I am currently working on 1935, Meow the Infinite, and Handmade Hero.