mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2026-03-08 20:26:17 +00:00
Merge branch 'master' into pet-robo
This commit is contained in:
commit
372544d28f
10 changed files with 95 additions and 71 deletions
|
|
@ -1543,7 +1543,7 @@ static void CV_SetCVar(consvar_t *var, const char *value, boolean stealth)
|
|||
}
|
||||
|
||||
// Only add to netcmd buffer if in a netgame, otherwise, just change it.
|
||||
if (netgame || multiplayer)
|
||||
if (netgame)
|
||||
{
|
||||
WRITEUINT16(p, var->netid);
|
||||
WRITESTRING(p, value);
|
||||
|
|
|
|||
|
|
@ -1881,7 +1881,7 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
|
|||
{
|
||||
char version[8] = "";
|
||||
#if VERSION > 0 || SUBVERSION > 0
|
||||
snprintf(version, sizeof (version), "%d.%d.%d", VERSION/100, VERSION%100, SUBVERSION);
|
||||
snprintf(version, sizeof (version), "%d.%d", VERSION, SUBVERSION);
|
||||
#else
|
||||
strcpy(version, GetRevisionString());
|
||||
#endif
|
||||
|
|
@ -2356,8 +2356,8 @@ static void CL_ConnectToServer(boolean viams)
|
|||
gametypestr = Gametype_Names[num];
|
||||
if (gametypestr)
|
||||
CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
|
||||
CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
|
||||
serverlist[i].info.version%100, serverlist[i].info.subversion);
|
||||
CONS_Printf(M_GetText("Version: %d.%d\n"),
|
||||
serverlist[i].info.version, serverlist[i].info.subversion);
|
||||
}
|
||||
SL_ClearServerList(servernode);
|
||||
#endif
|
||||
|
|
@ -3297,8 +3297,8 @@ static void Got_AddBot(UINT8 **p, INT32 playernum);
|
|||
// called one time at init
|
||||
void D_ClientServerInit(void)
|
||||
{
|
||||
DEBFILE(va("- - -== SRB2Kart v%d.%.2d.%d "VERSIONSTRING" debugfile ==- - -\n",
|
||||
VERSION/100, VERSION%100, SUBVERSION));
|
||||
DEBFILE(va("- - -== SRB2Kart v%d.%d "VERSIONSTRING" debugfile ==- - -\n",
|
||||
VERSION, SUBVERSION));
|
||||
|
||||
#ifndef NONET
|
||||
COM_AddCommand("getplayernum", Command_GetPlayerNum);
|
||||
|
|
@ -3881,7 +3881,7 @@ static void HandleConnect(SINT8 node)
|
|||
SV_SendRefuse(node, M_GetText("You have been banned\nfrom the server"));
|
||||
else if (netbuffer->u.clientcfg.version != VERSION
|
||||
|| netbuffer->u.clientcfg.subversion != SUBVERSION)
|
||||
SV_SendRefuse(node, va(M_GetText("Different SRB2Kart versions cannot\nplay a netgame!\n(server version %d.%d.%d)"), VERSION/100, VERSION%100, SUBVERSION));
|
||||
SV_SendRefuse(node, va(M_GetText("Different SRB2Kart versions cannot\nplay a netgame!\n(server version %d.%d)"), VERSION, SUBVERSION));
|
||||
else if (!cv_allownewplayer.value && node)
|
||||
SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment"));
|
||||
else if (D_NumPlayers() >= maxplayers)
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ extern char logfilename[1024];
|
|||
// most interface strings are ignored in development mode.
|
||||
// we use comprevision and compbranch instead.
|
||||
#else
|
||||
#define VERSION 200 // Game version
|
||||
#define VERSION 2 // Game version
|
||||
#define SUBVERSION 0 // more precise version number
|
||||
#define VERSIONSTRING "v2.0"
|
||||
#define VERSIONSTRINGW L"v2.0"
|
||||
|
|
|
|||
|
|
@ -6367,10 +6367,6 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
|
|||
|
||||
void K_KartPlayerAfterThink(player_t *player)
|
||||
{
|
||||
// Moved to afterthink, as at this point the players have had their distances to the finish line updated
|
||||
// and this will correctly account for all players
|
||||
K_KartUpdatePosition(player);
|
||||
|
||||
if (player->kartstuff[k_curshield]
|
||||
|| player->kartstuff[k_invincibilitytimer]
|
||||
|| (player->kartstuff[k_growshrinktimer] != 0 && player->kartstuff[k_growshrinktimer] % 5 == 4)) // 4 instead of 0 because this is afterthink!
|
||||
|
|
@ -6709,7 +6705,7 @@ static boolean K_PlayerCloserToNextWaypoints(waypoint_t *const waypoint, player_
|
|||
}
|
||||
|
||||
/*--------------------------------------------------
|
||||
static void K_UpdateDistanceFromFinishLine(player_t *const player)
|
||||
void K_UpdateDistanceFromFinishLine(player_t *const player)
|
||||
|
||||
Updates the distance a player has to the finish line.
|
||||
|
||||
|
|
@ -6719,7 +6715,7 @@ static boolean K_PlayerCloserToNextWaypoints(waypoint_t *const waypoint, player_
|
|||
Return:-
|
||||
None
|
||||
--------------------------------------------------*/
|
||||
static void K_UpdateDistanceFromFinishLine(player_t *const player)
|
||||
void K_UpdateDistanceFromFinishLine(player_t *const player)
|
||||
{
|
||||
if ((player != NULL) && (player->mo != NULL))
|
||||
{
|
||||
|
|
@ -7273,7 +7269,6 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
|
|||
boolean HOLDING_ITEM = (player->kartstuff[k_itemheld] || player->kartstuff[k_eggmanheld]);
|
||||
boolean NO_HYUDORO = (player->kartstuff[k_stolentimer] == 0 && player->kartstuff[k_stealingtimer] == 0);
|
||||
|
||||
K_UpdateDistanceFromFinishLine(player);
|
||||
player->pflags &= ~PF_HITFINISHLINE;
|
||||
|
||||
if (!player->exiting)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ void K_DropHnextList(player_t *player, boolean keepshields);
|
|||
void K_RepairOrbitChain(mobj_t *orbit);
|
||||
player_t *K_FindJawzTarget(mobj_t *actor, player_t *source);
|
||||
INT32 K_GetKartRingPower(player_t *player);
|
||||
void K_UpdateDistanceFromFinishLine(player_t *const player);
|
||||
boolean K_CheckPlayersRespawnColliding(INT32 playernum, fixed_t x, fixed_t y);
|
||||
INT16 K_GetKartTurnValue(player_t *player, INT16 turnvalue);
|
||||
INT32 K_GetKartDriftSparkValue(player_t *player);
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ static INT32 AddToMasterServer(boolean firstadd)
|
|||
strcpy(info->name, cv_servername.string);
|
||||
M_Memcpy(&info->room, & room, sizeof (INT32));
|
||||
#if VERSION > 0 || SUBVERSION > 0
|
||||
sprintf(info->version, "%d.%d.%d", VERSION/100, VERSION%100, SUBVERSION);
|
||||
sprintf(info->version, "%d.%d", VERSION, SUBVERSION);
|
||||
#else // Trunk build, send revision info
|
||||
strcpy(info->version, GetRevisionString());
|
||||
#endif
|
||||
|
|
@ -789,7 +789,7 @@ static INT32 RemoveFromMasterSever(void)
|
|||
strcpy(info->ip, "");
|
||||
strcpy(info->port, int2str(current_port));
|
||||
strcpy(info->name, registered_server.name);
|
||||
sprintf(info->version, "%d.%d.%d", VERSION/100, VERSION%100, SUBVERSION);
|
||||
sprintf(info->version, "%d.%d", VERSION, SUBVERSION);
|
||||
|
||||
msg.type = REMOVE_SERVER_MSG;
|
||||
msg.length = (UINT32)sizeof (msg_server_t);
|
||||
|
|
|
|||
73
src/p_mobj.c
73
src/p_mobj.c
|
|
@ -6149,7 +6149,7 @@ boolean P_IsKartItem(INT32 type)
|
|||
type == MT_JAWZ || type == MT_JAWZ_DUD || type == MT_JAWZ_SHIELD ||
|
||||
type == MT_SSMINE || type == MT_SSMINE_SHIELD ||
|
||||
type == MT_SINK || type == MT_SINK_SHIELD ||
|
||||
type == MT_SPB)
|
||||
type == MT_SPB || type == MT_BALLHOG || type == MT_BUBBLESHIELDTRAP)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
|
|
@ -7171,6 +7171,41 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Destroy items sector special
|
||||
if (mobj->type == MT_BANANA || mobj->type == MT_EGGMANITEM
|
||||
|| mobj->type == MT_ORBINAUT || mobj->type == MT_BALLHOG
|
||||
|| mobj->type == MT_JAWZ || mobj->type == MT_JAWZ_DUD
|
||||
|| mobj->type == MT_SSMINE || mobj->type == MT_BUBBLESHIELDTRAP)
|
||||
{
|
||||
if (mobj->health > 0 && P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
if (mobj->type == MT_SSMINE
|
||||
|| mobj->type == MT_BUBBLESHIELDTRAP
|
||||
|| mobj->type == MT_BALLHOG)
|
||||
{
|
||||
S_StartSound(mobj, mobj->info->deathsound);
|
||||
P_KillMobj(mobj, NULL, NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This Item Damage
|
||||
if (mobj->eflags & MFE_VERTICALFLIP)
|
||||
mobj->z -= mobj->height;
|
||||
else
|
||||
mobj->z += mobj->height;
|
||||
|
||||
S_StartSound(mobj, mobj->info->deathsound);
|
||||
P_KillMobj(mobj, NULL, NULL);
|
||||
|
||||
P_SetObjectMomZ(mobj, 8*FRACUNIT, false);
|
||||
P_InstaThrust(mobj, R_PointToAngle2(0, 0, mobj->momx, mobj->momy) + ANGLE_90, 16*FRACUNIT);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if it's pushable, or if it would be pushable other than temporary disablement, use the
|
||||
// separate thinker
|
||||
if (mobj->flags & MF_PUSHABLE || (mobj->info->flags & MF_PUSHABLE && mobj->fuse))
|
||||
|
|
@ -7935,12 +7970,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
{
|
||||
boolean grounded = P_IsObjectOnGround(mobj);
|
||||
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->flags2 & MF2_AMBUSH)
|
||||
{
|
||||
if (grounded && (mobj->flags & MF_NOCLIPTHING))
|
||||
|
|
@ -8011,12 +8040,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
{
|
||||
mobj_t *ghost = P_SpawnGhostMobj(mobj);
|
||||
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->target && !P_MobjWasRemoved(mobj->target) && mobj->target->player)
|
||||
{
|
||||
ghost->color = mobj->target->player->skincolor;
|
||||
|
|
@ -8041,12 +8064,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
{
|
||||
boolean grounded = P_IsObjectOnGround(mobj);
|
||||
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->flags2 & MF2_AMBUSH)
|
||||
{
|
||||
if (grounded && (mobj->flags & MF_NOCLIPTHING))
|
||||
|
|
@ -8120,12 +8137,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
case MT_BANANA:
|
||||
mobj->friction = ORIG_FRICTION/4;
|
||||
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->momx || mobj->momy)
|
||||
{
|
||||
mobj_t *ghost = P_SpawnGhostMobj(mobj);
|
||||
|
|
@ -8155,12 +8166,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
mobj_t *ghost = P_SpawnGhostMobj(mobj);
|
||||
ghost->fuse = 3;
|
||||
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->target && !P_MobjWasRemoved(mobj->target) && mobj->target->player)
|
||||
{
|
||||
ghost->color = mobj->target->player->skincolor;
|
||||
|
|
@ -8193,12 +8198,6 @@ void P_MobjThinker(mobj_t *mobj)
|
|||
mobj->threshold--;
|
||||
break;
|
||||
case MT_SSMINE:
|
||||
if (P_MobjTouchingSectorSpecial(mobj, 4, 7, true))
|
||||
{
|
||||
P_RemoveMobj(mobj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobj->target && mobj->target->player)
|
||||
mobj->color = mobj->target->player->skincolor;
|
||||
else
|
||||
|
|
|
|||
22
src/p_tick.c
22
src/p_tick.c
|
|
@ -616,6 +616,17 @@ void P_Ticker(boolean run)
|
|||
G_ReadDemoTiccmd(&players[i].cmd, i);
|
||||
}
|
||||
|
||||
// First loop: Ensure all players' distance to the finish line are all accurate
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
K_UpdateDistanceFromFinishLine(&players[i]);
|
||||
|
||||
// Second loop: Ensure all player positions reflect everyone's distances
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
K_KartUpdatePosition(&players[i]);
|
||||
|
||||
// OK! Now that we got all of that sorted, players can think!
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
P_PlayerThink(&players[i]);
|
||||
|
|
@ -778,6 +789,17 @@ void P_PreTicker(INT32 frames)
|
|||
{
|
||||
P_MapStart();
|
||||
|
||||
// First loop: Ensure all players' distance to the finish line are all accurate
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
K_UpdateDistanceFromFinishLine(&players[i]);
|
||||
|
||||
// Second loop: Ensure all player positions reflect everyone's distances
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
K_KartUpdatePosition(&players[i]);
|
||||
|
||||
// OK! Now that we got all of that sorted, players can think!
|
||||
for (i = 0; i < MAXPLAYERS; i++)
|
||||
if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
|
||||
{
|
||||
|
|
|
|||
39
src/p_user.c
39
src/p_user.c
|
|
@ -7140,13 +7140,14 @@ void P_ResetCamera(player_t *player, camera_t *thiscam)
|
|||
|
||||
boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcalled)
|
||||
{
|
||||
static UINT8 lookbackdelay[4] = {0,0,0,0};
|
||||
static boolean lookbackactive[MAXSPLITSCREENPLAYERS];
|
||||
static UINT8 lookbackdelay[MAXSPLITSCREENPLAYERS];
|
||||
UINT8 num;
|
||||
angle_t angle = 0, focusangle = 0, focusaiming = 0, pitch = 0;
|
||||
fixed_t x, y, z, dist, distxy, distz, height, viewpointx, viewpointy, camspeed, camdist, camheight, pviewheight;
|
||||
fixed_t x, y, z, dist, distxy, distz, viewpointx, viewpointy, camspeed, camdist, camheight, pviewheight;
|
||||
fixed_t pan, xpan, ypan;
|
||||
INT32 camrotate;
|
||||
boolean camstill, lookback;
|
||||
boolean camstill, lookback, lookbackdown;
|
||||
UINT8 timeover;
|
||||
mobj_t *mo;
|
||||
fixed_t f1, f2;
|
||||
|
|
@ -7326,15 +7327,19 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
camstill = true;
|
||||
else if (lookback || lookbackdelay[num]) // SRB2kart - Camera flipper
|
||||
{
|
||||
#define MAXLOOKBACKDELAY 2
|
||||
camspeed = FRACUNIT;
|
||||
if (lookback)
|
||||
{
|
||||
camrotate += 180;
|
||||
lookbackdelay[num] = 2;
|
||||
lookbackdelay[num] = MAXLOOKBACKDELAY;
|
||||
}
|
||||
else
|
||||
lookbackdelay[num]--;
|
||||
}
|
||||
lookbackdown = (lookbackdelay[num] == MAXLOOKBACKDELAY) != lookbackactive[num];
|
||||
lookbackactive[num] = (lookbackdelay[num] == MAXLOOKBACKDELAY);
|
||||
#undef MAXLOOKBACKDELAY
|
||||
|
||||
if (mo->eflags & MFE_VERTICALFLIP)
|
||||
camheight += thiscam->height;
|
||||
|
|
@ -7377,8 +7382,6 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
thiscam->angle = angle;
|
||||
}
|
||||
|
||||
height = camheight;
|
||||
|
||||
// sets ideal cam pos
|
||||
dist = camdist;
|
||||
|
||||
|
|
@ -7387,14 +7390,11 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
dist += abs(thiscam->momz)/4;
|
||||
|
||||
if (player->karthud[khud_boostcam])
|
||||
{
|
||||
dist -= FixedMul(11*dist/16, player->karthud[khud_boostcam]);
|
||||
height -= FixedMul(height, player->karthud[khud_boostcam]);
|
||||
}
|
||||
|
||||
if (mo->standingslope)
|
||||
{
|
||||
pitch = (angle_t)FixedMul(P_ReturnThrustX(mo, player->frameangle - mo->standingslope->xydirection, FRACUNIT), (fixed_t)mo->standingslope->zangle);
|
||||
pitch = (angle_t)FixedMul(P_ReturnThrustX(mo, thiscam->angle - mo->standingslope->xydirection, FRACUNIT), (fixed_t)mo->standingslope->zangle);
|
||||
if (mo->eflags & MFE_VERTICALFLIP)
|
||||
{
|
||||
if (pitch >= ANGLE_180)
|
||||
|
|
@ -7417,8 +7417,6 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
else
|
||||
distxy = dist;
|
||||
distz = -FixedMul(dist, FINESINE((pitch>>ANGLETOFINESHIFT) & FINEMASK));
|
||||
if (r_splitscreen == 1) // 2 player is weird, this helps keep players on screen
|
||||
distz = 3*distz/5;
|
||||
|
||||
x = mo->x - FixedMul(FINECOSINE((angle>>ANGLETOFINESHIFT) & FINEMASK), distxy);
|
||||
y = mo->y - FixedMul(FINESINE((angle>>ANGLETOFINESHIFT) & FINEMASK), distxy);
|
||||
|
|
@ -7459,9 +7457,15 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
pviewheight = FixedMul(32<<FRACBITS, mo->scale);
|
||||
|
||||
if (mo->eflags & MFE_VERTICALFLIP)
|
||||
z = mo->z + mo->height - pviewheight - camheight + distz;
|
||||
{
|
||||
distz = min(-camheight, distz);
|
||||
z = mo->z + mo->height - pviewheight + distz;
|
||||
}
|
||||
else
|
||||
z = mo->z + pviewheight + camheight + distz;
|
||||
{
|
||||
distz = max(camheight, distz);
|
||||
z = mo->z + pviewheight + distz;
|
||||
}
|
||||
|
||||
#ifndef NOCLIPCAM // Disable all z-clipping for noclip cam
|
||||
// move camera down to move under lower ceilings
|
||||
|
|
@ -7707,13 +7711,13 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
{
|
||||
angle = R_PointToAngle2(0, thiscam->z + thiscam->height, dist, mo->z + mo->height - P_GetPlayerHeight(player));
|
||||
if (thiscam->pitch < ANGLE_180 && thiscam->pitch > angle)
|
||||
angle = thiscam->pitch;
|
||||
angle += (thiscam->pitch - angle)/2;
|
||||
}
|
||||
else
|
||||
{
|
||||
angle = R_PointToAngle2(0, thiscam->z, dist, mo->z + P_GetPlayerHeight(player));
|
||||
if (thiscam->pitch >= ANGLE_180 && thiscam->pitch < angle)
|
||||
angle = thiscam->pitch;
|
||||
angle -= (angle - thiscam->pitch)/2;
|
||||
}
|
||||
|
||||
if (player->playerstate != PST_DEAD && !((player->pflags & PF_NIGHTSMODE) && player->exiting))
|
||||
|
|
@ -7756,6 +7760,9 @@ boolean P_MoveChaseCamera(player_t *player, camera_t *thiscam, boolean resetcall
|
|||
thiscam->aiming = ANGLE_22h;
|
||||
}
|
||||
|
||||
if (lookbackdown)
|
||||
P_MoveChaseCamera(player, thiscam, false);
|
||||
|
||||
return (x == thiscam->x && y == thiscam->y && z == thiscam->z && angle == thiscam->aiming);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -839,7 +839,7 @@ static void R_SetupFreelook(void)
|
|||
// clip it in the case we are looking a hardware 90 degrees full aiming
|
||||
// (lmps, network and use F12...)
|
||||
G_SoftwareClipAimingPitch((INT32 *)&aimingangle);
|
||||
dy = AIMINGTODY(aimingangle) * viewheight/BASEVIDHEIGHT;
|
||||
dy = AIMINGTODY(aimingangle) * viewwidth/BASEVIDWIDTH;
|
||||
yslope = &yslopetab[viewheight*8 - (viewheight/2 + dy)];
|
||||
}
|
||||
centery = (viewheight/2) + dy;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue