dxx-rebirth/similar/main/automap.cpp
Kp 197a9cbd98 Move joystick interpretation to happen after the event loop
If two or more events are delivered in the same loop, the previous
implementation would count joystick motion multiple times.  Fix this by
moving the joystick interpretation to occur once, after all the events
have been processed.
2022-02-27 14:23:53 +00:00

1788 lines
53 KiB
C++

/*
* Portions of this file are copyright Rebirth contributors and licensed as
* described in COPYING.txt.
* Portions of this file are copyright Parallax Software and licensed
* according to the Parallax license below.
* See COPYING.txt for license details.
THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED.
*/
/*
*
* Routines for displaying the auto-map.
*
*/
#include "dxxsconf.h"
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if DXX_USE_OGL
#include "ogl_init.h"
#endif
#include "dxxerror.h"
#include "3d.h"
#include "inferno.h"
#include "u_mem.h"
#include "render.h"
#include "object.h"
#include "game.h"
#include "polyobj.h"
#include "sounds.h"
#include "player.h"
#include "bm.h"
#include "key.h"
#include "newmenu.h"
#include "textures.h"
#include "hudmsg.h"
#include "timer.h"
#include "segpoint.h"
#include "joy.h"
#include "pcx.h"
#include "palette.h"
#include "wall.h"
#include "physfsx.h"
#include "gameseq.h"
#include "gamefont.h"
#include "gameseg.h"
#include "common/3d/globvars.h"
#include "multi.h"
#include "kconfig.h"
#include "endlevel.h"
#include "text.h"
#include "gauges.h"
#include "powerup.h"
#include "switch.h"
#include "automap.h"
#include "timer.h"
#include "config.h"
#include "playsave.h"
#include "window.h"
#include "playsave.h"
#include "args.h"
#include "physics.h"
#include "compiler-range_for.h"
#include "d_enumerate.h"
#include "d_levelstate.h"
#include "d_range.h"
#include "d_zip.h"
#include <memory>
#define LEAVE_TIME 0x4000
#define EF_USED 1 // This edge is used
#define EF_DEFINING 2 // A structure defining edge that should always draw.
#define EF_FRONTIER 4 // An edge between the known and the unknown.
#define EF_SECRET 8 // An edge that is part of a secret wall.
#define EF_GRATE 16 // A grate... draw it all the time.
#define EF_NO_FADE 32 // An edge that doesn't fade with distance
#define EF_TOO_FAR 64 // An edge that is too far away
namespace dcx {
namespace {
struct Edge_info
{
std::array<vertnum_t, 2> verts; // 8 bytes
std::array<sidenum_t, 4> sides; // 4 bytes
std::array<segnum_t, 4> segnum; // 16 bytes // This might not need to be stored... If you can access the normals of a side.
ubyte flags; // 1 bytes // See the EF_??? defines above.
color_t color; // 1 bytes
ubyte num_faces; // 1 bytes // 31 bytes...
};
struct automap : ::dcx::window
{
using ::dcx::window::window;
fix64 entry_time;
fix64 t1, t2;
static_assert(PF_WIGGLE < UINT8_MAX, "storing PF_WIGGLE into old_wiggle would truncate the value");
uint8_t old_wiggle;
uint8_t leave_mode = 0;
uint8_t pause_game;
vms_angvec tangles;
int max_segments_away = 0;
int segment_limit = 1;
// Edge list variables
int num_edges = 0;
unsigned max_edges; //set each frame
unsigned end_valid_edges = 0;
std::unique_ptr<Edge_info[]> edges;
std::unique_ptr<Edge_info *[]> drawingListBright;
// Screen canvas variables
grs_subcanvas automap_view;
grs_main_bitmap automap_background;
// Rendering variables
fix zoom = 0x9000;
vms_vector view_target;
vms_vector view_position;
fix farthest_dist = (F1_0 * 20 * 50); // 50 segments away
vms_matrix viewMatrix;
fix viewDist = 0;
segment_depth_array_t depth_array;
color_t wall_normal_color;
color_t wall_door_color;
color_t wall_door_blue;
color_t wall_door_gold;
color_t wall_door_red;
color_t hostage_color;
color_t green_31;
color_t white_63;
color_t blue_48;
color_t red_48;
};
}
}
namespace dsx {
namespace {
struct automap : ::dcx::automap
{
using ::dcx::automap::automap;
#if defined(DXX_BUILD_DESCENT_II)
color_t wall_revealed_color;
#endif
control_info controls;
virtual window_event_result event_handler(const d_event &) override;
};
static void init_automap_subcanvas(grs_subcanvas &view, grs_canvas &container)
{
#if defined(DXX_BUILD_DESCENT_I)
if (MacHog)
gr_init_sub_canvas(view, container, 38*(SWIDTH/640.0), 77*(SHEIGHT/480.0), 564*(SWIDTH/640.0), 381*(SHEIGHT/480.0));
else
#endif
gr_init_sub_canvas(view, container, (SWIDTH/23), (SHEIGHT/6), (SWIDTH/1.1), (SHEIGHT/1.45));
}
static void automap_build_edge_list(automap &am, int add_all_edges);
}
}
namespace dcx {
#define MAX_EDGES_FROM_VERTS(v) ((v)*4)
#define K_WALL_NORMAL_COLOR BM_XRGB(29, 29, 29 )
#define K_WALL_DOOR_COLOR BM_XRGB(5, 27, 5 )
#define K_WALL_DOOR_BLUE BM_XRGB(0, 0, 31)
#define K_WALL_DOOR_GOLD BM_XRGB(31, 31, 0)
#define K_WALL_DOOR_RED BM_XRGB(31, 0, 0)
#define K_WALL_REVEALED_COLOR BM_XRGB(0, 0, 25 ) //what you see when you have the full map powerup
#define K_HOSTAGE_COLOR BM_XRGB(0, 31, 0 )
#define K_FONT_COLOR_20 BM_XRGB(20, 20, 20 )
#define K_GREEN_31 BM_XRGB(0, 31, 0)
int Automap_active = 0;
namespace {
#ifndef NDEBUG
static uint8_t Automap_debug_show_all_segments;
#endif
static void automap_clear_visited(d_level_unique_automap_state &LevelUniqueAutomapState)
{
#ifndef NDEBUG
Automap_debug_show_all_segments = 0;
#endif
LevelUniqueAutomapState.Automap_visited = {};
}
static void init_automap_colors(automap &am)
{
am.wall_normal_color = K_WALL_NORMAL_COLOR;
am.wall_door_color = K_WALL_DOOR_COLOR;
am.wall_door_blue = K_WALL_DOOR_BLUE;
am.wall_door_gold = K_WALL_DOOR_GOLD;
am.wall_door_red = K_WALL_DOOR_RED;
am.hostage_color = K_HOSTAGE_COLOR;
am.green_31 = K_GREEN_31;
am.white_63 = gr_find_closest_color_current(63, 63, 63);
am.blue_48 = gr_find_closest_color_current(0, 0, 48);
am.red_48 = gr_find_closest_color_current(48, 0, 0);
}
void adjust_segment_limit(automap &am, const unsigned SegmentLimit)
{
const auto &depth_array = am.depth_array;
const auto predicate = [&depth_array, SegmentLimit](const segnum_t &e1) {
return depth_array[e1] <= SegmentLimit;
};
for (auto &i : unchecked_partial_range(am.edges.get(), am.end_valid_edges))
{
// Unchecked for speed
const auto &&range = unchecked_partial_range(i.segnum, i.num_faces);
if (std::any_of(range.begin(), range.end(), predicate))
i.flags &= ~EF_TOO_FAR;
else
i.flags |= EF_TOO_FAR;
}
}
}
}
namespace dsx {
namespace {
static void recompute_automap_segment_visibility(const d_level_unique_automap_state &LevelUniqueAutomapState, unsigned compute_depth_all_segments, const segnum_t initial_segnum, automap &am)
{
#ifndef NDEBUG
if (Automap_debug_show_all_segments)
compute_depth_all_segments = 1;
#endif
if (cheats.fullautomap)
compute_depth_all_segments = 1;
automap_build_edge_list(am, compute_depth_all_segments);
am.max_segments_away = set_segment_depths(initial_segnum, compute_depth_all_segments ? nullptr : &LevelUniqueAutomapState.Automap_visited, am.depth_array);
am.segment_limit = am.max_segments_away;
adjust_segment_limit(am, am.segment_limit);
}
#if defined(DXX_BUILD_DESCENT_II)
struct marker_delete_are_you_sure_menu : std::array<newmenu_item, 2>, newmenu
{
using array_type = std::array<newmenu_item, 2>;
d_level_unique_object_state &LevelUniqueObjectState;
segment_array &Segments;
d_marker_state &MarkerState;
marker_delete_are_you_sure_menu(grs_canvas &canvas, d_level_unique_object_state &LevelUniqueObjectState, segment_array &Segments, d_marker_state &MarkerState) :
array_type{{
newmenu_item::nm_item_menu{TXT_YES},
newmenu_item::nm_item_menu{TXT_NO},
}},
newmenu(menu_title{nullptr}, menu_subtitle{"Delete Marker?"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(*static_cast<array_type *>(this), 0), canvas),
LevelUniqueObjectState(LevelUniqueObjectState),
Segments(Segments),
MarkerState(MarkerState)
{
}
virtual window_event_result event_handler(const d_event &) override;
static std::pair<imobjidx_t *, game_marker_index> get_marker_object(d_marker_state &MarkerState);
void handle_selected_yes() const;
};
window_event_result marker_delete_are_you_sure_menu::event_handler(const d_event &event)
{
switch (event.type)
{
case EVENT_NEWMENU_SELECTED:
{
const auto citem = static_cast<const d_select_event &>(event).citem;
if (citem == 0)
/* User chose Yes */
handle_selected_yes();
/* The dialog should close after the user picks Yes or No.
*/
return window_event_result::close;
}
default:
return newmenu::event_handler(event);
}
}
std::pair<imobjidx_t *, game_marker_index> marker_delete_are_you_sure_menu::get_marker_object(d_marker_state &MarkerState)
{
const auto HighlightMarker = MarkerState.HighlightMarker;
if (!MarkerState.imobjidx.valid_index(HighlightMarker))
return {nullptr, HighlightMarker};
auto &mo = MarkerState.imobjidx[HighlightMarker];
return {mo == object_none ? nullptr : &mo, HighlightMarker};
}
void marker_delete_are_you_sure_menu::handle_selected_yes() const
{
const auto [mo, HighlightMarker] = get_marker_object(MarkerState);
if (!mo)
/* Check that the selected marker is still a valid object. */
return;
/* FIXME: this event should be sent to other players
* so that they remove the marker.
*/
obj_delete(LevelUniqueObjectState, Segments, LevelUniqueObjectState.Objects.vmptridx(std::exchange(*mo, object_none)));
MarkerState.message[HighlightMarker] = {};
MarkerState.HighlightMarker = game_marker_index::None;
}
void init_automap_colors(automap &am)
{
::dcx::init_automap_colors(am);
am.wall_revealed_color = K_WALL_REVEALED_COLOR;
}
#endif
// Map movement defines
#define PITCH_DEFAULT 9000
#define ZOOM_DEFAULT i2f(20*10)
#define ZOOM_MIN_VALUE i2f(20*5)
#define ZOOM_MAX_VALUE i2f(20*100)
}
/* MAX_DROP_MULTI_* must be a power of 2 for LastMarkerDropped to work
* properly.
*/
#define MAX_DROP_MULTI_COOP_0 2
#define MAX_DROP_MULTI_COOP_1 4
#define MAX_DROP_MULTI_COOP_P (max_numplayers > 4)
#define MAX_DROP_MULTI_COOP (MAX_DROP_MULTI_COOP_P ? MAX_DROP_MULTI_COOP_0 : MAX_DROP_MULTI_COOP_1)
#define MAX_DROP_MULTI_COMPETITIVE 2
#define MAX_DROP_SINGLE 9
#if defined(DXX_BUILD_DESCENT_II)
marker_message_text_t Marker_input;
static float MarkerScale=2.0;
d_marker_state MarkerState;
game_marker_index convert_player_marker_index_to_game_marker_index(const game_mode_flags game_mode, const unsigned max_numplayers, const unsigned player_num, const player_marker_index player_marker_num)
{
if (game_mode & GM_MULTI_COOP)
return static_cast<game_marker_index>((player_num * MAX_DROP_MULTI_COOP) + static_cast<unsigned>(player_marker_num));
if (game_mode & GM_MULTI)
return static_cast<game_marker_index>((player_num * MAX_DROP_MULTI_COMPETITIVE) + static_cast<unsigned>(player_marker_num));
return game_marker_index{player_marker_num};
}
unsigned d_marker_state::get_markers_per_player(const game_mode_flags game_mode, const unsigned max_numplayers)
{
if (game_mode & GM_MULTI_COOP)
return MAX_DROP_MULTI_COOP;
if (game_mode & GM_MULTI)
return MAX_DROP_MULTI_COMPETITIVE;
return MAX_DROP_SINGLE;
}
xrange<player_marker_index> get_player_marker_range(const unsigned maxdrop)
{
const auto base = player_marker_index::_0;
return {base, static_cast<player_marker_index>(static_cast<unsigned>(base) + maxdrop)};
}
playernum_t get_marker_owner(const game_mode_flags game_mode, const game_marker_index gmi, const unsigned max_numplayers)
{
const auto ugmi = static_cast<unsigned>(gmi);
if (game_mode & GM_MULTI_COOP)
{
/* This is split out to encourage the compiler to recognize that
* the divisor is a constant in every path, and in every path,
* the divisor was chosen to allow use of right shift in place
* of division.
*/
if (MAX_DROP_MULTI_COOP_P)
return ugmi / MAX_DROP_MULTI_COOP_0;
return ugmi / MAX_DROP_MULTI_COOP_1;
}
if (game_mode & GM_MULTI)
return ugmi / MAX_DROP_MULTI_COMPETITIVE;
return 0;
}
namespace {
xrange<game_marker_index> get_game_marker_range(const game_mode_flags game_mode, const unsigned max_numplayers, const unsigned player_num, const unsigned maxdrop)
{
const auto base = convert_player_marker_index_to_game_marker_index(game_mode, max_numplayers, player_num, player_marker_index::_0);
return {base, static_cast<game_marker_index>(static_cast<unsigned>(base) + maxdrop)};
}
}
#endif
# define automap_draw_line g3_draw_line
#if DXX_USE_OGL
#define DrawMarkerNumber(C,a,b,c,d) DrawMarkerNumber(a,b,c,d)
#endif
// -------------------------------------------------------------
namespace {
static void draw_all_edges(automap &am);
#if defined(DXX_BUILD_DESCENT_II)
static void DrawMarkerNumber(grs_canvas &canvas, const automap &am, const game_marker_index gmi, const player_marker_index pmi, const g3s_point &BasePoint)
{
struct xy
{
float x0, y0, x1, y1;
};
static constexpr enumerated_array<std::array<xy, 5>, 9, player_marker_index> sArray = {{{
{{
{-0.25, 0.75, 0, 1},
{0, 1, 0, -1},
{-1, -1, 1, -1},
}},
{{
{-1, 1, 1, 1},
{1, 1, 1, 0},
{-1, 0, 1, 0},
{-1, 0, -1, -1},
{-1, -1, 1, -1}
}},
{{
{-1, 1, 1, 1},
{1, 1, 1, -1},
{-1, -1, 1, -1},
{0, 0, 1, 0},
}},
{{
{-1, 1, -1, 0},
{-1, 0, 1, 0},
{1, 1, 1, -1},
}},
{{
{-1, 1, 1, 1},
{-1, 1, -1, 0},
{-1, 0, 1, 0},
{1, 0, 1, -1},
{-1, -1, 1, -1}
}},
{{
{-1, 1, 1, 1},
{-1, 1, -1, -1},
{-1, -1, 1, -1},
{1, -1, 1, 0},
{-1, 0, 1, 0}
}},
{{
{-1, 1, 1, 1},
{1, 1, 1, -1},
}},
{{
{-1, 1, 1, 1},
{1, 1, 1, -1},
{-1, -1, 1, -1},
{-1, -1, -1, 1},
{-1, 0, 1, 0}
}},
{{
{-1, 1, 1, 1},
{1, 1, 1, -1},
{-1, 0, 1, 0},
{-1, 0, -1, 1},
}}
}}};
static constexpr enumerated_array<uint_fast8_t, 9, player_marker_index> NumOfPoints = {{{3, 5, 4, 3, 5, 5, 2, 5, 4}}};
const auto color = (gmi == MarkerState.HighlightMarker ? am.white_63 : am.blue_48);
const auto scale_x = Matrix_scale.x;
const auto scale_y = Matrix_scale.y;
range_for (const auto &i, unchecked_partial_range(sArray[pmi], NumOfPoints[pmi]))
{
const auto ax0 = i.x0 * MarkerScale;
const auto ay0 = i.y0 * MarkerScale;
const auto ax1 = i.x1 * MarkerScale;
const auto ay1 = i.y1 * MarkerScale;
auto FromPoint = BasePoint;
auto ToPoint = BasePoint;
FromPoint.p3_x += fixmul(fl2f(ax0), scale_x);
FromPoint.p3_y += fixmul(fl2f(ay0), scale_y);
ToPoint.p3_x += fixmul(fl2f(ax1), scale_x);
ToPoint.p3_y += fixmul(fl2f(ay1), scale_y);
g3_code_point(FromPoint);
g3_code_point(ToPoint);
g3_project_point(FromPoint);
g3_project_point(ToPoint);
automap_draw_line(canvas, FromPoint, ToPoint, color);
}
}
static void DropMarker(fvmobjptridx &vmobjptridx, fvmsegptridx &vmsegptridx, const object &plrobj, const game_marker_index marker_num, const player_marker_index player_marker_num)
{
auto &marker_objidx = MarkerState.imobjidx[marker_num];
if (marker_objidx != object_none)
obj_delete(LevelUniqueObjectState, Segments, vmobjptridx(marker_objidx));
marker_objidx = drop_marker_object(plrobj.pos, vmsegptridx(plrobj.segnum), plrobj.orient, marker_num);
if (Game_mode & GM_MULTI)
multi_send_drop_marker(Player_num, plrobj.pos, player_marker_num, MarkerState.message[marker_num]);
}
}
void DropBuddyMarker(object &objp)
{
auto &Objects = LevelUniqueObjectState.Objects;
auto &vmobjptridx = Objects.vmptridx;
constexpr auto marker_num = game_marker_index::GuidebotDeathSite;
static_assert(MarkerState.message.valid_index(marker_num), "not enough markers");
auto &MarkerMessage = MarkerState.message[marker_num];
snprintf(&MarkerMessage[0], MarkerMessage.size(), "RIP: %s", static_cast<const char *>(PlayerCfg.GuidebotName));
auto &marker_objidx = MarkerState.imobjidx[marker_num];
if (marker_objidx != object_none)
obj_delete(LevelUniqueObjectState, Segments, vmobjptridx(marker_objidx));
marker_objidx = drop_marker_object(objp.pos, vmsegptridx(objp.segnum), objp.orient, marker_num);
}
namespace {
#define MARKER_SPHERE_SIZE 0x58000
static void DrawMarkers(fvcobjptr &vcobjptr, grs_canvas &canvas, automap &am)
{
static int cyc=10,cycdir=1;
const auto game_mode = Game_mode;
const auto max_numplayers = Netgame.max_numplayers;
const auto maxdrop = MarkerState.get_markers_per_player(game_mode, max_numplayers);
const auto &&game_marker_range = get_game_marker_range(game_mode, max_numplayers, Player_num, maxdrop);
const auto &&player_marker_range = get_player_marker_range(maxdrop);
const auto &&zipped_marker_range = zip(game_marker_range, player_marker_range, unchecked_partial_range(&MarkerState.imobjidx[*game_marker_range.begin()], maxdrop));
const auto &&mb = zipped_marker_range.begin();
const auto &&me = zipped_marker_range.end();
auto iter = mb;
/* Find the first marker object in the player's marker range that is
* not object_none. If every marker object in the range is
* object_none, then there are no markers to draw, so return.
*/
for (;;)
{
auto &&[gmi, pmi, objidx] = *iter;
(void)gmi;
(void)pmi;
if (objidx != object_none)
break;
if (++ iter == me)
return;
}
/* A marker was found, so at least one marker will be drawn. Set up
* colors for the markers.
*/
const auto current_cycle_color = cyc;
const std::array<color_t, 3> colors{{
gr_find_closest_color_current(current_cycle_color, 0, 0),
gr_find_closest_color_current(current_cycle_color + 10, 0, 0),
gr_find_closest_color_current(current_cycle_color + 20, 0, 0),
}};
for (; iter != me; ++iter)
{
auto &&[gmi, pmi, objidx] = *iter;
if (objidx != object_none)
{
/* Use cg3s_point so that the type is const for OpenGL and
* mutable for SDL-only.
*/
cg3s_point &&sphere_point = g3_rotate_point(vcobjptr(objidx)->pos);
g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE, colors[0]);
g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE / 2, colors[1]);
g3_draw_sphere(canvas, sphere_point, MARKER_SPHERE_SIZE / 4, colors[2]);
DrawMarkerNumber(canvas, am, gmi, pmi, sphere_point);
}
}
if (cycdir)
cyc+=2;
else
cyc-=2;
if (cyc>43)
{
cyc=43;
cycdir=0;
}
else if (cyc<10)
{
cyc=10;
cycdir=1;
}
}
static void ClearMarkers()
{
static_cast<d_marker_object_numbers &>(MarkerState) = {};
MarkerState.message = {};
}
#endif
}
void automap_clear_visited(d_level_unique_automap_state &LevelUniqueAutomapState)
{
#if defined(DXX_BUILD_DESCENT_II)
ClearMarkers();
#endif
::dcx::automap_clear_visited(LevelUniqueAutomapState);
}
namespace {
static void draw_player(grs_canvas &canvas, const object_base &obj, const uint8_t color)
{
// Draw Console player -- shaped like a ellipse with an arrow.
auto sphere_point = g3_rotate_point(obj.pos);
const auto obj_size = obj.size;
g3_draw_sphere(canvas, sphere_point, obj_size, color);
// Draw shaft of arrow
const auto &&head_pos = vm_vec_scale_add(obj.pos, obj.orient.fvec, obj_size * 2);
{
auto &&arrow_point = g3_rotate_point(vm_vec_scale_add(obj.pos, obj.orient.fvec, obj_size * 3));
automap_draw_line(canvas, sphere_point, arrow_point, color);
// Draw right head of arrow
{
const auto &&rhead_pos = vm_vec_scale_add(head_pos, obj.orient.rvec, obj_size);
auto head_point = g3_rotate_point(rhead_pos);
automap_draw_line(canvas, arrow_point, head_point, color);
}
// Draw left head of arrow
{
const auto &&lhead_pos = vm_vec_scale_add(head_pos, obj.orient.rvec, -obj_size);
auto head_point = g3_rotate_point(lhead_pos);
automap_draw_line(canvas, arrow_point, head_point, color);
}
}
// Draw player's up vector
{
const auto &&arrow_pos = vm_vec_scale_add(obj.pos, obj.orient.uvec, obj_size * 2);
auto arrow_point = g3_rotate_point(arrow_pos);
automap_draw_line(canvas, sphere_point, arrow_point, color);
}
}
#if defined(DXX_BUILD_DESCENT_II)
//name for each group. maybe move somewhere else
constexpr char system_name[][17] = {
"Zeta Aquilae",
"Quartzon System",
"Brimspark System",
"Limefrost Spiral",
"Baloris Prime",
"Omega System"};
#endif
static void name_frame(grs_canvas &canvas, automap &am)
{
gr_set_fontcolor(canvas, am.green_31, -1);
char name_level_left[128];
auto &game_font = *GAME_FONT;
#if defined(DXX_BUILD_DESCENT_I)
const char *name_level;
if (Current_level_num > 0)
{
snprintf(name_level_left, sizeof(name_level_left), "%s %i: %s",TXT_LEVEL, Current_level_num, static_cast<const char *>(Current_level_name));
name_level = name_level_left;
}
else
name_level = Current_level_name;
gr_string(canvas, game_font, (SWIDTH / 64), (SHEIGHT / 48), name_level);
#elif defined(DXX_BUILD_DESCENT_II)
char name_level_right[128];
if (Current_level_num > 0)
snprintf(name_level_left, sizeof(name_level_left), "%s %i",TXT_LEVEL, Current_level_num);
else
snprintf(name_level_left, sizeof(name_level_left), "Secret Level %i",-Current_level_num);
const char *const current_level_name = Current_level_name;
if (PLAYING_BUILTIN_MISSION && Current_level_num > 0)
snprintf(name_level_right, sizeof(name_level_right), "%s %d: %s", system_name[(Current_level_num-1)/4], ((Current_level_num - 1) % 4) + 1, current_level_name);
else
snprintf(name_level_right, sizeof(name_level_right), " %s", current_level_name);
gr_string(canvas, game_font, (SWIDTH / 64), (SHEIGHT / 48), name_level_left);
const auto &&[wr, h] = gr_get_string_size(game_font, name_level_right);
gr_string(canvas, game_font, canvas.cv_bitmap.bm_w - wr - (SWIDTH / 64), (SHEIGHT / 48), name_level_right, wr, h);
#endif
}
static void automap_apply_input(automap &am, const vms_matrix &plrorient, const vms_vector &plrpos)
{
constexpr int SLIDE_SPEED = 350;
constexpr int ZOOM_SPEED_FACTOR = 500; //(1500)
constexpr int ROT_SPEED_DIVISOR = 115000;
if (PlayerCfg.AutomapFreeFlight)
{
if (am.controls.state.fire_primary)
{
// Reset orientation
am.controls.state.fire_primary = 0;
am.viewMatrix = plrorient;
vm_vec_scale_add(am.view_position, plrpos, am.viewMatrix.fvec, -ZOOM_DEFAULT);
}
if (am.controls.pitch_time || am.controls.heading_time || am.controls.bank_time)
{
vms_angvec tangles;
tangles.p = fixdiv(am.controls.pitch_time, ROT_SPEED_DIVISOR);
tangles.h = fixdiv(am.controls.heading_time, ROT_SPEED_DIVISOR);
tangles.b = fixdiv(am.controls.bank_time, ROT_SPEED_DIVISOR * 2);
const auto &&tempm = vm_angles_2_matrix(tangles);
am.viewMatrix = vm_matrix_x_matrix(am.viewMatrix, tempm);
check_and_fix_matrix(am.viewMatrix);
}
if (am.controls.forward_thrust_time || am.controls.vertical_thrust_time || am.controls.sideways_thrust_time)
{
vm_vec_scale_add2(am.view_position, am.viewMatrix.fvec, am.controls.forward_thrust_time * ZOOM_SPEED_FACTOR);
vm_vec_scale_add2(am.view_position, am.viewMatrix.uvec, am.controls.vertical_thrust_time * SLIDE_SPEED);
vm_vec_scale_add2(am.view_position, am.viewMatrix.rvec, am.controls.sideways_thrust_time * SLIDE_SPEED);
// Crude wrapping check
clamp_fix_symmetric(am.view_position.x, F1_0*32000);
clamp_fix_symmetric(am.view_position.y, F1_0*32000);
clamp_fix_symmetric(am.view_position.z, F1_0*32000);
}
}
else
{
if (am.controls.state.fire_primary)
{
// Reset orientation
am.viewDist = ZOOM_DEFAULT;
am.tangles.p = PITCH_DEFAULT;
am.tangles.h = 0;
am.tangles.b = 0;
am.view_target = plrpos;
am.controls.state.fire_primary = 0;
}
am.viewDist -= am.controls.forward_thrust_time * ZOOM_SPEED_FACTOR;
am.tangles.p += fixdiv(am.controls.pitch_time, ROT_SPEED_DIVISOR);
am.tangles.h += fixdiv(am.controls.heading_time, ROT_SPEED_DIVISOR);
am.tangles.b += fixdiv(am.controls.bank_time, ROT_SPEED_DIVISOR * 2);
if (am.controls.vertical_thrust_time || am.controls.sideways_thrust_time)
{
vms_angvec tangles1;
vms_vector old_vt;
old_vt = am.view_target;
tangles1 = am.tangles;
const auto &&tempm = vm_angles_2_matrix(tangles1);
vm_matrix_x_matrix(am.viewMatrix, plrorient, tempm);
vm_vec_scale_add2(am.view_target, am.viewMatrix.uvec, am.controls.vertical_thrust_time * SLIDE_SPEED);
vm_vec_scale_add2(am.view_target, am.viewMatrix.rvec, am.controls.sideways_thrust_time * SLIDE_SPEED);
if (vm_vec_dist_quick(am.view_target, plrpos) > i2f(1000))
am.view_target = old_vt;
}
const auto &&tempm = vm_angles_2_matrix(am.tangles);
vm_matrix_x_matrix(am.viewMatrix, plrorient, tempm);
clamp_fix_lh(am.viewDist, ZOOM_MIN_VALUE, ZOOM_MAX_VALUE);
}
}
static void draw_automap(fvcobjptr &vcobjptr, automap &am)
{
if ( am.leave_mode==0 && am.controls.state.automap && (timer_query()-am.entry_time)>LEAVE_TIME)
am.leave_mode = 1;
{
auto &canvas = am.w_canv;
show_fullscr(canvas, am.automap_background);
gr_set_fontcolor(canvas, BM_XRGB(20, 20, 20), -1);
{
int x, y;
#if defined(DXX_BUILD_DESCENT_I)
if (MacHog)
x = 80 * (SWIDTH / 640.), y = 36 * (SHEIGHT / 480.);
else
#endif
x = SWIDTH / 8, y = SHEIGHT / 16;
gr_string(canvas, *HUGE_FONT, x, y, TXT_AUTOMAP);
}
{
int x;
int y0, y1, y2;
#if defined(DXX_BUILD_DESCENT_I)
const auto s1 = TXT_SLIDE_UPDOWN;
const auto &s2 = "F9/F10 Changes viewing distance";
if (!MacHog)
{
x = SWIDTH / 4.923;
y0 = SHEIGHT / 1.126;
y1 = SHEIGHT / 1.083;
y2 = SHEIGHT / 1.043;
}
else
{
// for the Mac automap they're shown up the top, hence the different layout
x = 265 * (SWIDTH / 640.);
y0 = 27 * (SHEIGHT / 480.);
y1 = 44 * (SHEIGHT / 480.);
y2 = 61 * (SHEIGHT / 480.);
}
#elif defined(DXX_BUILD_DESCENT_II)
const auto &s1 = "F9/F10 Changes viewing distance";
const auto s2 = TXT_AUTOMAP_MARKER;
x = SWIDTH / 10.666;
y0 = SHEIGHT / 1.126;
y1 = SHEIGHT / 1.083;
y2 = SHEIGHT / 1.043;
#endif
auto &game_font = *GAME_FONT;
gr_string(canvas, game_font, x, y0, TXT_TURN_SHIP);
gr_string(canvas, game_font, x, y1, s1);
gr_string(canvas, game_font, x, y2, s2);
}
}
auto &canvas = am.automap_view;
gr_clear_canvas(canvas, BM_XRGB(0,0,0));
g3_start_frame(canvas);
render_start_frame();
if (!PlayerCfg.AutomapFreeFlight)
vm_vec_scale_add(am.view_position,am.view_target,am.viewMatrix.fvec,-am.viewDist);
g3_set_view_matrix(am.view_position,am.viewMatrix,am.zoom);
draw_all_edges(am);
// Draw player...
const auto &self_ship_rgb = player_rgb[get_player_or_team_color(Player_num)];
const auto closest_color = BM_XRGB(self_ship_rgb.r, self_ship_rgb.g, self_ship_rgb.b);
draw_player(canvas, vcobjptr(get_local_player().objnum), closest_color);
#if defined(DXX_BUILD_DESCENT_II)
DrawMarkers(vcobjptr, canvas, am);
#endif
// Draw player(s)...
const unsigned show_all_players = (Game_mode & GM_MULTI_COOP) || Netgame.game_flag.show_on_map;
if (show_all_players || (Game_mode & GM_TEAM))
{
const unsigned local_player_team = get_team(Player_num);
for (unsigned i = 0; i < N_players; ++i)
{
if (i == Player_num)
continue;
if (show_all_players || local_player_team == get_team(i))
{
auto &plr = *vcplayerptr(i);
auto &objp = *vcobjptr(plr.objnum);
if (objp.type == OBJ_PLAYER)
{
const auto &other_ship_rgb = player_rgb[get_player_or_team_color(i)];
draw_player(canvas, objp, BM_XRGB(other_ship_rgb.r, other_ship_rgb.g, other_ship_rgb.b));
}
}
}
}
range_for (const auto &&objp, vcobjptr)
{
switch( objp->type ) {
case OBJ_HOSTAGE:
{
auto sphere_point = g3_rotate_point(objp->pos);
g3_draw_sphere(canvas, sphere_point, objp->size, am.hostage_color);
}
break;
case OBJ_POWERUP:
if (LevelUniqueAutomapState.Automap_visited[objp->segnum]
#ifndef NDEBUG
|| Automap_debug_show_all_segments
#endif
)
{
ubyte id = get_powerup_id(objp);
unsigned r, g, b;
if (id==POW_KEY_RED)
r = 63 * 2, g = 5 * 2, b = 5 * 2;
else if (id==POW_KEY_BLUE)
r = 5 * 2, g = 5 * 2, b = 63 * 2;
else if (id==POW_KEY_GOLD)
r = 63 * 2, g = 63 * 2, b = 10 * 2;
else
break;
{
const auto color = gr_find_closest_color(r, g, b);
auto sphere_point = g3_rotate_point(objp->pos);
g3_draw_sphere(canvas, sphere_point, objp->size * 4, color);
}
}
break;
default:
break;
}
}
g3_end_frame();
name_frame(canvas, am);
#if defined(DXX_BUILD_DESCENT_II)
{
const auto HighlightMarker = MarkerState.HighlightMarker;
if (MarkerState.message.valid_index(HighlightMarker))
{
auto &m = MarkerState.message[HighlightMarker];
gr_printf(canvas, *canvas.cv_font, (SWIDTH/64), (SHEIGHT/18), "Marker %u%c %s", static_cast<unsigned>(HighlightMarker) + 1, m[0] ? ':' : 0, &m[0]);
}
}
#endif
if (PlayerCfg.MouseFlightSim && PlayerCfg.MouseFSIndicator)
{
const auto gwidth = canvas.cv_bitmap.bm_w;
const auto gheight = canvas.cv_bitmap.bm_h;
auto &raw_mouse_axis = am.controls.raw_mouse_axis;
show_mousefs_indicator(canvas, raw_mouse_axis[0], raw_mouse_axis[1], raw_mouse_axis[2], gwidth - (gheight / 8), gheight - (gheight / 8), gheight / 5);
}
am.t2 = timer_query();
const auto vsync = CGameCfg.VSync;
const auto bound = F1_0 / (vsync ? MAXIMUM_FPS : CGameArg.SysMaxFPS);
const auto may_sleep = !CGameArg.SysNoNiceFPS && !vsync;
while (am.t2 - am.t1 < bound) // ogl is fast enough that the automap can read the input too fast and you start to turn really slow. So delay a bit (and free up some cpu :)
{
if (Game_mode & GM_MULTI)
multi_do_frame(); // during long wait, keep packets flowing
if (may_sleep)
timer_delay(F1_0>>8);
am.t2 = timer_update();
}
if (am.pause_game)
{
FrameTime=am.t2-am.t1;
calc_d_tick();
}
am.t1 = am.t2;
}
#if defined(DXX_BUILD_DESCENT_I)
#define MAP_BACKGROUND_FILENAME (((SWIDTH>=640&&SHEIGHT>=480) && PHYSFSX_exists("maph.pcx",1))?"maph.pcx":"map.pcx")
#elif defined(DXX_BUILD_DESCENT_II)
#define MAP_BACKGROUND_FILENAME ((HIRESMODE && PHYSFSX_exists("mapb.pcx",1))?"mapb.pcx":"map.pcx")
#endif
static void recompute_automap_segment_visibility(const d_level_unique_automap_state &LevelUniqueAutomapState, const object &plrobj, automap &am)
{
recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj.ctype.player_info.powerup_flags & PLAYER_FLAGS_MAP_ALL, plrobj.segnum, am);
}
static window_event_result automap_key_command(const d_event &event, automap &am)
{
#if defined(DXX_BUILD_DESCENT_I) || !defined(NDEBUG)
auto &Objects = LevelUniqueObjectState.Objects;
auto &vmobjptr = Objects.vmptr;
#endif
int c = event_key_get(event);
switch (c)
{
#if DXX_USE_SCREENSHOT
case KEY_PRINT_SCREEN: {
gr_set_default_canvas();
save_screen_shot(1);
return window_event_result::handled;
}
#endif
case KEY_ESC:
if (am.leave_mode == 0)
{
return window_event_result::close;
}
return window_event_result::handled;
#if defined(DXX_BUILD_DESCENT_I)
case KEY_ALTED+KEY_F: // Alt+F shows full map, if cheats enabled
if (cheats.enabled)
{
cheats.fullautomap = !cheats.fullautomap;
// if cheat of map powerup, work with full depth
auto &plrobj = get_local_plrobj();
recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, am);
}
return window_event_result::handled;
#endif
#ifndef NDEBUG
case KEY_DEBUGGED+KEY_F: {
Automap_debug_show_all_segments = !Automap_debug_show_all_segments;
auto &plrobj = get_local_plrobj();
recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, am);
}
return window_event_result::handled;
#endif
case KEY_F9:
if (am.segment_limit > 1)
{
am.segment_limit--;
adjust_segment_limit(am, am.segment_limit);
}
return window_event_result::handled;
case KEY_F10:
if (am.segment_limit < am.max_segments_away) {
am.segment_limit++;
adjust_segment_limit(am, am.segment_limit);
}
return window_event_result::handled;
#if defined(DXX_BUILD_DESCENT_II)
case KEY_1:
case KEY_2:
case KEY_3:
case KEY_4:
case KEY_5:
case KEY_6:
case KEY_7:
case KEY_8:
case KEY_9:
case KEY_0:
{
const auto game_mode = Game_mode;
const auto max_numplayers = Netgame.max_numplayers;
const auto maxdrop = MarkerState.get_markers_per_player(game_mode, max_numplayers);
const uint8_t marker_num = c - KEY_1;
if (marker_num <= maxdrop)
{
const auto gmi = convert_player_marker_index_to_game_marker_index(game_mode, max_numplayers, Player_num, player_marker_index{marker_num});
if (MarkerState.imobjidx[gmi] != object_none)
MarkerState.HighlightMarker = gmi;
}
else
MarkerState.HighlightMarker = game_marker_index::None;
}
return window_event_result::handled;
case KEY_D+KEY_CTRLED:
{
if (const auto [mo, HighlightMarker] = marker_delete_are_you_sure_menu::get_marker_object(MarkerState); mo == nullptr)
{
(void)HighlightMarker;
/* If the selected index is not a valid marker, do
* not offer to delete anything.
*/
return window_event_result::handled;
}
auto menu = window_create<marker_delete_are_you_sure_menu>(grd_curscreen->sc_canvas, LevelUniqueObjectState, Segments, MarkerState);
(void)menu;
}
return window_event_result::handled;
#ifndef RELEASE
case KEY_F11: //KEY_COMMA:
if (MarkerScale>.5)
MarkerScale-=.5;
return window_event_result::handled;
case KEY_F12: //KEY_PERIOD:
if (MarkerScale<30.0)
MarkerScale+=.5;
return window_event_result::handled;
#endif
#endif
}
return window_event_result::ignored;
}
static window_event_result automap_process_input(const d_event &event, automap &am)
{
kconfig_read_controls(am.controls, event, 1);
if (!am.controls.state.automap && am.leave_mode == 1)
{
return window_event_result::close;
}
if (am.controls.state.automap)
{
am.controls.state.automap = 0;
if (am.leave_mode == 0)
{
return window_event_result::close;
}
}
return window_event_result::ignored;
}
}
window_event_result automap::event_handler(const d_event &event)
{
auto &Objects = LevelUniqueObjectState.Objects;
auto &vcobjptr = Objects.vcptr;
auto &vmobjptr = Objects.vmptr;
switch (event.type)
{
case EVENT_WINDOW_ACTIVATED:
game_flush_inputs(controls);
event_toggle_focus(1);
key_toggle_repeat(0);
break;
case EVENT_WINDOW_DEACTIVATED:
event_toggle_focus(0);
key_toggle_repeat(1);
break;
#if SDL_MAJOR_VERSION == 2
case EVENT_WINDOW_RESIZE:
init_automap_subcanvas(automap_view, grd_curscreen->sc_canvas);
break;
#endif
case EVENT_IDLE:
#if DXX_MAX_BUTTONS_PER_JOYSTICK
case EVENT_JOYSTICK_BUTTON_UP:
case EVENT_JOYSTICK_BUTTON_DOWN:
#endif
#if DXX_MAX_AXES_PER_JOYSTICK
case EVENT_JOYSTICK_MOVED:
#endif
case EVENT_MOUSE_BUTTON_UP:
case EVENT_MOUSE_BUTTON_DOWN:
case EVENT_MOUSE_MOVED:
case EVENT_KEY_RELEASE:
return automap_process_input(event, *this);
case EVENT_KEY_COMMAND:
{
window_event_result kret = automap_key_command(event, *this);
if (kret == window_event_result::ignored)
kret = automap_process_input(event, *this);
return kret;
}
case EVENT_WINDOW_DRAW:
{
auto &plrobj = get_local_plrobj();
automap_apply_input(*this, plrobj.orient, plrobj.pos);
}
draw_automap(vcobjptr, *this);
break;
case EVENT_WINDOW_CLOSE:
if (!pause_game)
ConsoleObject->mtype.phys_info.flags |= old_wiggle; // Restore wiggle
event_toggle_focus(0);
key_toggle_repeat(1);
/* grd_curcanv points to `automap_view`, so grd_curcanv
* would become a dangling pointer after the call to delete.
* Redirect it to the default screen to avoid pointing to
* freed memory. Setting grd_curcanv to nullptr would be
* better, but some code assumes that grd_curcanv is never
* nullptr, so instead set it to the default canvas.
* Eventually, grd_curcanv will be removed entirely.
*/
gr_set_default_canvas();
Game_wind->set_visible(1);
Automap_active = 0;
multi_send_msgsend_state(msgsend_state::none);
return window_event_result::ignored; // continue closing
case EVENT_LOOP_BEGIN_LOOP:
kconfig_begin_loop(controls);
break;
case EVENT_LOOP_END_LOOP:
kconfig_end_loop(controls, FrameTime);
break;
default:
return window_event_result::ignored;
break;
}
return window_event_result::handled;
}
void do_automap()
{
auto &Objects = LevelUniqueObjectState.Objects;
auto &vmobjptr = Objects.vmptr;
palette_array_t pal;
auto am = window_create<automap>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
const auto max_edges = LevelSharedSegmentState.Num_segments * 12;
am->max_edges = max_edges;
am->edges = std::make_unique<Edge_info[]>(max_edges);
am->drawingListBright = std::make_unique<Edge_info *[]>(max_edges);
init_automap_colors(*am);
am->pause_game = !((Game_mode & GM_MULTI) && (!Endlevel_sequence)); // Set to 1 if everything is paused during automap...No pause during net.
if (am->pause_game) {
Game_wind->set_visible(0);
}
else
{
am->old_wiggle = ConsoleObject->mtype.phys_info.flags & PF_WIGGLE; // Save old wiggle
ConsoleObject->mtype.phys_info.flags &= ~PF_WIGGLE; // Turn off wiggle
}
//Max_edges = min(MAX_EDGES_FROM_VERTS(Num_vertices),MAX_EDGES); //make maybe smaller than max
gr_set_default_canvas();
if ( am->viewDist==0 )
am->viewDist = ZOOM_DEFAULT;
auto &plrobj = get_local_plrobj();
am->viewMatrix = plrobj.orient;
am->tangles.p = PITCH_DEFAULT;
am->tangles.h = 0;
am->tangles.b = 0;
am->view_target = plrobj.pos;
if (PlayerCfg.AutomapFreeFlight)
vm_vec_scale_add(am->view_position, plrobj.pos, am->viewMatrix.fvec, -ZOOM_DEFAULT);
am->t1 = am->entry_time = timer_query();
am->t2 = am->t1;
//Fill in Automap_visited from Objects[Players[Player_num].objnum].segnum
recompute_automap_segment_visibility(LevelUniqueAutomapState, plrobj, *am);
// ZICO - code from above to show frame in OGL correctly. Redundant, but better readable.
// KREATOR - Now applies to all platforms so double buffering is supported
{
const auto pcx_error = pcx_read_bitmap_or_default(MAP_BACKGROUND_FILENAME, am->automap_background, pal);
if (pcx_error != pcx_result::SUCCESS)
con_printf(CON_URGENT, DXX_STRINGIZE_FL(__FILE__, __LINE__, "automap: File %s - PCX error: %s"), MAP_BACKGROUND_FILENAME, pcx_errormsg(pcx_error));
gr_remap_bitmap_good(am->automap_background, pal, -1, -1);
}
init_automap_subcanvas(am->automap_view, grd_curscreen->sc_canvas);
gr_palette_load( gr_palette );
Automap_active = 1;
multi_send_msgsend_state(msgsend_state::automap);
}
namespace {
void draw_all_edges(automap &am)
{
#if !DXX_USE_OGL
grs_canvas &canvas = am.automap_view;
#endif
auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
auto &Vertices = LevelSharedVertexState.get_vertices();
int j;
unsigned nbright = 0;
ubyte nfacing,nnfacing;
fix distance;
fix min_distance = INT32_MAX;
auto &vcvertptr = Vertices.vcptr;
range_for (auto &i, unchecked_partial_range(am.edges.get(), am.end_valid_edges))
{
const auto e = &i;
if (!(e->flags & EF_USED)) continue;
if ( e->flags & EF_TOO_FAR) continue;
if (e->flags&EF_FRONTIER) { // A line that is between what we have seen and what we haven't
if ((!(e->flags & EF_SECRET)) && (e->color == am.wall_normal_color))
continue; // If a line isn't secret and is normal color, then don't draw it
}
distance = Segment_points[e->verts[1]].p3_z;
if (min_distance>distance )
min_distance = distance;
if (!rotate_list(vcvertptr, e->verts).uand)
{ //all off screen?
nfacing = nnfacing = 0;
auto &tv1 = *vcvertptr(e->verts[0]);
j = 0;
while( j<e->num_faces && (nfacing==0 || nnfacing==0) ) {
if (!g3_check_normal_facing(tv1, vcsegptr(e->segnum[j])->shared_segment::sides[e->sides[j]].normals[0]))
nfacing++;
else
nnfacing++;
j++;
}
if ( nfacing && nnfacing ) {
// a contour line
am.drawingListBright[nbright++] = e;
} else if ( e->flags&(EF_DEFINING|EF_GRATE) ) {
if ( nfacing == 0 ) {
const uint8_t color = (e->flags & EF_NO_FADE)
? e->color
: gr_fade_table[(gr_fade_level{8})][e->color];
g3_draw_line(canvas, Segment_points[e->verts[0]], Segment_points[e->verts[1]], color);
} else {
am.drawingListBright[nbright++] = e;
}
}
}
}
if ( min_distance < 0 ) min_distance = 0;
// Sort the bright ones using a shell sort
const auto &&range = unchecked_partial_range(am.drawingListBright.get(), nbright);
std::sort(range.begin(), range.end(), [](const Edge_info *const a, const Edge_info *const b) {
const auto &v1 = a->verts[0];
const auto &v2 = b->verts[0];
return Segment_points[v1].p3_z < Segment_points[v2].p3_z;
});
// Draw the bright ones
range_for (const auto e, range)
{
const auto p1 = &Segment_points[e->verts[0]];
const auto p2 = &Segment_points[e->verts[1]];
fix dist;
dist = p1->p3_z - min_distance;
// Make distance be 1.0 to 0.0, where 0.0 is 10 segments away;
if ( dist < 0 ) dist=0;
if ( dist >= am.farthest_dist ) continue;
const auto color = (e->flags & EF_NO_FADE)
? e->color
: gr_fade_table[static_cast<gr_fade_level>(f2i((F1_0 - fixdiv(dist, am.farthest_dist)) * 31))][e->color];
g3_draw_line(canvas, *p1, *p2, color);
}
}
//==================================================================
//
// All routines below here are used to build the Edge list
//
//==================================================================
//finds edge, filling in edge_ptr. if found old edge, returns index, else return -1
static std::pair<Edge_info &, std::size_t> automap_find_edge(automap &am, const vertnum_t v0, const vertnum_t v1)
{
const auto &&hash_object = std::hash<vertnum_t>{};
const auto initial_hash_slot = (hash_object(v0) ^ (hash_object(v1) << 10)) % am.max_edges;
for (auto current_hash_slot = initial_hash_slot;;)
{
auto &e = am.edges[current_hash_slot];
const auto ev0 = e.verts[0];
const auto ev1 = e.verts[1];
if (e.num_faces == 0)
return {e, current_hash_slot};
if (v1 == ev1 && v0 == ev0)
return {e, UINT32_MAX};
else {
if (++ current_hash_slot == am.max_edges)
current_hash_slot = 0;
if (current_hash_slot == initial_hash_slot)
throw std::runtime_error("edge list full: search wrapped without finding a free slot");
}
}
}
static void add_one_edge(automap &am, vertnum_t va, vertnum_t vb, const uint8_t color, const sidenum_t side, const segnum_t segnum, const uint8_t flags)
{
if (am.num_edges >= am.max_edges)
{
// GET JOHN! (And tell him that his
// MAX_EDGES_FROM_VERTS formula is hosed.)
// If he's not around, save the mine,
// and send him mail so he can look
// at the mine later. Don't modify it.
// This is important if this happens.
Int3(); // LOOK ABOVE!!!!!!
return;
}
if ( va > vb ) {
std::swap(va, vb);
}
const auto &&ef = automap_find_edge(am, va, vb);
const auto e = &ef.first;
if (ef.second != UINT32_MAX)
{
e->verts[0] = va;
e->verts[1] = vb;
e->color = color;
e->num_faces = 1;
e->flags = EF_USED | EF_DEFINING; // Assume a normal line
e->sides[0] = side;
e->segnum[0] = segnum;
++ am.num_edges;
const auto i = ef.second + 1;
if (am.end_valid_edges < i)
am.end_valid_edges = i;
} else {
if ( color != am.wall_normal_color )
#if defined(DXX_BUILD_DESCENT_II)
if (color != am.wall_revealed_color)
#endif
e->color = color;
if ( e->num_faces < 4 ) {
e->sides[e->num_faces] = side;
e->segnum[e->num_faces] = segnum;
e->num_faces++;
}
}
e->flags |= flags;
}
static void add_one_unknown_edge(automap &am, vertnum_t va, vertnum_t vb)
{
if ( va > vb ) {
std::swap(va, vb);
}
const auto &&ef = automap_find_edge(am, va, vb);
if (ef.second == UINT32_MAX)
ef.first.flags |= EF_FRONTIER; // Mark as a border edge
}
static void add_segment_edges(fvcsegptr &vcsegptr, fvcwallptr &vcwallptr, automap &am, const vcsegptridx_t seg)
{
auto &ControlCenterState = LevelUniqueObjectState.ControlCenterState;
auto &WallAnims = GameSharedState.WallAnims;
#if defined(DXX_BUILD_DESCENT_II)
auto &Objects = LevelUniqueObjectState.Objects;
auto &vmobjptr = Objects.vmptr;
#endif
for (const auto sn : MAX_SIDES_PER_SEGMENT)
{
uint8_t hidden_flag = 0;
uint8_t is_grate = 0;
uint8_t no_fade = 0;
uint8_t color = 255;
if (seg->shared_segment::children[sn] == segment_none) {
color = am.wall_normal_color;
}
switch( seg->special ) {
case segment_special::nothing:
case segment_special::repaircen:
case segment_special::goal_blue:
case segment_special::goal_red:
break;
case segment_special::fuelcen:
color = BM_XRGB( 29, 27, 13 );
break;
case segment_special::controlcen:
if (ControlCenterState.Control_center_present)
color = BM_XRGB( 29, 0, 0 );
break;
case segment_special::robotmaker:
color = BM_XRGB( 29, 0, 31 );
break;
}
const auto wall_num = seg->shared_segment::sides[sn].wall_num;
if (wall_num != wall_none)
{
auto &w = *vcwallptr(wall_num);
#if defined(DXX_BUILD_DESCENT_II)
auto trigger_num = w.trigger;
auto &Triggers = LevelUniqueWallSubsystemState.Triggers;
auto &vmtrgptr = Triggers.vmptr;
if (trigger_num != trigger_none && vmtrgptr(trigger_num)->type == trigger_action::secret_exit)
{
color = BM_XRGB( 29, 0, 31 );
no_fade = EF_NO_FADE;
goto Here;
}
#endif
switch(w.type)
{
case WALL_DOOR:
if ((w.keys == wall_key::blue && (color = am.wall_door_blue, true)) ||
(w.keys == wall_key::gold && (color = am.wall_door_gold, true)) ||
(w.keys == wall_key::red && (color = am.wall_door_red, true)))
{
no_fade = EF_NO_FADE;
} else if (!(WallAnims[w.clip_num].flags & WCF_HIDDEN)) {
const auto connected_seg = seg->shared_segment::children[sn];
if (connected_seg != segment_none) {
const shared_segment &vcseg = *vcsegptr(connected_seg);
const auto &connected_side = find_connect_side(seg, vcseg);
if (connected_side == side_none)
break;
auto &wall = *vcwallptr(vcseg.sides[connected_side].wall_num);
switch (wall.keys)
{
case wall_key::blue:
color = am.wall_door_blue;
no_fade = EF_NO_FADE;
break;
case wall_key::gold:
color = am.wall_door_gold;
no_fade = EF_NO_FADE;
break;
case wall_key::red:
color = am.wall_door_red;
no_fade = EF_NO_FADE;
break;
default:
color = am.wall_door_color;
break;
}
}
} else {
color = am.wall_normal_color;
hidden_flag = EF_SECRET;
}
break;
case WALL_CLOSED:
// Make grates draw properly
// NOTE: In original D1, is_grate is 1, hidden_flag not used so grates never fade. I (zico) like this so I leave this alone for now.
if (!(is_grate = WALL_IS_DOORWAY(GameBitmaps, Textures, vcwallptr, seg, sn) & WALL_IS_DOORWAY_FLAG::rendpast))
hidden_flag = EF_SECRET;
color = am.wall_normal_color;
break;
case WALL_BLASTABLE:
// Hostage doors
color = am.wall_door_color;
break;
}
}
if (seg==Player_init[Player_num].segnum)
color = BM_XRGB(31,0,31);
if ( color != 255 ) {
#if defined(DXX_BUILD_DESCENT_II)
// If they have a map powerup, draw unvisited areas in dark blue.
// NOTE: D1 originally had this part of code but w/o cheat-check. It's only supposed to draw blue with powerup that does not exist in D1. So make this D2-only
#ifndef NDEBUG
if (!Automap_debug_show_all_segments)
#endif
{
auto &player_info = get_local_plrobj().ctype.player_info;
if ((cheats.fullautomap || player_info.powerup_flags & PLAYER_FLAGS_MAP_ALL) && !LevelUniqueAutomapState.Automap_visited[seg])
color = am.wall_revealed_color;
}
Here:
#endif
const auto vertex_list = get_side_verts(seg,sn);
const uint8_t flags = hidden_flag | no_fade;
add_one_edge(am, vertex_list[0], vertex_list[1], color, sn, seg, flags);
add_one_edge(am, vertex_list[1], vertex_list[2], color, sn, seg, flags);
add_one_edge(am, vertex_list[2], vertex_list[3], color, sn, seg, flags);
add_one_edge(am, vertex_list[3], vertex_list[0], color, sn, seg, flags);
if ( is_grate ) {
const uint8_t grate_flags = flags | EF_GRATE;
add_one_edge(am, vertex_list[0], vertex_list[2], color, sn, seg, grate_flags);
add_one_edge(am, vertex_list[1], vertex_list[3], color, sn, seg, grate_flags);
}
}
}
}
// Adds all the edges from a segment we haven't visited yet.
static void add_unknown_segment_edges(automap &am, const shared_segment &seg)
{
for (const auto &&[sn, child] : enumerate(seg.children))
{
// Only add edges that have no children
if (child == segment_none)
{
const auto vertex_list = get_side_verts(seg, static_cast<sidenum_t>(sn));
add_one_unknown_edge( am, vertex_list[0], vertex_list[1] );
add_one_unknown_edge( am, vertex_list[1], vertex_list[2] );
add_one_unknown_edge( am, vertex_list[2], vertex_list[3] );
add_one_unknown_edge( am, vertex_list[3], vertex_list[0] );
}
}
}
void automap_build_edge_list(automap &am, int add_all_edges)
{
// clear edge list
for (auto &i : unchecked_partial_range(am.edges.get(), am.max_edges))
{
i.num_faces = 0;
i.flags = 0;
}
am.num_edges = 0;
am.end_valid_edges = 0;
auto &Walls = LevelUniqueWallSubsystemState.Walls;
auto &vcwallptr = Walls.vcptr;
if (add_all_edges) {
// Cheating, add all edges as visited
range_for (const auto &&segp, vcsegptridx)
{
#if DXX_USE_EDITOR
if (segp->shared_segment::segnum != segment_none)
#endif
{
add_segment_edges(vcsegptr, vcwallptr, am, segp);
}
}
} else {
// Not cheating, add visited edges, and then unvisited edges
range_for (const auto &&segp, vcsegptridx)
{
#if DXX_USE_EDITOR
if (segp->shared_segment::segnum != segment_none)
#endif
if (LevelUniqueAutomapState.Automap_visited[segp])
{
add_segment_edges(vcsegptr, vcwallptr, am, segp);
}
}
range_for (const auto &&segp, vcsegptridx)
{
#if DXX_USE_EDITOR
if (segp->shared_segment::segnum != segment_none)
#endif
if (!LevelUniqueAutomapState.Automap_visited[segp])
{
add_unknown_segment_edges(am, segp);
}
}
}
// Find unnecessary lines (These are lines that don't have to be drawn because they have small curvature)
for (auto &i : unchecked_partial_range(am.edges.get(), am.end_valid_edges))
{
const auto e = &i;
if (!(e->flags&EF_USED)) continue;
const auto num_faces = e->num_faces;
if (num_faces < 2)
continue;
for (unsigned e1 = 0; e1 < num_faces; ++e1)
{
const auto e1segnum = e->segnum[e1];
const auto &e1siden0 = vcsegptr(e1segnum)->shared_segment::sides[e->sides[e1]].normals[0];
for (unsigned e2 = 1; e2 < num_faces; ++e2)
{
if (e1 == e2)
continue;
const auto e2segnum = e->segnum[e2];
if (e1segnum == e2segnum)
continue;
if (vm_vec_dot(e1siden0, vcsegptr(e2segnum)->shared_segment::sides[e->sides[e2]].normals[0]) > (F1_0 - (F1_0 / 10)))
{
e->flags &= (~EF_DEFINING);
break;
}
}
if (!(e->flags & EF_DEFINING))
break;
}
}
}
}
#if defined(DXX_BUILD_DESCENT_II)
static unsigned Marker_index;
void InitMarkerInput ()
{
//find free marker slot
const auto game_mode = Game_mode;
const auto max_numplayers = Netgame.max_numplayers;
const auto maxdrop = MarkerState.get_markers_per_player(game_mode, max_numplayers);
const auto &&game_marker_range = get_game_marker_range(game_mode, max_numplayers, Player_num, maxdrop);
const auto &&player_marker_range = get_player_marker_range(maxdrop);
const auto &&zipped_marker_range = zip(player_marker_range, unchecked_partial_range(&MarkerState.imobjidx[*game_marker_range.begin()], maxdrop));
const auto &&mb = zipped_marker_range.begin();
const auto &&me = zipped_marker_range.end();
auto iter = mb;
for (;;)
{
auto &&[pmi, objidx] = *iter;
if (objidx == object_none) //found free slot!
{
MarkerState.MarkerBeingDefined = pmi;
break;
}
if (++ iter == me) //no free slot
{
if (game_mode & GM_MULTI)
{
//in multi, replace oldest
MarkerState.MarkerBeingDefined = static_cast<player_marker_index>((static_cast<unsigned>(MarkerState.LastMarkerDropped) + 1) & (maxdrop - 1));
break;
}
else
{
HUD_init_message_literal(HM_DEFAULT, "No free marker slots");
return;
}
}
}
//got a free slot. start inputting marker message
Marker_input[0]=0;
Marker_index=0;
key_toggle_repeat(1);
}
window_event_result MarkerInputMessage(int key, control_info &Controls)
{
auto &Objects = LevelUniqueObjectState.Objects;
auto &vmobjptr = Objects.vmptr;
auto &vmobjptridx = Objects.vmptridx;
switch( key )
{
case KEY_LEFT:
case KEY_BACKSP:
case KEY_PAD4:
if (Marker_index > 0)
Marker_index--;
Marker_input[Marker_index] = 0;
break;
case KEY_ENTER:
{
const auto player_marker_num = MarkerState.MarkerBeingDefined;
MarkerState.LastMarkerDropped = player_marker_num;
const auto game_marker_num = convert_player_marker_index_to_game_marker_index(Game_mode, Netgame.max_numplayers, Player_num, player_marker_num);
MarkerState.message[game_marker_num] = Marker_input;
DropMarker(vmobjptridx, vmsegptridx, get_local_plrobj(), game_marker_num, player_marker_num);
}
[[fallthrough]];
case KEY_F8:
case KEY_ESC:
MarkerState.MarkerBeingDefined = player_marker_index::None;
key_toggle_repeat(0);
game_flush_inputs(Controls);
break;
default:
{
int ascii = key_ascii();
if ((ascii < 255 ))
if (Marker_index < Marker_input.size() - 1)
{
Marker_input[Marker_index++] = ascii;
Marker_input[Marker_index] = 0;
}
return window_event_result::ignored;
}
}
return window_event_result::handled;
}
#endif
}