Implement and parse the propdata system from Source 2004 and make our
func_breakable entity aware of it.
This commit is contained in:
parent
3cc01ca512
commit
3bceff6a2c
11 changed files with 464 additions and 2 deletions
|
@ -20,7 +20,6 @@
|
|||
void
|
||||
CSQC_Init(float apilevel, string enginename, float engineversion)
|
||||
{
|
||||
Sound_Init();
|
||||
pSeat = &g_seats[0];
|
||||
pSeatLocal = &g_seatslocal[0];
|
||||
|
||||
|
@ -86,6 +85,10 @@ CSQC_Init(float apilevel, string enginename, float engineversion)
|
|||
registercommand("+zoomin");
|
||||
registercommand("-zoomin");
|
||||
|
||||
/* Sound shaders */
|
||||
Sound_Init();
|
||||
PropData_Init();
|
||||
|
||||
/* VOX */
|
||||
Vox_Init();
|
||||
|
||||
|
@ -1107,6 +1110,7 @@ CSQC_Shutdown(void)
|
|||
Sentences_Shutdown();
|
||||
Titles_Shutdown();
|
||||
Sound_Shutdown();
|
||||
PropData_Shutdown();
|
||||
Vox_Shutdown();
|
||||
EFX_Shutdown();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ void
|
|||
Event_ProcessModel(float flTimeStamp, int iCode, string strData)
|
||||
{
|
||||
switch(iCode) {
|
||||
case 1004:
|
||||
sound(self, CHAN_BODY, strData, 1.0f, ATTN_NORM);
|
||||
break;
|
||||
case 5004: /* view model sound */
|
||||
localsound(strData, CHAN_AUTO, 1.0);
|
||||
break;
|
||||
|
|
|
@ -108,6 +108,7 @@ class func_breakable:CBaseTrigger
|
|||
int m_iMaterial;
|
||||
float m_flDelay;
|
||||
float m_flExplodeMag;
|
||||
float m_flExplodeRad;
|
||||
string m_strBreakSpawn;
|
||||
|
||||
/*entity m_pressAttacker;
|
||||
|
@ -170,7 +171,7 @@ func_breakable::Explode(void)
|
|||
vector rp = absmin + (0.5 * (absmax - absmin));
|
||||
FX_BreakModel(vlen(size) / 10, absmin, absmax, [0,0,0], m_iMaterial);
|
||||
FX_Explosion(rp);
|
||||
Damage_Radius(rp, this, m_flExplodeMag, m_flExplodeMag * 2.5f, TRUE, 0);
|
||||
Damage_Radius(rp, this, m_flExplodeMag, m_flExplodeRad, TRUE, 0);
|
||||
UseTargets(this, TRIG_TOGGLE, 0.0f); /* delay... ignored. */
|
||||
Hide();
|
||||
}
|
||||
|
@ -299,6 +300,12 @@ func_breakable::Respawn(void)
|
|||
if (!health) {
|
||||
health = 15;
|
||||
}
|
||||
|
||||
if (HasPropData()) {
|
||||
health = GetPropData(PROPINFO_HEALTH);
|
||||
m_flExplodeMag = GetPropData(PROPINFO_EXPLOSIVE_DMG);
|
||||
m_flExplodeRad = GetPropData(PROPINFO_EXPLOSIVE_RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -310,6 +317,7 @@ func_breakable::SpawnKey(string strKey, string strValue)
|
|||
break;
|
||||
case "explodemagnitude":
|
||||
m_flExplodeMag = stof(strValue);
|
||||
m_flExplodeRad = m_flExplodeMag * 2.5f;
|
||||
break;
|
||||
case "spawnobject":
|
||||
int oid = stoi(strValue);
|
||||
|
|
|
@ -18,6 +18,9 @@ class CBaseEntity
|
|||
{
|
||||
int m_iBody;
|
||||
|
||||
/* prop data */
|
||||
int m_iPropData;
|
||||
|
||||
#ifdef CLIENT
|
||||
float m_flSentenceTime;
|
||||
sound_t *m_pSentenceQue;
|
||||
|
@ -62,6 +65,8 @@ class CBaseEntity
|
|||
virtual vector(void) GetSpawnAngles;
|
||||
virtual string(void) GetSpawnModel;
|
||||
virtual float(void) GetSpawnHealth;
|
||||
virtual int(void) HasPropData;
|
||||
virtual __variant(int) GetPropData;
|
||||
|
||||
/* Input/Output System */
|
||||
string m_strOnTrigger;
|
||||
|
|
|
@ -816,6 +816,9 @@ CBaseEntity::SpawnInit(void)
|
|||
SpawnKey(argv(i), argv(i+1));
|
||||
}
|
||||
|
||||
/* tokenization complete, now we can load propdata */
|
||||
PropData_Finish();
|
||||
|
||||
/* some entity might involuntarily call SpawnInit as part of being
|
||||
a member of CBaseEntity. So we need to make sure that it doesn't
|
||||
inherit stuff from the last previously loaded entity */
|
||||
|
@ -848,6 +851,18 @@ CBaseEntity::Hide(void)
|
|||
SetMovetype(MOVETYPE_NONE);
|
||||
takedamage = DAMAGE_NO;
|
||||
}
|
||||
|
||||
int
|
||||
CBaseEntity::HasPropData(void)
|
||||
{
|
||||
return (m_iPropData != -1) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
__variant
|
||||
CBaseEntity::GetPropData(int type)
|
||||
{
|
||||
return Prop_GetInfo(m_iPropData, type);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
|
@ -860,7 +875,14 @@ client doesn't have to do a whole lot here
|
|||
void
|
||||
CBaseEntity::CBaseEntity(void)
|
||||
{
|
||||
|
||||
#ifdef SERVER
|
||||
/* don't call this function more than once per entity */
|
||||
if (identity == 1)
|
||||
return;
|
||||
|
||||
m_iPropData = -1;
|
||||
|
||||
/* Not in Deathmatch */
|
||||
if (spawnflags & 2048) {
|
||||
if (cvar("sv_playerslots") > 1) {
|
||||
|
@ -943,6 +965,10 @@ CBaseEntity::SetModel(string newModel)
|
|||
model = newModel;
|
||||
setmodel(this, newModel);
|
||||
SetSendFlags(BASEFL_CHANGED_MODELINDEX);
|
||||
|
||||
if (model && m_iPropData == -1) {
|
||||
PropData_ForModel(model);
|
||||
}
|
||||
}
|
||||
void
|
||||
CBaseEntity::SetModelindex(float newModelIndex)
|
||||
|
@ -1103,6 +1129,9 @@ CBaseEntity::SpawnKey(string strKey, string strValue)
|
|||
/* we do re-read a lot of the builtin fields in case we want to set
|
||||
defaults. just in case anybody is wondering. */
|
||||
switch (strKey) {
|
||||
case "propdata":
|
||||
PropData_SetStage(strValue);
|
||||
break;
|
||||
case "scale":
|
||||
scale = stof(strValue);
|
||||
break;
|
||||
|
|
|
@ -427,6 +427,7 @@ initents(void)
|
|||
|
||||
/* sound shader init */
|
||||
Sound_Init();
|
||||
PropData_Init();
|
||||
|
||||
/* only bother doing so on Half-Life BSP */
|
||||
if (serverkeyfloat("*bspversion") == BSPVER_HL) {
|
||||
|
|
|
@ -39,6 +39,8 @@ Event_ServerModelEvent(float flTimeStamp, int iCode, string strData)
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 1004:
|
||||
break;
|
||||
default:
|
||||
dprint(sprintf("^3[SERVER]^7 Unknown model-event code " \
|
||||
"%i with data %s\n", iCode, strData));
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include "memory.h"
|
||||
#include "spectator.h"
|
||||
#include "platform.h"
|
||||
#include "propdata.h"
|
||||
#include "vehicles.h"
|
||||
|
||||
#define BSPVER_PREREL 28
|
||||
|
|
|
@ -6,5 +6,6 @@ sound.qc
|
|||
math.qc
|
||||
player.qc
|
||||
player_pmove.qc
|
||||
propdata.qc
|
||||
vehicles.qc
|
||||
#endlist
|
||||
|
|
126
src/shared/propdata.h
Normal file
126
src/shared/propdata.h
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (c) 2016-2021 Marco Hladik <marco@icculus.org>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
Prop-Data specs
|
||||
|
||||
According to Source SDK's propdata.txt we've got
|
||||
a kinda sorta hacky definition table for this stuff:
|
||||
|
||||
"PropData.txt"
|
||||
{
|
||||
|
||||
"sometype"
|
||||
{
|
||||
// .. key/field attributes
|
||||
// e.g.
|
||||
"health" "10"
|
||||
"breakable_model" "somematerial"
|
||||
}
|
||||
|
||||
"BreakableModels"
|
||||
{
|
||||
// completely unrelated to types defined in propdata.txt
|
||||
// but somehow still part of it?
|
||||
|
||||
"somematerial"
|
||||
{
|
||||
// model path / fadeout time pair
|
||||
// e.g.
|
||||
"foo.vvm" "2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
The idea is that props specify the type of prop they are ("sometype") and it defines
|
||||
a set of sensible defaults.
|
||||
|
||||
However, props can override any parts of this inside the model data itself.
|
||||
Currently no model format FTEQW supports allows for reading of said propdata.
|
||||
However we'll be loading "foobar.vvm.propdata" to remedy this for those.
|
||||
*/
|
||||
|
||||
var string g_curPropData;
|
||||
|
||||
typedef enumflags
|
||||
{
|
||||
PDFL_BLOCKLOS, /* Does this block an AIs light of sight? */
|
||||
PDFL_AIWALKABLE, /* can AI walk on this? */
|
||||
PDFL_ALLOWSTATIC /* static simulation possible? */
|
||||
} propdataFlag_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
string name;
|
||||
string base;
|
||||
float health; /* health until break */
|
||||
propdataFlag_t flags;
|
||||
float damage_bullets; /* dmg multipliers */
|
||||
float damage_melee;
|
||||
float damage_explosive;
|
||||
float explosive_damage; /* once the damage/radius keys are set, make explosion upon break */
|
||||
float explosive_radius;
|
||||
string breakable_model; /* name of BreakableModels entry in PropData.txt */
|
||||
int breakable_count;
|
||||
} propdata_t;
|
||||
|
||||
/* entity will have to have a .propdata field pointing to a propdata id */
|
||||
propdata_t *g_propdata;
|
||||
int g_propdata_count;
|
||||
var hashtable g_hashpropdata;
|
||||
|
||||
/* necessary API functions */
|
||||
void PropData_Init(void);
|
||||
void PropData_Shutdown(void);
|
||||
|
||||
int PropData_Load(string); /* called when we read entity data, returns -1 when not found */
|
||||
int PropData_ForModel(string); /* called when we set a model, returns -1 when not found */
|
||||
//int PropData_Read(string); /* this just handles the contents of a prop_data model string */
|
||||
|
||||
void PropData_SetStage(string);
|
||||
void PropData_Finish(void);
|
||||
|
||||
/* querying API */
|
||||
typedef enum
|
||||
{
|
||||
PROPINFO_HEALTH,
|
||||
PROPINFO_FLAGS,
|
||||
PROPINFO_DMG_BULLET,
|
||||
PROPINFO_DMG_MELEE,
|
||||
PROPINFO_DMG_EXPLOSIVE,
|
||||
PROPINFO_EXPLOSIVE_DMG,
|
||||
PROPINFO_EXPLOSIVE_RADIUS,
|
||||
PROPINFO_BREAKMODEL,
|
||||
PROPINFO_BREAKCOUNT
|
||||
} propinfo_t;
|
||||
__variant Prop_GetInfo(int, int);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
string model;
|
||||
float fadetime;
|
||||
} breakmodel_t;
|
||||
|
||||
/* entity will have a .breakmodel field pointing to a breakmodel id */
|
||||
propdata_t *g_breakmodel;
|
||||
int g_breakmodel_count;
|
||||
|
||||
/* necessary API functions */
|
||||
//void BreakModel_Init(void);
|
||||
//void BreakModel_Shutdown(void);
|
||||
|
||||
//int BreakModel_Load(string); /* called when we precache a model, returns -1 when not found */
|
||||
//int BreakModel_Read(string); /* this just handles the contents of a prop_data model string */
|
282
src/shared/propdata.qc
Normal file
282
src/shared/propdata.qc
Normal file
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* Copyright (c) 2016-2021 Marco Hladik <marco@icculus.org>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
void
|
||||
PropData_Shutdown(void)
|
||||
{
|
||||
if (g_propdata) {
|
||||
memfree(g_propdata);
|
||||
}
|
||||
|
||||
g_propdata_count = 0;
|
||||
g_hashpropdata = 0;
|
||||
}
|
||||
|
||||
void
|
||||
PropData_Init(void)
|
||||
{
|
||||
PropData_Shutdown();
|
||||
}
|
||||
|
||||
__variant
|
||||
Prop_GetInfo(int i, int type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PROPINFO_HEALTH:
|
||||
return (__variant)g_propdata[i].health;
|
||||
case PROPINFO_FLAGS:
|
||||
return (__variant)g_propdata[i].flags;
|
||||
case PROPINFO_DMG_BULLET:
|
||||
return (__variant)g_propdata[i].damage_bullets;
|
||||
case PROPINFO_DMG_MELEE:
|
||||
return (__variant)g_propdata[i].damage_melee;
|
||||
case PROPINFO_DMG_EXPLOSIVE:
|
||||
return (__variant)g_propdata[i].damage_explosive;
|
||||
case PROPINFO_EXPLOSIVE_DMG:
|
||||
return (__variant)g_propdata[i].explosive_damage;
|
||||
case PROPINFO_EXPLOSIVE_RADIUS:
|
||||
return (__variant)g_propdata[i].explosive_radius;
|
||||
case PROPINFO_BREAKMODEL:
|
||||
return (__variant)g_propdata[i].breakable_model;
|
||||
case PROPINFO_BREAKCOUNT:
|
||||
return (__variant)g_propdata[i].breakable_count;
|
||||
default:
|
||||
return __NULL__;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PropData_ParseField(int i, int a)
|
||||
{
|
||||
switch (argv(0)) {
|
||||
case "base":
|
||||
g_propdata[i].base = argv(1);
|
||||
break;
|
||||
case "blockLOS":
|
||||
g_propdata[i].flags |= PDFL_BLOCKLOS;
|
||||
break;
|
||||
case "AIWalkable":
|
||||
g_propdata[i].flags |= PDFL_AIWALKABLE;
|
||||
break;
|
||||
case "allow_static":
|
||||
g_propdata[i].flags |= PDFL_ALLOWSTATIC;
|
||||
break;
|
||||
case "dmg.bullets":
|
||||
g_propdata[i].damage_bullets = stof(argv(1));
|
||||
break;
|
||||
case "dmg.club":
|
||||
g_propdata[i].damage_melee = stof(argv(1));
|
||||
break;
|
||||
case "dmg.explosive":
|
||||
g_propdata[i].damage_explosive = stof(argv(1));
|
||||
break;
|
||||
case "health":
|
||||
g_propdata[i].health = stof(argv(1));
|
||||
break;
|
||||
case "explosive_damage":
|
||||
g_propdata[i].explosive_damage = stof(argv(1));
|
||||
break;
|
||||
case "explosive_radius":
|
||||
g_propdata[i].explosive_radius = stof(argv(1));
|
||||
break;
|
||||
case "breakable_model":
|
||||
g_propdata[i].breakable_model = argv(1);
|
||||
break;
|
||||
case "breakable_count":
|
||||
g_propdata[i].breakable_count = stof(argv(1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* concerned with dealing with keeping track of braces and parsing lines */
|
||||
int
|
||||
PropData_Parse(int i, string line, string type)
|
||||
{
|
||||
int c;
|
||||
string key;
|
||||
static string t_name;
|
||||
static int braced = 0;
|
||||
|
||||
c = tokenize_console(line);
|
||||
key = argv(0);
|
||||
|
||||
switch(key) {
|
||||
case "{":
|
||||
braced++;
|
||||
break;
|
||||
case "}":
|
||||
braced--;
|
||||
t_name = "";
|
||||
|
||||
/* done */
|
||||
if (braced == 0)
|
||||
return (1);
|
||||
break;
|
||||
default:
|
||||
if (braced == 2 && t_name != "") {
|
||||
PropData_ParseField(i, c);
|
||||
} else if (braced == 1) {
|
||||
/* name/identifer of our message */
|
||||
t_name = strtolower(key);
|
||||
|
||||
if (t_name == type) {
|
||||
/* I guess it's what we want */
|
||||
g_propdata[i].name = type;
|
||||
} else {
|
||||
/* not what we're looking for */
|
||||
t_name = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return (0);
|
||||
}
|
||||
|
||||
/* goes through and looks for a specifically named propdata type inside the scripts dir */
|
||||
int
|
||||
PropData_Load(string type)
|
||||
{
|
||||
searchhandle sh;
|
||||
filestream fh;
|
||||
string line;
|
||||
int index;
|
||||
|
||||
if (!type)
|
||||
return -1;
|
||||
|
||||
index = g_propdata_count;
|
||||
type = strtolower(type);
|
||||
|
||||
/* create the hash-table if it doesn't exist */
|
||||
if (!g_hashpropdata) {
|
||||
g_hashpropdata = hash_createtab(2, HASH_ADD);
|
||||
}
|
||||
|
||||
/* check if it's already cached */
|
||||
for (int i = 0; i < g_propdata_count; i++) {
|
||||
if (type == g_propdata[i].name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
g_propdata_count++;
|
||||
g_propdata = (propdata_t *)memrealloc(g_propdata, sizeof(propdata_t), index, g_propdata_count);
|
||||
|
||||
/* Defaults go here */
|
||||
|
||||
sh = search_begin("scripts/propdata*.txt", TRUE, TRUE);
|
||||
|
||||
for (int i = 0; i < search_getsize(sh); i++) {
|
||||
fh = fopen(search_getfilename(sh, i), FILE_READ);
|
||||
if (fh < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while ((line = fgets(fh))) {
|
||||
/* when we found it, quit */
|
||||
if (PropData_Parse(index, line, type) == TRUE) {
|
||||
search_end(sh);
|
||||
fclose(fh);
|
||||
hash_add(g_hashpropdata, type, (int)index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
fclose(fh);
|
||||
}
|
||||
|
||||
print("^1[PROPDATA] No type found for ");
|
||||
print(type);
|
||||
print("\n");
|
||||
|
||||
search_end(sh);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* goes through and looks for a specifically named propdata type inside the scripts dir */
|
||||
int
|
||||
PropData_ForModel(string modelname)
|
||||
{
|
||||
filestream fh;
|
||||
string line;
|
||||
int index;
|
||||
|
||||
if (!modelname)
|
||||
return -1;
|
||||
|
||||
if (substring(modelname, 0, 1) == "*")
|
||||
return -1;
|
||||
|
||||
index = g_propdata_count;
|
||||
modelname = strtolower(modelname);
|
||||
|
||||
dprint("[PROPDATA] Loading model propdata ");
|
||||
dprint(modelname);
|
||||
dprint("\n");
|
||||
|
||||
/* create the hash-table if it doesn't exist */
|
||||
if (!g_hashpropdata) {
|
||||
g_hashpropdata = hash_createtab(2, HASH_ADD);
|
||||
}
|
||||
|
||||
/* check if it's already cached */
|
||||
for (int i = 0; i < g_propdata_count; i++) {
|
||||
if (modelname == g_propdata[i].name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
g_propdata_count++;
|
||||
g_propdata = (propdata_t *)memrealloc(g_propdata, sizeof(propdata_t), index, g_propdata_count);
|
||||
|
||||
/* Defaults go here */
|
||||
|
||||
fh = fopen(strcat(modelname, ".propdata"), FILE_READ);
|
||||
if (fh < 0) {
|
||||
dprint(sprintf("Can't find propdata for model %s\n", modelname));
|
||||
return -1;
|
||||
}
|
||||
while ((line = fgets(fh))) {
|
||||
/* when we found it, quit */
|
||||
if (PropData_Parse(index, line, "prop_data") == TRUE) {
|
||||
fclose(fh);
|
||||
hash_add(g_hashpropdata, modelname, (int)index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
fclose(fh);
|
||||
|
||||
dprint("^1[PROPDATA] No type found for ");
|
||||
dprint(modelname);
|
||||
dprint("\n");
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* we can only tokenize one thing at a time, so we save the type for the current
|
||||
entity away for later, so we can parse it properly by then when we've exited the
|
||||
SpawnKey loop. Using a global will save us some memory at least */
|
||||
void
|
||||
PropData_SetStage(string type)
|
||||
{
|
||||
g_curPropData = type;
|
||||
}
|
||||
|
||||
void
|
||||
PropData_Finish(void)
|
||||
{
|
||||
PropData_Load(g_curPropData);
|
||||
g_curPropData = "";
|
||||
}
|
Loading…
Reference in a new issue