2020-09-06 10:44:58 +00:00
//-------------------------------------------------------------------------
/*
Copyright ( C ) 1996 , 2003 - 3 D Realms Entertainment
Copyright ( C ) 2020 - Christoph Oelckers
This file is part of Duke Nukem 3 D version 1.5 - Atomic Edition
Duke Nukem 3 D is free software ; you can redistribute it and / or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation ; either version 2
of the License , or ( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE .
See the GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program ; if not , write to the Free Software
Foundation , Inc . , 59 Temple Place - Suite 330 , Boston , MA 02111 - 1307 , USA .
Original Source : 1996 - Todd Replogle
Prepared for public release : 03 / 21 / 2003 - Charlie Wiederhold , 3 D Realms
Modifications for JonoF ' s port by Jonathon Fowler ( jf @ jonof . id . au )
*/
//-------------------------------------------------------------------------
# include "automap.h"
# include "cstat.h"
# include "c_dispatch.h"
# include "c_cvars.h"
# include "gstrings.h"
# include "printf.h"
2020-09-06 11:39:57 +00:00
# include "serializer.h"
2020-09-06 18:49:43 +00:00
# include "v_2ddrawer.h"
# include "earcut.hpp"
# include "buildtiles.h"
# include "d_event.h"
# include "c_bind.h"
# include "gamestate.h"
# include "gamecontrol.h"
# include "quotemgr.h"
# include "v_video.h"
# include "gamestruct.h"
2020-10-13 19:38:24 +00:00
# include "v_draw.h"
2020-09-06 10:44:58 +00:00
2020-09-06 18:49:43 +00:00
CVAR ( Bool , am_followplayer , true , CVAR_ARCHIVE )
CVAR ( Bool , am_rotate , true , CVAR_ARCHIVE )
CVAR ( Bool , am_textfont , false , CVAR_ARCHIVE )
CVAR ( Bool , am_showlabel , false , CVAR_ARCHIVE )
CVAR ( Bool , am_nameontop , false , CVAR_ARCHIVE )
2020-09-06 10:44:58 +00:00
2020-09-06 18:49:43 +00:00
int automapMode ;
2020-09-06 21:12:47 +00:00
static float am_zoomdir ;
2020-09-06 18:49:43 +00:00
int follow_x = INT_MAX , follow_y = INT_MAX , follow_a = INT_MAX ;
static int gZoom = 768 ;
2020-09-06 10:44:58 +00:00
bool automapping ;
bool gFullMap ;
FixedBitArray < MAXSECTORS > show2dsector ;
FixedBitArray < MAXWALLS > show2dwall ;
FixedBitArray < MAXSPRITES > show2dsprite ;
2020-09-06 18:49:43 +00:00
static int x_min_bound = INT_MAX , y_min_bound , x_max_bound , y_max_bound ;
CVAR ( Color , am_twosidedcolor , 0xaaaaaa , CVAR_ARCHIVE )
CVAR ( Color , am_onesidedcolor , 0xaaaaaa , CVAR_ARCHIVE )
CVAR ( Color , am_playercolor , 0xaaaaaa , CVAR_ARCHIVE )
CVAR ( Color , am_ovtwosidedcolor , 0xaaaaaa , CVAR_ARCHIVE )
CVAR ( Color , am_ovonesidedcolor , 0xaaaaaa , CVAR_ARCHIVE )
CVAR ( Color , am_ovplayercolor , 0xaaaaaa , CVAR_ARCHIVE )
2020-09-06 10:44:58 +00:00
2020-09-06 11:39:57 +00:00
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
2020-09-06 18:49:43 +00:00
CCMD ( allmap )
{
if ( ! CheckCheatmode ( true , false ) )
{
gFullMap = ! gFullMap ;
Printf ( " %s \n " , GStrings ( gFullMap ? " SHOW MAP: ON " : " SHOW MAP: OFF " ) ) ;
}
}
CCMD ( togglemap )
{
if ( gamestate = = GS_LEVEL )
{
automapMode + + ;
if ( automapMode = = am_count ) automapMode = am_off ;
if ( ( g_gameType & GAMEFLAG_BLOOD ) & & automapMode = = am_overlay ) automapMode = am_full ; // todo: investigate if this can be re-enabled
}
}
CCMD ( togglefollow )
{
am_followplayer = ! am_followplayer ;
auto msg = quoteMgr . GetQuote ( am_followplayer ? 84 : 83 ) ;
if ( ! msg | | ! * msg ) msg = am_followplayer ? GStrings ( " FOLLOW MODE ON " ) : GStrings ( " FOLLOW MODE Off " ) ;
2020-09-06 21:12:47 +00:00
Printf ( PRINT_NOTIFY , " %s \n " , msg ) ;
if ( am_followplayer ) follow_x = INT_MAX ;
2020-09-06 18:49:43 +00:00
}
2020-09-06 21:12:47 +00:00
CCMD ( togglerotate )
{
am_rotate = ! am_rotate ;
2020-09-07 17:56:28 +00:00
auto msg = am_rotate ? GStrings ( " TXT_ROTATE_ON " ) : GStrings ( " TXT_ROTATE_OFF " ) ;
2020-09-06 21:12:47 +00:00
Printf ( PRINT_NOTIFY , " %s \n " , msg ) ;
}
2020-09-06 18:49:43 +00:00
CCMD ( am_zoom )
2020-09-06 11:39:57 +00:00
{
2020-09-06 18:49:43 +00:00
if ( argv . argc ( ) > = 2 )
{
am_zoomdir = ( float ) atof ( argv [ 1 ] ) ;
}
2020-09-06 11:39:57 +00:00
}
2020-09-06 18:49:43 +00:00
//==========================================================================
//
// AM_Responder
// Handle automap exclusive bindings.
//
//==========================================================================
bool AM_Responder ( event_t * ev , bool last )
{
if ( ev - > type = = EV_KeyDown | | ev - > type = = EV_KeyUp )
{
if ( am_followplayer )
{
// check for am_pan* and ignore in follow mode
const char * defbind = AutomapBindings . GetBind ( ev - > data1 ) ;
if ( defbind & & ! strnicmp ( defbind , " +am_pan " , 7 ) ) return false ;
}
bool res = C_DoKey ( ev , & AutomapBindings , nullptr ) ;
if ( res & & ev - > type = = EV_KeyUp & & ! last )
{
// If this is a release event we also need to check if it released a button in the main Bindings
// so that that button does not get stuck.
const char * defbind = Bindings . GetBind ( ev - > data1 ) ;
return ( ! defbind | | defbind [ 0 ] ! = ' + ' ) ; // Let G_Responder handle button releases
}
return res ;
}
return false ;
}
2020-09-06 11:39:57 +00:00
2020-09-06 10:44:58 +00:00
//---------------------------------------------------------------------------
//
2020-09-06 18:49:43 +00:00
//
2020-09-06 10:44:58 +00:00
//
2020-09-06 18:49:43 +00:00
//---------------------------------------------------------------------------
static void CalcMapBounds ( )
{
2020-09-06 21:12:47 +00:00
x_min_bound = INT_MAX ;
y_min_bound = INT_MAX ;
x_max_bound = INT_MIN ;
y_max_bound = INT_MIN ;
2020-09-06 18:49:43 +00:00
for ( int i = 0 ; i < numwalls ; i + + )
{
// get map min and max coordinates
2020-09-06 21:12:47 +00:00
if ( wall [ i ] . x < x_min_bound ) x_min_bound = wall [ i ] . x ;
if ( wall [ i ] . y < y_min_bound ) y_min_bound = wall [ i ] . y ;
if ( wall [ i ] . x > x_max_bound ) x_max_bound = wall [ i ] . x ;
if ( wall [ i ] . y > y_max_bound ) y_max_bound = wall [ i ] . y ;
2020-09-06 18:49:43 +00:00
}
}
//---------------------------------------------------------------------------
//
//
2020-09-06 10:44:58 +00:00
//
//---------------------------------------------------------------------------
2020-09-06 18:49:43 +00:00
void AutomapControl ( )
2020-09-06 10:44:58 +00:00
{
2020-09-06 18:49:43 +00:00
static int nonsharedtimer ;
int ms = screen - > FrameTime ;
int interval ;
2020-09-06 21:12:47 +00:00
int panvert = 0 , panhorz = 0 ;
2020-09-06 18:49:43 +00:00
if ( nonsharedtimer > 0 | | ms < nonsharedtimer )
2020-09-06 10:44:58 +00:00
{
2020-09-06 18:49:43 +00:00
interval = ms - nonsharedtimer ;
}
else
{
interval = 0 ;
}
nonsharedtimer = screen - > FrameTime ;
if ( System_WantGuiCapture ( ) )
return ;
if ( automapMode ! = am_off )
{
2020-09-06 21:12:47 +00:00
const int keymove = 4 ;
2020-09-06 18:49:43 +00:00
if ( am_zoomdir > 0 )
{
gZoom = xs_CRoundToInt ( gZoom * am_zoomdir ) ;
}
else if ( am_zoomdir < 0 )
{
gZoom = xs_CRoundToInt ( gZoom / - am_zoomdir ) ;
}
am_zoomdir = 0 ;
2020-09-07 18:39:07 +00:00
double j = interval * 35. / gZoom ;
2020-09-06 18:49:43 +00:00
if ( buttonMap . ButtonDown ( gamefunc_Enlarge_Screen ) )
gZoom + = ( int ) fmulscale6 ( j , max ( gZoom , 256 ) ) ;
if ( buttonMap . ButtonDown ( gamefunc_Shrink_Screen ) )
gZoom - = ( int ) fmulscale6 ( j , max ( gZoom , 256 ) ) ;
2020-09-06 21:12:47 +00:00
gZoom = clamp ( gZoom , 48 , 2048 ) ;
if ( ! am_followplayer )
{
if ( buttonMap . ButtonDown ( gamefunc_AM_PanLeft ) )
panhorz + = keymove ;
2020-09-06 18:49:43 +00:00
2020-09-06 21:12:47 +00:00
if ( buttonMap . ButtonDown ( gamefunc_AM_PanRight ) )
panhorz - = keymove ;
2020-09-06 18:49:43 +00:00
2020-09-06 21:12:47 +00:00
if ( buttonMap . ButtonDown ( gamefunc_AM_PanUp ) )
panvert + = keymove ;
2020-09-06 18:49:43 +00:00
2020-09-06 21:12:47 +00:00
if ( buttonMap . ButtonDown ( gamefunc_AM_PanDown ) )
panvert - = keymove ;
2020-09-06 18:49:43 +00:00
2020-11-14 09:05:42 +00:00
int momx = mulscale9 ( panvert , bcos ( follow_a ) ) ;
int momy = mulscale9 ( panvert , bsin ( follow_a ) ) ;
2020-09-06 18:49:43 +00:00
2020-11-14 09:05:42 +00:00
momx + = mulscale9 ( panhorz , bsin ( follow_a ) ) ;
momy + = mulscale9 ( panhorz , - bcos ( follow_a ) ) ;
2020-09-06 18:49:43 +00:00
2020-09-07 18:39:07 +00:00
follow_x + = int ( momx * j ) ;
follow_y + = int ( momy * j ) ;
2020-09-06 18:49:43 +00:00
2020-09-06 21:12:47 +00:00
if ( x_min_bound = = INT_MAX ) CalcMapBounds ( ) ;
follow_x = clamp ( follow_x , x_min_bound , x_max_bound ) ;
follow_y = clamp ( follow_y , y_min_bound , y_max_bound ) ;
}
2020-09-06 10:44:58 +00:00
}
}
2020-09-06 18:49:43 +00:00
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void SerializeAutomap ( FSerializer & arc )
{
if ( arc . BeginObject ( " automap " ) )
{
arc ( " automapping " , automapping )
( " fullmap " , gFullMap )
// Only store what's needed. Unfortunately for sprites it is not that easy
. SerializeMemory ( " mappedsectors " , show2dsector . Storage ( ) , ( numsectors + 7 ) / 8 )
. SerializeMemory ( " mappedwalls " , show2dwall . Storage ( ) , ( numwalls + 7 ) / 8 )
. SerializeMemory ( " mappedsprites " , show2dsprite . Storage ( ) , MAXSPRITES / 8 )
. EndObject ( ) ;
}
}
2020-09-06 10:44:58 +00:00
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void ClearAutomap ( )
{
2020-09-06 18:49:43 +00:00
show2dsector . Zero ( ) ;
show2dwall . Zero ( ) ;
show2dsprite . Zero ( ) ;
x_min_bound = INT_MAX ;
2020-09-06 10:44:58 +00:00
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void MarkSectorSeen ( int i )
{
if ( i > = 0 )
{
show2dsector . Set ( i ) ;
auto wal = & wall [ sector [ i ] . wallptr ] ;
for ( int j = sector [ i ] . wallnum ; j > 0 ; j - - , wal + + )
{
i = wal - > nextsector ;
if ( i < 0 ) continue ;
if ( wal - > cstat & 0x0071 ) continue ;
if ( wall [ wal - > nextwall ] . cstat & 0x0071 ) continue ;
if ( sector [ i ] . lotag = = 32767 ) continue ;
if ( sector [ i ] . ceilingz > = sector [ i ] . floorz ) continue ;
show2dsector . Set ( i ) ;
}
}
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
2020-09-06 18:49:43 +00:00
void drawlinergb ( int32_t x1 , int32_t y1 , int32_t x2 , int32_t y2 , PalEntry p )
{
twod - > AddLine ( x1 / 4096.f , y1 / 4096.f , x2 / 4096.f , y2 / 4096.f , windowxy1 . x , windowxy1 . y , windowxy2 . x , windowxy2 . y , p ) ;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
PalEntry RedLineColor ( )
{
// todo:
// Blood uses palette index 12 (99,99,99)
// Exhumed uses palette index 111 (roughly 170,170,170) but darkens the line in overlay mode the farther it is away from the player in vertical direction.
// Shadow Warrior uses palette index 152 in overlay mode and index 12 in full map mode. (152: 84, 88, 40)
return automapMode = = am_overlay ? * am_ovtwosidedcolor : * am_twosidedcolor ;
}
PalEntry WhiteLineColor ( )
{
// todo:
// Blood uses palette index 24
// Exhumed uses palette index 111 (roughly 170,170,170) but darkens the line in overlay mode the farther it is away from the player in vertical direction.
// Shadow Warrior uses palette index 24 (60,60,60)
return automapMode = = am_overlay ? * am_ovonesidedcolor : * am_onesidedcolor ;
}
PalEntry PlayerLineColor ( )
{
return automapMode = = am_overlay ? * am_ovplayercolor : * am_playercolor ;
}
CCMD ( printpalcol )
{
if ( argv . argc ( ) < 2 ) return ;
int i = atoi ( argv [ 1 ] ) ;
Printf ( " %d, %d, %d \n " , GPalette . BaseColors [ i ] . r , GPalette . BaseColors [ i ] . g , GPalette . BaseColors [ i ] . b ) ;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
bool ShowRedLine ( int j , int i )
{
auto wal = & wall [ j ] ;
if ( ! ( g_gameType & GAMEFLAG_SW ) )
{
return ! gFullMap & & ! show2dsector [ wal - > nextsector ] ;
}
else
{
if ( ! gFullMap )
{
if ( ! show2dwall [ j ] ) return false ;
int k = wal - > nextwall ;
if ( k > j & & ! show2dwall [ k ] ) return false ; //???
}
if ( automapMode = = am_full )
{
if ( sector [ i ] . floorz ! = sector [ i ] . ceilingz )
if ( sector [ wal - > nextsector ] . floorz ! = sector [ wal - > nextsector ] . ceilingz )
if ( ( ( wal - > cstat | wall [ wal - > nextwall ] . cstat ) & ( 16 + 32 ) ) = = 0 )
if ( sector [ i ] . floorz = = sector [ wal - > nextsector ] . floorz )
return false ;
if ( sector [ i ] . floorpicnum ! = sector [ wal - > nextsector ] . floorpicnum )
return false ;
if ( sector [ i ] . floorshade ! = sector [ wal - > nextsector ] . floorshade )
return false ;
}
return true ;
}
}
//---------------------------------------------------------------------------
//
// two sided lines
//
//---------------------------------------------------------------------------
void drawredlines ( int cposx , int cposy , int czoom , int cang )
{
2020-11-14 09:05:42 +00:00
int xvect = - bsin ( cang ) * czoom ;
int yvect = - bcos ( cang ) * czoom ;
2020-09-06 18:49:43 +00:00
int xvect2 = mulscale16 ( xvect , yxaspect ) ;
int yvect2 = mulscale16 ( yvect , yxaspect ) ;
for ( int i = 0 ; i < numsectors ; i + + )
{
if ( ! gFullMap & & ! show2dsector [ i ] ) continue ;
int startwall = sector [ i ] . wallptr ;
int endwall = sector [ i ] . wallptr + sector [ i ] . wallnum ;
int z1 = sector [ i ] . ceilingz ;
int z2 = sector [ i ] . floorz ;
walltype * wal ;
int j ;
for ( j = startwall , wal = & wall [ startwall ] ; j < endwall ; j + + , wal + + )
{
int k = wal - > nextwall ;
if ( k < 0 | | k > = MAXWALLS ) continue ;
if ( sector [ wal - > nextsector ] . ceilingz = = z1 & & sector [ wal - > nextsector ] . floorz = = z2 )
if ( ( ( wal - > cstat | wall [ wal - > nextwall ] . cstat ) & ( 16 + 32 ) ) = = 0 ) continue ;
if ( ShowRedLine ( j , i ) )
{
int ox = wal - > x - cposx ;
int oy = wal - > y - cposy ;
int x1 = dmulscale16 ( ox , xvect , - oy , yvect ) + ( xdim < < 11 ) ;
int y1 = dmulscale16 ( oy , xvect2 , ox , yvect2 ) + ( ydim < < 11 ) ;
auto wal2 = & wall [ wal - > point2 ] ;
ox = wal2 - > x - cposx ;
oy = wal2 - > y - cposy ;
int x2 = dmulscale16 ( ox , xvect , - oy , yvect ) + ( xdim < < 11 ) ;
int y2 = dmulscale16 ( oy , xvect2 , ox , yvect2 ) + ( ydim < < 11 ) ;
drawlinergb ( x1 , y1 , x2 , y2 , RedLineColor ( ) ) ;
}
}
}
}
//---------------------------------------------------------------------------
//
// one sided lines
//
//---------------------------------------------------------------------------
static void drawwhitelines ( int cposx , int cposy , int czoom , int cang )
{
2020-11-14 09:05:42 +00:00
int xvect = - bsin ( cang ) * czoom ;
int yvect = - bcos ( cang ) * czoom ;
2020-09-06 18:49:43 +00:00
int xvect2 = mulscale16 ( xvect , yxaspect ) ;
int yvect2 = mulscale16 ( yvect , yxaspect ) ;
for ( int i = numsectors - 1 ; i > = 0 ; i - - )
{
if ( ! gFullMap & & ! show2dsector [ i ] & & ! ( g_gameType & GAMEFLAG_SW ) ) continue ;
int startwall = sector [ i ] . wallptr ;
int endwall = sector [ i ] . wallptr + sector [ i ] . wallnum ;
walltype * wal ;
int j ;
for ( j = startwall , wal = & wall [ startwall ] ; j < endwall ; j + + , wal + + )
{
if ( wal - > nextwall > = 0 ) continue ;
if ( ! tileGetTexture ( wal - > picnum ) - > isValid ( ) ) continue ;
if ( ( g_gameType & GAMEFLAG_SW ) & & ! gFullMap & & ! show2dwall [ j ] )
continue ;
int ox = wal - > x - cposx ;
int oy = wal - > y - cposy ;
int x1 = dmulscale16 ( ox , xvect , - oy , yvect ) + ( xdim < < 11 ) ;
int y1 = dmulscale16 ( oy , xvect2 , ox , yvect2 ) + ( ydim < < 11 ) ;
int k = wal - > point2 ;
auto wal2 = & wall [ k ] ;
ox = wal2 - > x - cposx ;
oy = wal2 - > y - cposy ;
int x2 = dmulscale16 ( ox , xvect , - oy , yvect ) + ( xdim < < 11 ) ;
int y2 = dmulscale16 ( oy , xvect2 , ox , yvect2 ) + ( ydim < < 11 ) ;
drawlinergb ( x1 , y1 , x2 , y2 , WhiteLineColor ( ) ) ;
}
}
}
void DrawPlayerArrow ( int cposx , int cposy , int cang , int pl_x , int pl_y , int zoom , int pl_angle )
{
int arrow [ ] =
{
0 , 65536 , 0 , - 65536 ,
0 , 65536 , - 32768 , 32878 ,
0 , 65536 , 32768 , 32878 ,
} ;
2020-11-14 09:05:42 +00:00
int xvect = - bsin ( cang ) * zoom ;
int yvect = - bcos ( cang ) * zoom ;
2020-09-06 18:49:43 +00:00
int xvect2 = mulscale16 ( xvect , yxaspect ) ;
int yvect2 = mulscale16 ( yvect , yxaspect ) ;
2020-11-14 09:05:42 +00:00
int pxvect = - bsin ( pl_angle ) ;
int pyvect = - bcos ( pl_angle ) ;
2020-09-06 18:49:43 +00:00
for ( int i = 0 ; i < 12 ; i + = 4 )
{
int px1 = dmulscale16 ( arrow [ i ] , pxvect , - arrow [ i + 1 ] , pyvect ) ;
int py1 = dmulscale16 ( arrow [ i + 1 ] , pxvect , arrow [ i ] , pyvect ) + ( ydim < < 11 ) ;
int px2 = dmulscale16 ( arrow [ i + 2 ] , pxvect , - arrow [ i + 3 ] , pyvect ) ;
int py2 = dmulscale16 ( arrow [ i + 3 ] , pxvect , arrow [ i + 2 ] , pyvect ) + ( ydim < < 11 ) ;
int ox1 = px1 - cposx ;
int oy1 = py1 - cposx ;
int ox2 = px2 - cposx ;
int oy2 = py2 - cposx ;
int sx1 = dmulscale16 ( ox1 , xvect , - oy1 , yvect ) + ( xdim < < 11 ) ;
int sy1 = dmulscale16 ( oy1 , xvect2 , ox1 , yvect2 ) + ( ydim < < 11 ) ;
int sx2 = dmulscale16 ( ox2 , xvect , - oy2 , yvect ) + ( xdim < < 11 ) ;
int sy2 = dmulscale16 ( oy2 , xvect2 , ox2 , yvect2 ) + ( ydim < < 11 ) ;
drawlinergb ( sx1 , sy1 , sx2 , sy2 , WhiteLineColor ( ) ) ;
}
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
2020-09-06 10:44:58 +00:00
void DrawOverheadMap ( int pl_x , int pl_y , int pl_angle )
{
2020-09-06 21:12:47 +00:00
if ( am_followplayer | | follow_x = = INT_MAX )
{
follow_x = pl_x ;
follow_y = pl_y ;
}
int x = follow_x ;
int y = follow_y ;
2020-09-06 18:49:43 +00:00
follow_a = am_rotate ? pl_angle : 0 ;
2020-09-06 21:12:47 +00:00
AutomapControl ( ) ;
2020-09-06 18:49:43 +00:00
if ( automapMode = = am_full )
{
twod - > ClearScreen ( ) ;
renderDrawMapView ( x , y , gZoom , follow_a ) ;
}
int32_t tmpydim = ( xdim * 5 ) / 8 ;
renderSetAspect ( 65536 , divscale16 ( tmpydim * 320 , xdim * 200 ) ) ;
drawredlines ( x , y , gZoom , follow_a ) ;
drawwhitelines ( x , y , gZoom , follow_a ) ;
2020-09-06 19:15:59 +00:00
if ( ! gi - > DrawAutomapPlayer ( x , y , gZoom , follow_a ) )
DrawPlayerArrow ( x , y , follow_a , pl_x , pl_y , gZoom , - pl_angle ) ;
2020-09-06 18:49:43 +00:00
2020-09-06 10:44:58 +00:00
}