mirror of https://github.com/ioquake/ioq3.git
Update UI player animation handling to match CGame
Fix "Error parsing animation file" messages in UI. Caused by fixing the handling of missing tokens in animation.cfg parser in a past commit. Fix new Team Arena torso animation frame numbers in UI. Add support for fixedtorso and fixedlegs keywords. Add support for reversed animations (negative numframes).
This commit is contained in:
parent
098d97bdb0
commit
11b3bca555
|
@ -486,6 +486,9 @@ typedef struct {
|
||||||
|
|
||||||
animation_t animations[MAX_ANIMATIONS];
|
animation_t animations[MAX_ANIMATIONS];
|
||||||
|
|
||||||
|
qboolean fixedlegs; // true if legs yaw is always the same as torso yaw
|
||||||
|
qboolean fixedtorso; // true if torso never changes yaw
|
||||||
|
|
||||||
qhandle_t weaponModel;
|
qhandle_t weaponModel;
|
||||||
qhandle_t barrelModel;
|
qhandle_t barrelModel;
|
||||||
qhandle_t flashModel;
|
qhandle_t flashModel;
|
||||||
|
|
|
@ -363,7 +363,7 @@ UI_RunLerpFrame
|
||||||
===============
|
===============
|
||||||
*/
|
*/
|
||||||
static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation ) {
|
static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation ) {
|
||||||
int f;
|
int f, numFrames;
|
||||||
animation_t *anim;
|
animation_t *anim;
|
||||||
|
|
||||||
// see if the animation sequence is switching
|
// see if the animation sequence is switching
|
||||||
|
@ -379,25 +379,41 @@ static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation
|
||||||
|
|
||||||
// get the next frame based on the animation
|
// get the next frame based on the animation
|
||||||
anim = lf->animation;
|
anim = lf->animation;
|
||||||
|
if ( !anim->frameLerp ) {
|
||||||
|
return; // shouldn't happen
|
||||||
|
}
|
||||||
if ( dp_realtime < lf->animationTime ) {
|
if ( dp_realtime < lf->animationTime ) {
|
||||||
lf->frameTime = lf->animationTime; // initial lerp
|
lf->frameTime = lf->animationTime; // initial lerp
|
||||||
} else {
|
} else {
|
||||||
lf->frameTime = lf->oldFrameTime + anim->frameLerp;
|
lf->frameTime = lf->oldFrameTime + anim->frameLerp;
|
||||||
}
|
}
|
||||||
f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp;
|
f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp;
|
||||||
if ( f >= anim->numFrames ) {
|
|
||||||
f -= anim->numFrames;
|
numFrames = anim->numFrames;
|
||||||
|
if (anim->flipflop) {
|
||||||
|
numFrames *= 2;
|
||||||
|
}
|
||||||
|
if ( f >= numFrames ) {
|
||||||
|
f -= numFrames;
|
||||||
if ( anim->loopFrames ) {
|
if ( anim->loopFrames ) {
|
||||||
f %= anim->loopFrames;
|
f %= anim->loopFrames;
|
||||||
f += anim->numFrames - anim->loopFrames;
|
f += anim->numFrames - anim->loopFrames;
|
||||||
} else {
|
} else {
|
||||||
f = anim->numFrames - 1;
|
f = numFrames - 1;
|
||||||
// the animation is stuck at the end, so it
|
// the animation is stuck at the end, so it
|
||||||
// can immediately transition to another sequence
|
// can immediately transition to another sequence
|
||||||
lf->frameTime = dp_realtime;
|
lf->frameTime = dp_realtime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( anim->reversed ) {
|
||||||
|
lf->frame = anim->firstFrame + anim->numFrames - 1 - f;
|
||||||
|
}
|
||||||
|
else if (anim->flipflop && f>=anim->numFrames) {
|
||||||
|
lf->frame = anim->firstFrame + anim->numFrames - 1 - (f%anim->numFrames);
|
||||||
|
}
|
||||||
|
else {
|
||||||
lf->frame = anim->firstFrame + f;
|
lf->frame = anim->firstFrame + f;
|
||||||
|
}
|
||||||
if ( dp_realtime > lf->frameTime ) {
|
if ( dp_realtime > lf->frameTime ) {
|
||||||
lf->frameTime = dp_realtime;
|
lf->frameTime = dp_realtime;
|
||||||
}
|
}
|
||||||
|
@ -615,6 +631,16 @@ static void UI_PlayerAngles( playerInfo_t *pi, vec3_t legs[3], vec3_t torso[3],
|
||||||
UI_SwingAngles( dest, 15, 30, 0.1f, &pi->torso.pitchAngle, &pi->torso.pitching );
|
UI_SwingAngles( dest, 15, 30, 0.1f, &pi->torso.pitchAngle, &pi->torso.pitching );
|
||||||
torsoAngles[PITCH] = pi->torso.pitchAngle;
|
torsoAngles[PITCH] = pi->torso.pitchAngle;
|
||||||
|
|
||||||
|
if ( pi->fixedtorso ) {
|
||||||
|
torsoAngles[PITCH] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( pi->fixedlegs ) {
|
||||||
|
legsAngles[YAW] = torsoAngles[YAW];
|
||||||
|
legsAngles[PITCH] = 0.0f;
|
||||||
|
legsAngles[ROLL] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// pull the angles back out of the hierarchial chain
|
// pull the angles back out of the hierarchial chain
|
||||||
AnglesSubtract( headAngles, torsoAngles, headAngles );
|
AnglesSubtract( headAngles, torsoAngles, headAngles );
|
||||||
AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
|
AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
|
||||||
|
@ -930,7 +956,7 @@ static qboolean UI_RegisterClientSkin( playerInfo_t *pi, const char *modelName,
|
||||||
UI_ParseAnimationFile
|
UI_ParseAnimationFile
|
||||||
======================
|
======================
|
||||||
*/
|
*/
|
||||||
static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animations ) {
|
static qboolean UI_ParseAnimationFile( const char *filename, playerInfo_t *pi ) {
|
||||||
char *text_p, *prev;
|
char *text_p, *prev;
|
||||||
int len;
|
int len;
|
||||||
int i;
|
int i;
|
||||||
|
@ -939,9 +965,15 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
int skip;
|
int skip;
|
||||||
char text[20000];
|
char text[20000];
|
||||||
fileHandle_t f;
|
fileHandle_t f;
|
||||||
|
animation_t *animations;
|
||||||
|
|
||||||
|
animations = pi->animations;
|
||||||
|
|
||||||
memset( animations, 0, sizeof( animation_t ) * MAX_ANIMATIONS );
|
memset( animations, 0, sizeof( animation_t ) * MAX_ANIMATIONS );
|
||||||
|
|
||||||
|
pi->fixedlegs = qfalse;
|
||||||
|
pi->fixedtorso = qfalse;
|
||||||
|
|
||||||
// load the file
|
// load the file
|
||||||
len = trap_FS_FOpenFile( filename, &f, FS_READ );
|
len = trap_FS_FOpenFile( filename, &f, FS_READ );
|
||||||
if ( len <= 0 ) {
|
if ( len <= 0 ) {
|
||||||
|
@ -987,6 +1019,12 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
} else if ( !Q_stricmp( token, "fixedlegs" ) ) {
|
||||||
|
pi->fixedlegs = qtrue;
|
||||||
|
continue;
|
||||||
|
} else if ( !Q_stricmp( token, "fixedtorso" ) ) {
|
||||||
|
pi->fixedtorso = qtrue;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it is a number, start parsing animations
|
// if it is a number, start parsing animations
|
||||||
|
@ -1003,6 +1041,16 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
|
|
||||||
token = COM_Parse( &text_p );
|
token = COM_Parse( &text_p );
|
||||||
if ( !token[0] ) {
|
if ( !token[0] ) {
|
||||||
|
if( i >= TORSO_GETFLAG && i <= TORSO_NEGATIVE ) {
|
||||||
|
animations[i].firstFrame = animations[TORSO_GESTURE].firstFrame;
|
||||||
|
animations[i].frameLerp = animations[TORSO_GESTURE].frameLerp;
|
||||||
|
animations[i].initialLerp = animations[TORSO_GESTURE].initialLerp;
|
||||||
|
animations[i].loopFrames = animations[TORSO_GESTURE].loopFrames;
|
||||||
|
animations[i].numFrames = animations[TORSO_GESTURE].numFrames;
|
||||||
|
animations[i].reversed = qfalse;
|
||||||
|
animations[i].flipflop = qfalse;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
animations[i].firstFrame = atoi( token );
|
animations[i].firstFrame = atoi( token );
|
||||||
|
@ -1010,7 +1058,7 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
if ( i == LEGS_WALKCR ) {
|
if ( i == LEGS_WALKCR ) {
|
||||||
skip = animations[LEGS_WALKCR].firstFrame - animations[TORSO_GESTURE].firstFrame;
|
skip = animations[LEGS_WALKCR].firstFrame - animations[TORSO_GESTURE].firstFrame;
|
||||||
}
|
}
|
||||||
if ( i >= LEGS_WALKCR ) {
|
if ( i >= LEGS_WALKCR && i<TORSO_GETFLAG) {
|
||||||
animations[i].firstFrame -= skip;
|
animations[i].firstFrame -= skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,6 +1068,14 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
}
|
}
|
||||||
animations[i].numFrames = atoi( token );
|
animations[i].numFrames = atoi( token );
|
||||||
|
|
||||||
|
animations[i].reversed = qfalse;
|
||||||
|
animations[i].flipflop = qfalse;
|
||||||
|
// if numFrames is negative the animation is reversed
|
||||||
|
if (animations[i].numFrames < 0) {
|
||||||
|
animations[i].numFrames = -animations[i].numFrames;
|
||||||
|
animations[i].reversed = qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
token = COM_Parse( &text_p );
|
token = COM_Parse( &text_p );
|
||||||
if ( !token[0] ) {
|
if ( !token[0] ) {
|
||||||
break;
|
break;
|
||||||
|
@ -1110,7 +1166,7 @@ qboolean UI_RegisterClientModelname( playerInfo_t *pi, const char *modelSkinName
|
||||||
|
|
||||||
// load the animations
|
// load the animations
|
||||||
Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName );
|
Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName );
|
||||||
if ( !UI_ParseAnimationFile( filename, pi->animations ) ) {
|
if ( !UI_ParseAnimationFile( filename, pi ) ) {
|
||||||
Com_Printf( "Failed to load animation file %s\n", filename );
|
Com_Printf( "Failed to load animation file %s\n", filename );
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
|
@ -533,6 +533,9 @@ typedef struct {
|
||||||
|
|
||||||
animation_t animations[MAX_TOTALANIMATIONS];
|
animation_t animations[MAX_TOTALANIMATIONS];
|
||||||
|
|
||||||
|
qboolean fixedlegs; // true if legs yaw is always the same as torso yaw
|
||||||
|
qboolean fixedtorso; // true if torso never changes yaw
|
||||||
|
|
||||||
qhandle_t weaponModel;
|
qhandle_t weaponModel;
|
||||||
qhandle_t barrelModel;
|
qhandle_t barrelModel;
|
||||||
qhandle_t flashModel;
|
qhandle_t flashModel;
|
||||||
|
|
|
@ -364,7 +364,7 @@ UI_RunLerpFrame
|
||||||
===============
|
===============
|
||||||
*/
|
*/
|
||||||
static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation ) {
|
static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation ) {
|
||||||
int f;
|
int f, numFrames;
|
||||||
animation_t *anim;
|
animation_t *anim;
|
||||||
|
|
||||||
// see if the animation sequence is switching
|
// see if the animation sequence is switching
|
||||||
|
@ -380,25 +380,41 @@ static void UI_RunLerpFrame( playerInfo_t *ci, lerpFrame_t *lf, int newAnimation
|
||||||
|
|
||||||
// get the next frame based on the animation
|
// get the next frame based on the animation
|
||||||
anim = lf->animation;
|
anim = lf->animation;
|
||||||
|
if ( !anim->frameLerp ) {
|
||||||
|
return; // shouldn't happen
|
||||||
|
}
|
||||||
if ( dp_realtime < lf->animationTime ) {
|
if ( dp_realtime < lf->animationTime ) {
|
||||||
lf->frameTime = lf->animationTime; // initial lerp
|
lf->frameTime = lf->animationTime; // initial lerp
|
||||||
} else {
|
} else {
|
||||||
lf->frameTime = lf->oldFrameTime + anim->frameLerp;
|
lf->frameTime = lf->oldFrameTime + anim->frameLerp;
|
||||||
}
|
}
|
||||||
f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp;
|
f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp;
|
||||||
if ( f >= anim->numFrames ) {
|
|
||||||
f -= anim->numFrames;
|
numFrames = anim->numFrames;
|
||||||
|
if (anim->flipflop) {
|
||||||
|
numFrames *= 2;
|
||||||
|
}
|
||||||
|
if ( f >= numFrames ) {
|
||||||
|
f -= numFrames;
|
||||||
if ( anim->loopFrames ) {
|
if ( anim->loopFrames ) {
|
||||||
f %= anim->loopFrames;
|
f %= anim->loopFrames;
|
||||||
f += anim->numFrames - anim->loopFrames;
|
f += anim->numFrames - anim->loopFrames;
|
||||||
} else {
|
} else {
|
||||||
f = anim->numFrames - 1;
|
f = numFrames - 1;
|
||||||
// the animation is stuck at the end, so it
|
// the animation is stuck at the end, so it
|
||||||
// can immediately transition to another sequence
|
// can immediately transition to another sequence
|
||||||
lf->frameTime = dp_realtime;
|
lf->frameTime = dp_realtime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( anim->reversed ) {
|
||||||
|
lf->frame = anim->firstFrame + anim->numFrames - 1 - f;
|
||||||
|
}
|
||||||
|
else if (anim->flipflop && f>=anim->numFrames) {
|
||||||
|
lf->frame = anim->firstFrame + anim->numFrames - 1 - (f%anim->numFrames);
|
||||||
|
}
|
||||||
|
else {
|
||||||
lf->frame = anim->firstFrame + f;
|
lf->frame = anim->firstFrame + f;
|
||||||
|
}
|
||||||
if ( dp_realtime > lf->frameTime ) {
|
if ( dp_realtime > lf->frameTime ) {
|
||||||
lf->frameTime = dp_realtime;
|
lf->frameTime = dp_realtime;
|
||||||
}
|
}
|
||||||
|
@ -616,6 +632,16 @@ static void UI_PlayerAngles( playerInfo_t *pi, vec3_t legs[3], vec3_t torso[3],
|
||||||
UI_SwingAngles( dest, 15, 30, 0.1f, &pi->torso.pitchAngle, &pi->torso.pitching );
|
UI_SwingAngles( dest, 15, 30, 0.1f, &pi->torso.pitchAngle, &pi->torso.pitching );
|
||||||
torsoAngles[PITCH] = pi->torso.pitchAngle;
|
torsoAngles[PITCH] = pi->torso.pitchAngle;
|
||||||
|
|
||||||
|
if ( pi->fixedtorso ) {
|
||||||
|
torsoAngles[PITCH] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( pi->fixedlegs ) {
|
||||||
|
legsAngles[YAW] = torsoAngles[YAW];
|
||||||
|
legsAngles[PITCH] = 0.0f;
|
||||||
|
legsAngles[ROLL] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// pull the angles back out of the hierarchial chain
|
// pull the angles back out of the hierarchial chain
|
||||||
AnglesSubtract( headAngles, torsoAngles, headAngles );
|
AnglesSubtract( headAngles, torsoAngles, headAngles );
|
||||||
AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
|
AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
|
||||||
|
@ -1015,7 +1041,7 @@ static qboolean UI_RegisterClientSkin( playerInfo_t *pi, const char *modelName,
|
||||||
UI_ParseAnimationFile
|
UI_ParseAnimationFile
|
||||||
======================
|
======================
|
||||||
*/
|
*/
|
||||||
static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animations ) {
|
static qboolean UI_ParseAnimationFile( const char *filename, playerInfo_t *pi ) {
|
||||||
char *text_p, *prev;
|
char *text_p, *prev;
|
||||||
int len;
|
int len;
|
||||||
int i;
|
int i;
|
||||||
|
@ -1024,9 +1050,15 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
int skip;
|
int skip;
|
||||||
char text[20000];
|
char text[20000];
|
||||||
fileHandle_t f;
|
fileHandle_t f;
|
||||||
|
animation_t *animations;
|
||||||
|
|
||||||
|
animations = pi->animations;
|
||||||
|
|
||||||
memset( animations, 0, sizeof( animation_t ) * MAX_ANIMATIONS );
|
memset( animations, 0, sizeof( animation_t ) * MAX_ANIMATIONS );
|
||||||
|
|
||||||
|
pi->fixedlegs = qfalse;
|
||||||
|
pi->fixedtorso = qfalse;
|
||||||
|
|
||||||
// load the file
|
// load the file
|
||||||
len = trap_FS_FOpenFile( filename, &f, FS_READ );
|
len = trap_FS_FOpenFile( filename, &f, FS_READ );
|
||||||
if ( len <= 0 ) {
|
if ( len <= 0 ) {
|
||||||
|
@ -1074,6 +1106,12 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
} else if ( !Q_stricmp( token, "fixedlegs" ) ) {
|
||||||
|
pi->fixedlegs = qtrue;
|
||||||
|
continue;
|
||||||
|
} else if ( !Q_stricmp( token, "fixedtorso" ) ) {
|
||||||
|
pi->fixedtorso = qtrue;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it is a number, start parsing animations
|
// if it is a number, start parsing animations
|
||||||
|
@ -1090,6 +1128,16 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
|
|
||||||
token = COM_Parse( &text_p );
|
token = COM_Parse( &text_p );
|
||||||
if ( !token[0] ) {
|
if ( !token[0] ) {
|
||||||
|
if( i >= TORSO_GETFLAG && i <= TORSO_NEGATIVE ) {
|
||||||
|
animations[i].firstFrame = animations[TORSO_GESTURE].firstFrame;
|
||||||
|
animations[i].frameLerp = animations[TORSO_GESTURE].frameLerp;
|
||||||
|
animations[i].initialLerp = animations[TORSO_GESTURE].initialLerp;
|
||||||
|
animations[i].loopFrames = animations[TORSO_GESTURE].loopFrames;
|
||||||
|
animations[i].numFrames = animations[TORSO_GESTURE].numFrames;
|
||||||
|
animations[i].reversed = qfalse;
|
||||||
|
animations[i].flipflop = qfalse;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
animations[i].firstFrame = atoi( token );
|
animations[i].firstFrame = atoi( token );
|
||||||
|
@ -1097,7 +1145,7 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
if ( i == LEGS_WALKCR ) {
|
if ( i == LEGS_WALKCR ) {
|
||||||
skip = animations[LEGS_WALKCR].firstFrame - animations[TORSO_GESTURE].firstFrame;
|
skip = animations[LEGS_WALKCR].firstFrame - animations[TORSO_GESTURE].firstFrame;
|
||||||
}
|
}
|
||||||
if ( i >= LEGS_WALKCR ) {
|
if ( i >= LEGS_WALKCR && i<TORSO_GETFLAG) {
|
||||||
animations[i].firstFrame -= skip;
|
animations[i].firstFrame -= skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1107,6 +1155,14 @@ static qboolean UI_ParseAnimationFile( const char *filename, animation_t *animat
|
||||||
}
|
}
|
||||||
animations[i].numFrames = atoi( token );
|
animations[i].numFrames = atoi( token );
|
||||||
|
|
||||||
|
animations[i].reversed = qfalse;
|
||||||
|
animations[i].flipflop = qfalse;
|
||||||
|
// if numFrames is negative the animation is reversed
|
||||||
|
if (animations[i].numFrames < 0) {
|
||||||
|
animations[i].numFrames = -animations[i].numFrames;
|
||||||
|
animations[i].reversed = qtrue;
|
||||||
|
}
|
||||||
|
|
||||||
token = COM_Parse( &text_p );
|
token = COM_Parse( &text_p );
|
||||||
if ( !token[0] ) {
|
if ( !token[0] ) {
|
||||||
break;
|
break;
|
||||||
|
@ -1225,9 +1281,9 @@ qboolean UI_RegisterClientModelname( playerInfo_t *pi, const char *modelSkinName
|
||||||
|
|
||||||
// load the animations
|
// load the animations
|
||||||
Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName );
|
Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName );
|
||||||
if ( !UI_ParseAnimationFile( filename, pi->animations ) ) {
|
if ( !UI_ParseAnimationFile( filename, pi ) ) {
|
||||||
Com_sprintf( filename, sizeof( filename ), "models/players/characters/%s/animation.cfg", modelName );
|
Com_sprintf( filename, sizeof( filename ), "models/players/characters/%s/animation.cfg", modelName );
|
||||||
if ( !UI_ParseAnimationFile( filename, pi->animations ) ) {
|
if ( !UI_ParseAnimationFile( filename, pi ) ) {
|
||||||
Com_Printf( "Failed to load animation file %s\n", filename );
|
Com_Printf( "Failed to load animation file %s\n", filename );
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue