mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-03-14 12:20:45 +00:00
Weapons add, special respawns
This commit is contained in:
parent
1515a02712
commit
5aa2fb1b2d
3 changed files with 199 additions and 134 deletions
|
@ -1349,20 +1349,34 @@ static void Use_Compass(edict_t *ent, gitem_t *inv)
|
|||
#define SPEC_RESPAWN_TIME 60
|
||||
// time before they will get respawned
|
||||
#define SPEC_TECH_TIMEOUT 60
|
||||
constexpr item_id_t weap_ids[] = {
|
||||
IT_WEAPON_MP5,
|
||||
IT_WEAPON_M4,
|
||||
IT_WEAPON_M3,
|
||||
IT_WEAPON_HANDCANNON,
|
||||
IT_WEAPON_SNIPER,
|
||||
IT_WEAPON_KNIFE,
|
||||
};
|
||||
|
||||
void SpecThink(edict_t * spec);
|
||||
|
||||
static edict_t *FindSpecSpawn(void)
|
||||
static edict_t *FindSpecSpawn()
|
||||
{
|
||||
edict_t *spot = NULL;
|
||||
int i = rand() % 16;
|
||||
return SelectDeathmatchSpawnPoint(false, true, true).spot;
|
||||
}
|
||||
|
||||
while (i--)
|
||||
spot = G_Find(spot, FOFS(classname), "info_player_deathmatch");
|
||||
if (!spot)
|
||||
spot = G_Find(spot, FOFS(classname), "info_player_deathmatch");
|
||||
THINK(SpecThink) (edict_t *special) -> void
|
||||
{
|
||||
edict_t *spot;
|
||||
|
||||
return spot;
|
||||
if ((spot = FindSpecSpawn()) != nullptr)
|
||||
{
|
||||
SpawnSpec(special->item, spot);
|
||||
G_FreeEdict(special);
|
||||
}
|
||||
else
|
||||
{
|
||||
special->nextthink = level.time + 10_ms;
|
||||
special->think = SpecThink;
|
||||
}
|
||||
}
|
||||
|
||||
static void SpawnSpec(gitem_t * item, edict_t * spot)
|
||||
|
@ -1372,15 +1386,14 @@ static void SpawnSpec(gitem_t * item, edict_t * spot)
|
|||
|
||||
ent = G_Spawn();
|
||||
ent->classname = item->classname;
|
||||
ent->typeNum = item->typeNum;
|
||||
ent->item = item;
|
||||
ent->spawnflags = DROPPED_ITEM;
|
||||
ent->spawnflags = SPAWNFLAG_ITEM_DROPPED;
|
||||
ent->s.effects = item->world_model_flags;
|
||||
ent->s.renderfx = RF_GLOW;
|
||||
VectorSet(ent->mins, -15, -15, -15);
|
||||
VectorSet(ent->maxs, 15, 15, 15);
|
||||
// zucc dumb hack to make laser look like it is on the ground
|
||||
if (item->typeNum == LASER_NUM) {
|
||||
if (item->tag == POWERUP_LASERSIGHT) {
|
||||
VectorSet(ent->mins, -15, -15, -1);
|
||||
VectorSet(ent->maxs, 15, 15, 1);
|
||||
}
|
||||
|
@ -1394,52 +1407,18 @@ static void SpawnSpec(gitem_t * item, edict_t * spot)
|
|||
angles[1] = rand() % 360;
|
||||
angles[2] = 0;
|
||||
|
||||
AngleVectors(angles, forward, right, NULL);
|
||||
VectorCopy(spot->s.origin, ent->s.origin);
|
||||
AngleVectors(angles, forward, right, nullptr);
|
||||
ent->s.origin = spot->s.origin;
|
||||
ent->s.origin[2] += 16;
|
||||
VectorCopy(ent->s.origin, ent->old_origin);
|
||||
VectorScale(forward, 100, ent->velocity);
|
||||
ent->velocity = forward * 100;
|
||||
ent->velocity[2] = 300;
|
||||
|
||||
ent->nextthink = level.framenum + SPEC_RESPAWN_TIME * HZ;
|
||||
ent->nextthink = level.time + 60_ms;
|
||||
ent->think = SpecThink;
|
||||
|
||||
gi.linkentity(ent);
|
||||
}
|
||||
|
||||
void SpawnSpecs(edict_t * ent)
|
||||
{
|
||||
gitem_t *spec;
|
||||
edict_t *spot;
|
||||
int i, itemNum;
|
||||
|
||||
G_FreeEdict(ent);
|
||||
|
||||
// if(item_respawnmode->value)
|
||||
// return;
|
||||
|
||||
for(i = 0; i<ITEM_COUNT; i++)
|
||||
{
|
||||
itemNum = ITEM_FIRST + i;
|
||||
|
||||
if ((spec = GET_ITEM(itemNum)) != NULL && (spot = FindSpecSpawn()) != NULL) {
|
||||
//gi.dprintf("Spawning special item '%s'.\n", tnames[i]);
|
||||
SpawnSpec(spec, spot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SpecThink(edict_t * spec)
|
||||
{
|
||||
edict_t *spot;
|
||||
|
||||
spot = FindSpecSpawn();
|
||||
if (spot) {
|
||||
SpawnSpec(spec->item, spot);
|
||||
}
|
||||
|
||||
G_FreeEdict(spec);
|
||||
}
|
||||
|
||||
static void MakeTouchSpecThink(edict_t * ent)
|
||||
{
|
||||
|
@ -1447,31 +1426,26 @@ static void MakeTouchSpecThink(edict_t * ent)
|
|||
ent->touch = Touch_Item;
|
||||
|
||||
if (allitem->value) {
|
||||
ent->nextthink = level.framenum + 1 * HZ;
|
||||
ent->nextthink = level.time + 10_ms;
|
||||
ent->think = G_FreeEdict;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameSettings & GS_ROUNDBASED) {
|
||||
ent->nextthink = level.framenum + 60 * HZ; //FIXME: should this be roundtime left
|
||||
ent->nextthink = level.time + 60_ms; //FIXME: should this be roundtime left
|
||||
ent->think = G_FreeEdict;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameSettings & GS_WEAPONCHOOSE) {
|
||||
ent->nextthink = level.framenum + 6 * HZ;
|
||||
ent->nextthink = level.time + 60_ms;
|
||||
ent->think = G_FreeEdict;
|
||||
return;
|
||||
}
|
||||
|
||||
if(item_respawnmode->value) {
|
||||
ent->nextthink = level.framenum + (item_respawn->value*0.5f) * HZ;
|
||||
ent->think = G_FreeEdict;
|
||||
}
|
||||
else {
|
||||
ent->nextthink = level.framenum + item_respawn->value * HZ;
|
||||
ent->think = SpecThink;
|
||||
}
|
||||
// All else..
|
||||
ent->nextthink = level.time + 60_ms;
|
||||
ent->think = SpecThink;
|
||||
}
|
||||
|
||||
void Drop_Spec(edict_t * ent, gitem_t * item)
|
||||
|
@ -1480,11 +1454,11 @@ void Drop_Spec(edict_t * ent, gitem_t * item)
|
|||
|
||||
spec = Drop_Item(ent, item);
|
||||
//gi.cprintf(ent, PRINT_HIGH, "Dropping special item.\n");
|
||||
spec->nextthink = level.framenum + 1 * HZ;
|
||||
spec->nextthink = level.time + 10_ms;
|
||||
spec->think = MakeTouchSpecThink;
|
||||
//zucc this and the one below should probably be -- not = 0, if
|
||||
// a server turns on multiple item pickup.
|
||||
ent->client->inventory[ITEM_INDEX(item)]--;
|
||||
ent->client->pers.inventory[ITEM_INDEX(item)]--;
|
||||
}
|
||||
|
||||
void DeadDropSpec(edict_t * ent)
|
||||
|
@ -1493,56 +1467,34 @@ void DeadDropSpec(edict_t * ent)
|
|||
edict_t *dropped;
|
||||
int i, itemNum;
|
||||
|
||||
for(i = 0; i<ITEM_COUNT; i++)
|
||||
i = 0;
|
||||
for (; i < q_countof(weap_ids); i++)
|
||||
{
|
||||
itemNum = ITEM_FIRST + i;
|
||||
if (INV_AMMO(ent, itemNum) > 0) {
|
||||
spec = GET_ITEM(itemNum);
|
||||
dropped = Drop_Item(ent, spec);
|
||||
if (ent->client->pers.inventory[weap_ids[i]])
|
||||
{
|
||||
dropped = Drop_Item(ent, GetItemByIndex(weap_ids[i]));
|
||||
// hack the velocity to make it bounce random
|
||||
dropped->velocity[0] = (rand() % 600) - 300;
|
||||
dropped->velocity[1] = (rand() % 600) - 300;
|
||||
dropped->nextthink = level.framenum + 1 * HZ;
|
||||
dropped->nextthink = level.time + 10_ms;
|
||||
dropped->think = MakeTouchSpecThink;
|
||||
dropped->owner = NULL;
|
||||
dropped->spawnflags = DROPPED_PLAYER_ITEM;
|
||||
ent->client->inventory[ITEM_INDEX(spec)] = 0;
|
||||
dropped->owner = nullptr;
|
||||
dropped->spawnflags = SPAWNFLAG_ITEM_DROPPED_PLAYER;
|
||||
ent->client->pers.inventory[weap_ids[i]] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// frees the passed edict!
|
||||
void RespawnSpec(edict_t * ent)
|
||||
{
|
||||
edict_t *spot;
|
||||
|
||||
if ((spot = FindSpecSpawn()) != NULL)
|
||||
SpawnSpec(ent->item, spot);
|
||||
G_FreeEdict(ent);
|
||||
}
|
||||
|
||||
void SetupSpecSpawn(void)
|
||||
{
|
||||
edict_t *ent;
|
||||
|
||||
if (level.specspawn)
|
||||
return;
|
||||
|
||||
ent = G_Spawn();
|
||||
ent->nextthink = level.framenum + 4 * HZ;
|
||||
ent->think = SpawnSpecs;
|
||||
level.specspawn = 1;
|
||||
}
|
||||
|
||||
void AddItem(edict_t *ent, gitem_t *item)
|
||||
{
|
||||
ent->client->inventory[ITEM_INDEX (item)]++;
|
||||
ent->client->pers.inventory[ITEM_INDEX (item)]++;
|
||||
ent->client->unique_item_total++;
|
||||
if (item->typeNum == LASER_NUM)
|
||||
|
||||
if (item->tag == POWERUP_LASERSIGHT)
|
||||
{
|
||||
SP_LaserSight(ent, item); //ent->item->use(other, ent->item);
|
||||
}
|
||||
else if (item->typeNum == BAND_NUM)
|
||||
else if (item->tag == POWERUP_BANDOLIER)
|
||||
{
|
||||
|
||||
if (ent->client->max_pistolmags < 4)
|
||||
|
@ -1578,12 +1530,14 @@ bool Pickup_Special (edict_t * ent, edict_t * other)
|
|||
|
||||
return true;
|
||||
}
|
||||
void Drop_Special (edict_t * ent, gitem_t * item)
|
||||
|
||||
|
||||
void Drop_Special (edict_t *ent, gitem_t *item)
|
||||
{
|
||||
int count;
|
||||
|
||||
ent->client->unique_item_total--;
|
||||
if (item->typeNum == BAND_NUM && INV_AMMO(ent, BAND_NUM) <= 1)
|
||||
if (item->tag == POWERUP_BANDOLIER && INV_AMMO(ent, IT_ITEM_BANDOLIER) <= 1)
|
||||
{
|
||||
if (gameSettings & GS_DEATHMATCH)
|
||||
count = 2;
|
||||
|
@ -1591,11 +1545,11 @@ void Drop_Special (edict_t * ent, gitem_t * item)
|
|||
count = 1;
|
||||
|
||||
ent->client->max_pistolmags = count;
|
||||
if (INV_AMMO(ent, MK23_ANUM) > count)
|
||||
INV_AMMO(ent, MK23_ANUM) = count;
|
||||
if (INV_AMMO(ent, IT_AMMO_BULLETS) > count)
|
||||
INV_AMMO(ent, IT_AMMO_BULLETS) = count;
|
||||
|
||||
if (!(gameSettings & GS_DEATHMATCH)) {
|
||||
if(ent->client->pers.chosenWeapon->typeNum == HC_NUM)
|
||||
if(ent->client->pers.chosenWeapon->id == IT_WEAPON_HANDCANNON)
|
||||
count = 12;
|
||||
else
|
||||
count = 7;
|
||||
|
@ -1603,52 +1557,36 @@ void Drop_Special (edict_t * ent, gitem_t * item)
|
|||
count = 14;
|
||||
}
|
||||
ent->client->max_shells = count;
|
||||
if (INV_AMMO(ent, SHELL_ANUM) > count)
|
||||
INV_AMMO(ent, SHELL_ANUM) = count;
|
||||
if (INV_AMMO(ent, IT_AMMO_SHELLS) > count)
|
||||
INV_AMMO(ent, IT_AMMO_SHELLS) = count;
|
||||
|
||||
ent->client->max_m4mags = 1;
|
||||
if (INV_AMMO(ent, M4_ANUM) > 1)
|
||||
INV_AMMO(ent, M4_ANUM) = 1;
|
||||
if (INV_AMMO(ent, IT_AMMO_CELLS) > 1)
|
||||
INV_AMMO(ent, IT_AMMO_CELLS) = 1;
|
||||
|
||||
ent->client->grenade_max = 2;
|
||||
if (use_buggy_bandolier->value == 0) {
|
||||
if ((gameSettings & GS_DEATHMATCH) && INV_AMMO(ent, GRENADE_NUM) > 2)
|
||||
INV_AMMO(ent, GRENADE_NUM) = 2;
|
||||
else if (teamplay->value) {
|
||||
if (ent->client->curr_weap == GRENADE_NUM)
|
||||
INV_AMMO(ent, GRENADE_NUM) = 1;
|
||||
else
|
||||
INV_AMMO(ent, GRENADE_NUM) = 0;
|
||||
}
|
||||
} else {
|
||||
if (INV_AMMO(ent, GRENADE_NUM) > 2)
|
||||
INV_AMMO(ent, GRENADE_NUM) = 2;
|
||||
if (INV_AMMO(ent, IT_WEAPON_GRENADES) > 2)
|
||||
INV_AMMO(ent, IT_WEAPON_GRENADES) = 2;
|
||||
}
|
||||
if (gameSettings & GS_DEATHMATCH)
|
||||
count = 2;
|
||||
else
|
||||
count = 1;
|
||||
ent->client->max_mp5mags = count;
|
||||
if (INV_AMMO(ent, MP5_ANUM) > count)
|
||||
INV_AMMO(ent, MP5_ANUM) = count;
|
||||
if (INV_AMMO(ent, IT_AMMO_ROCKETS) > count)
|
||||
INV_AMMO(ent, IT_AMMO_ROCKETS) = count;
|
||||
|
||||
ent->client->knife_max = 10;
|
||||
if (INV_AMMO(ent, KNIFE_NUM) > 10)
|
||||
INV_AMMO(ent, KNIFE_NUM) = 10;
|
||||
if (INV_AMMO(ent, IT_WEAPON_KNIFE) > 10)
|
||||
INV_AMMO(ent, IT_WEAPON_KNIFE) = 10;
|
||||
|
||||
if (gameSettings & GS_DEATHMATCH)
|
||||
count = 20;
|
||||
else
|
||||
count = 10;
|
||||
ent->client->max_sniper_rnds = count;
|
||||
if (INV_AMMO(ent, SNIPER_ANUM) > count)
|
||||
INV_AMMO(ent, SNIPER_ANUM) = count;
|
||||
|
||||
if (ent->client->unique_weapon_total > unique_weapons->value && !allweapon->value)
|
||||
{
|
||||
DropExtraSpecial (ent);
|
||||
gi.cprintf (ent, PRINT_HIGH, "One of your guns is dropped with the bandolier.\n");
|
||||
}
|
||||
if (INV_AMMO(ent, IT_AMMO_SLUGS) > count)
|
||||
INV_AMMO(ent, IT_AMMO_SLUGS) = count;
|
||||
}
|
||||
Drop_Spec(ent, item);
|
||||
ValidateSelectedItem(ent);
|
||||
|
@ -2215,7 +2153,7 @@ model="models/items/ammo/rockets/medium/tris.md2"
|
|||
/*QUAKED item_helmet (.3 .3 1) (-16 -16 -16) (16 16 16)
|
||||
*/
|
||||
{
|
||||
/* id */ IT_ITEM_LASERSIGHT,
|
||||
/* id */ IT_ITEM_HELM,
|
||||
/* classname */ "item_helmet",
|
||||
/* pickup */ Pickup_Special,
|
||||
/* use */ nullptr,
|
||||
|
|
|
@ -1861,6 +1861,52 @@ extern gitem_t itemlist[IT_TOTAL];
|
|||
// Action Add
|
||||
//======================================================================
|
||||
|
||||
#define IS_ALIVE(ent) ((ent)->solid != SOLID_NOT && (ent)->deadflag)
|
||||
|
||||
#define GS_DEATHMATCH 1
|
||||
#define GS_TEAMPLAY 2
|
||||
#define GS_MATCHMODE 4
|
||||
#define GS_ROUNDBASED 8
|
||||
#define GS_WEAPONCHOOSE 16
|
||||
|
||||
// sniper modes
|
||||
#define SNIPER_1X 0
|
||||
#define SNIPER_2X 1
|
||||
#define SNIPER_4X 2
|
||||
#define SNIPER_6X 3
|
||||
#define SNIPER_MODE_MAX 4
|
||||
|
||||
//TempFile sniper zoom moved to constants
|
||||
#define SNIPER_FOV1 90
|
||||
#define SNIPER_FOV2 45
|
||||
#define SNIPER_FOV4 20
|
||||
#define SNIPER_FOV6 10
|
||||
|
||||
#define GRENADE_IDLE_FIRST 40
|
||||
#define GRENADE_IDLE_LAST 69
|
||||
#define GRENADE_THROW_FIRST 4
|
||||
#define GRENADE_THROW_LAST 9 // throw it on frame 8?
|
||||
// Igor's back in Time to hard grenades :-)
|
||||
#define GRENADE_DAMRAD_CLASSIC 170
|
||||
#define GRENADE_DAMRAD 250
|
||||
|
||||
#define SPEC_WEAPON_RESPAWN 1
|
||||
#define BANDAGE_TIME 27 // 10 = 1 second
|
||||
#define ENHANCED_BANDAGE_TIME 10
|
||||
#define BLEED_TIME 10 // 10 = 1 second is time for losing 1 health at slowest bleed rate
|
||||
|
||||
|
||||
int32_t gameSettings; // Round based, deathmatch, etc?
|
||||
|
||||
extern cvar_t *allitem;
|
||||
extern cvar_t *allweapon;
|
||||
extern cvar_t *unique_items;
|
||||
extern cvar_t *unique_weapons;
|
||||
extern cvar_t *allow_hoarding;
|
||||
extern cvar_t *item_respawn;
|
||||
extern cvar_t *weapon_respawn;
|
||||
extern cvar_t *ammo_respawn;
|
||||
|
||||
void LaserSightThink (edict_t * self);
|
||||
void SP_LaserSight (edict_t * self, gitem_t * item);
|
||||
void Cmd_Reload_f (edict_t * ent);
|
||||
|
@ -1903,7 +1949,7 @@ void Compass_Update(edict_t *ent, bool first);
|
|||
|
||||
// ACTION
|
||||
#define ITEM_INDEX(x) ((x)-itemlist)
|
||||
#define INV_AMMO(ent, num) ((ent)->client->inventory[items[(num)].index])
|
||||
#define INV_AMMO(ent, num) ((ent)->client->pers.inventory[items[(num)].index])
|
||||
#define GET_ITEM(num) (&itemlist[items[(num)].index])
|
||||
|
||||
//
|
||||
|
@ -2665,6 +2711,22 @@ struct client_persistant_t
|
|||
int32_t lives; // player lives left (1 = no respawns remaining)
|
||||
uint8_t n64_crouch_warn_times;
|
||||
gtime_t n64_crouch_warning;
|
||||
|
||||
// Action Add
|
||||
|
||||
gitem_t *chosenItem; // item for teamplay
|
||||
gitem_t *chosenWeapon; // weapon for teamplay
|
||||
int32_t mk23_mode; // firing mode, semi or auto
|
||||
int32_t mp5_mode;
|
||||
int32_t m4_mode;
|
||||
int32_t knife_mode;
|
||||
int32_t grenade_mode;
|
||||
int32_t hc_mode;
|
||||
int32_t id; // id command on or off
|
||||
int32_t irvision; // ir on or off (only matters if player has ir device, currently bandolier)
|
||||
|
||||
|
||||
// Action Add End
|
||||
};
|
||||
|
||||
// client data that stays across deathmatch respawns
|
||||
|
@ -2898,6 +2960,36 @@ struct gclient_t
|
|||
height_fog_t heightfog;
|
||||
|
||||
gtime_t last_attacker_time;
|
||||
|
||||
// Action Add
|
||||
int32_t unique_item_total;
|
||||
int32_t unique_weapon_total;
|
||||
|
||||
int inventory[MAX_ITEMS];
|
||||
// ammo capacities
|
||||
int32_t max_pistolmags;
|
||||
int32_t max_shells;
|
||||
int32_t max_mp5mags;
|
||||
int32_t max_m4mags;
|
||||
int32_t max_sniper_rnds;
|
||||
int32_t mk23_max;
|
||||
int32_t mk23_rds;
|
||||
int32_t dual_max;
|
||||
int32_t dual_rds;
|
||||
int32_t shot_max;
|
||||
int32_t shot_rds;
|
||||
int32_t sniper_max;
|
||||
int32_t sniper_rds;
|
||||
int32_t mp5_max;
|
||||
int32_t mp5_rds;
|
||||
int32_t m4_max;
|
||||
int32_t m4_rds;
|
||||
int32_t cannon_max;
|
||||
int32_t cannon_rds;
|
||||
int32_t knife_max;
|
||||
int32_t grenade_max;
|
||||
|
||||
// Action Add End
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
|
|
|
@ -137,6 +137,24 @@ cvar_t *ai_model_scale;
|
|||
cvar_t *ai_allow_dm_spawn;
|
||||
cvar_t *ai_movement_disabled;
|
||||
|
||||
//======================================================================
|
||||
// Action Add
|
||||
//======================================================================
|
||||
|
||||
cvar_t *allitem;
|
||||
cvar_t *allweapon;
|
||||
cvar_t *unique_items;
|
||||
cvar_t *unique_weapons;
|
||||
cvar_t *allow_hoarding;
|
||||
cvar_t *item_respawn;
|
||||
cvar_t *weapon_respawn;
|
||||
cvar_t *ammo_respawn;
|
||||
|
||||
//======================================================================
|
||||
// Action Add End
|
||||
//======================================================================
|
||||
|
||||
|
||||
static cvar_t *g_frames_per_frame;
|
||||
|
||||
void SpawnEntities(const char *mapname, const char *entities, const char *spawnpoint);
|
||||
|
@ -352,6 +370,23 @@ void InitGame()
|
|||
g_map_list_shuffle = gi.cvar("g_map_list_shuffle", "0", CVAR_NOFLAGS);
|
||||
g_lag_compensation = gi.cvar("g_lag_compensation", "1", CVAR_NOFLAGS);
|
||||
|
||||
//======================================================================
|
||||
// Action Add
|
||||
//======================================================================
|
||||
|
||||
allitem = gi.cvar("allitem", "0", CVAR_NOFLAGS);
|
||||
allweapon = gi.cvar("allweapon", "0", CVAR_NOFLAGS);
|
||||
unique_items = gi.cvar("unique_items", "1", CVAR_NOFLAGS);
|
||||
unique_weapons = gi.cvar("unique_weapons", "1", CVAR_NOFLAGS);
|
||||
allow_hoarding = gi.cvar("allow_hoarding", "0", CVAR_NOFLAGS);
|
||||
item_respawn = gi.cvar("item_respawn", "59", CVAR_NOFLAGS);
|
||||
weapon_respawn = gi.cvar("weapon_respawn", "74", CVAR_NOFLAGS);
|
||||
ammo_respawn = gi.cvar("ammo_respawn", "30", CVAR_NOFLAGS);
|
||||
|
||||
//======================================================================
|
||||
// Action Add
|
||||
//======================================================================
|
||||
|
||||
// items
|
||||
InitItems();
|
||||
|
||||
|
|
Loading…
Reference in a new issue