mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-16 18:19:55 +00:00
Merge remote branch 'swick/initocfg'
This commit is contained in:
commit
1201c271b2
8 changed files with 147 additions and 40 deletions
|
@ -12,12 +12,22 @@ MwIniImporter::MwIniImporter() {
|
||||||
const char *map[][2] =
|
const char *map[][2] =
|
||||||
{
|
{
|
||||||
{ "fps", "General:Show FPS" },
|
{ "fps", "General:Show FPS" },
|
||||||
|
{ "nosound", "General:Disable Audio" },
|
||||||
{ 0, 0 }
|
{ 0, 0 }
|
||||||
};
|
};
|
||||||
|
const char *fallback[] = {
|
||||||
|
"Weather:Sunrise Time",
|
||||||
|
"Weather:Sunset Time",
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
for(int i=0; map[i][0]; i++) {
|
for(int i=0; map[i][0]; i++) {
|
||||||
mMergeMap.insert(std::make_pair<std::string, std::string>(map[i][0], map[i][1]));
|
mMergeMap.insert(std::make_pair<std::string, std::string>(map[i][0], map[i][1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(int i=0; fallback[i]; i++) {
|
||||||
|
mMergeFallback.push_back(fallback[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MwIniImporter::setVerbose(bool verbose) {
|
void MwIniImporter::setVerbose(bool verbose) {
|
||||||
|
@ -116,16 +126,38 @@ void MwIniImporter::merge(multistrmap &cfg, multistrmap &ini) {
|
||||||
multistrmap::iterator iniIt;
|
multistrmap::iterator iniIt;
|
||||||
for(strmap::iterator it=mMergeMap.begin(); it!=mMergeMap.end(); it++) {
|
for(strmap::iterator it=mMergeMap.begin(); it!=mMergeMap.end(); it++) {
|
||||||
if((iniIt = ini.find(it->second)) != ini.end()) {
|
if((iniIt = ini.find(it->second)) != ini.end()) {
|
||||||
cfg.erase(it->first);
|
for(std::vector<std::string>::iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); vc++) {
|
||||||
if(!this->specialMerge(it->first, it->second, cfg, ini)) {
|
cfg.erase(it->first);
|
||||||
cfg.insert(std::make_pair<std::string, std::vector<std::string> >(it->first, iniIt->second));
|
insertMultistrmap(cfg, it->first, *vc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MwIniImporter::specialMerge(std::string cfgKey, std::string iniKey, multistrmap &cfg, multistrmap &ini) {
|
void MwIniImporter::mergeFallback(multistrmap &cfg, multistrmap &ini) {
|
||||||
return false;
|
cfg.erase("fallback");
|
||||||
|
|
||||||
|
multistrmap::iterator cfgIt;
|
||||||
|
multistrmap::iterator iniIt;
|
||||||
|
for(std::vector<std::string>::iterator it=mMergeFallback.begin(); it!=mMergeFallback.end(); it++) {
|
||||||
|
if((iniIt = ini.find(*it)) != ini.end()) {
|
||||||
|
for(std::vector<std::string>::iterator vc = iniIt->second.begin(); vc != iniIt->second.end(); vc++) {
|
||||||
|
std::string value(*it);
|
||||||
|
std::replace( value.begin(), value.end(), ' ', '_' );
|
||||||
|
std::replace( value.begin(), value.end(), ':', '_' );
|
||||||
|
value.append(",").append(vc->substr(0,vc->length()-1));
|
||||||
|
insertMultistrmap(cfg, "fallback", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void MwIniImporter::insertMultistrmap(multistrmap &cfg, std::string key, std::string value) {
|
||||||
|
multistrmap::iterator it = cfg.find(key);
|
||||||
|
if(it == cfg.end()) {
|
||||||
|
cfg.insert(std::make_pair<std::string, std::vector<std::string> >(key, std::vector<std::string>() ));
|
||||||
|
}
|
||||||
|
cfg[key].push_back(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MwIniImporter::importGameFiles(multistrmap &cfg, multistrmap &ini) {
|
void MwIniImporter::importGameFiles(multistrmap &cfg, multistrmap &ini) {
|
||||||
|
|
|
@ -18,14 +18,16 @@ class MwIniImporter {
|
||||||
multistrmap loadIniFile(std::string filename);
|
multistrmap loadIniFile(std::string filename);
|
||||||
multistrmap loadCfgFile(std::string filename);
|
multistrmap loadCfgFile(std::string filename);
|
||||||
void merge(multistrmap &cfg, multistrmap &ini);
|
void merge(multistrmap &cfg, multistrmap &ini);
|
||||||
|
void mergeFallback(multistrmap &cfg, multistrmap &ini);
|
||||||
void importGameFiles(multistrmap &cfg, multistrmap &ini);
|
void importGameFiles(multistrmap &cfg, multistrmap &ini);
|
||||||
void writeToFile(boost::iostreams::stream<boost::iostreams::file_sink> &out, multistrmap &cfg);
|
void writeToFile(boost::iostreams::stream<boost::iostreams::file_sink> &out, multistrmap &cfg);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool specialMerge(std::string cfgKey, std::string iniKey, multistrmap &cfg, multistrmap &ini);
|
void insertMultistrmap(multistrmap &cfg, std::string key, std::string value);
|
||||||
std::string numberToString(int n);
|
std::string numberToString(int n);
|
||||||
bool mVerbose;
|
bool mVerbose;
|
||||||
strmap mMergeMap;
|
strmap mMergeMap;
|
||||||
|
std::vector<std::string> mMergeFallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ namespace bpo = boost::program_options;
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
bpo::options_description desc("Syntax: mwiniimporter <options>\nAllowed options");
|
bpo::options_description desc("Syntax: mwiniimporter <options> inifile configfile\nAllowed options");
|
||||||
|
bpo::positional_options_description p_desc;
|
||||||
desc.add_options()
|
desc.add_options()
|
||||||
("help,h", "produce help message")
|
("help,h", "produce help message")
|
||||||
("verbose,v", "verbose output")
|
("verbose,v", "verbose output")
|
||||||
|
@ -18,28 +19,22 @@ int main(int argc, char *argv[]) {
|
||||||
("output,o", bpo::value<std::string>()->default_value(""), "openmw.cfg file")
|
("output,o", bpo::value<std::string>()->default_value(""), "openmw.cfg file")
|
||||||
("game-files,g", "import esm and esp files")
|
("game-files,g", "import esm and esp files")
|
||||||
;
|
;
|
||||||
|
p_desc.add("ini", 1).add("cfg", 1);
|
||||||
|
|
||||||
bpo::variables_map vm;
|
bpo::variables_map vm;
|
||||||
try {
|
bpo::parsed_options parsed = bpo::command_line_parser(argc, argv)
|
||||||
bpo::store(boost::program_options::parse_command_line(argc, argv, desc), vm);
|
.options(desc)
|
||||||
|
.positional(p_desc)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
bpo::store(parsed, vm);
|
||||||
|
|
||||||
// parse help before calling notify because we dont want it to throw an error if help is set
|
if(vm.count("help") || !vm.count("ini") || !vm.count("cfg")) {
|
||||||
if(vm.count("help")) {
|
std::cout << desc;
|
||||||
std::cout << desc;
|
return 0;
|
||||||
return 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bpo::notify(vm);
|
bpo::notify(vm);
|
||||||
|
|
||||||
}
|
|
||||||
catch(std::exception& e) {
|
|
||||||
std::cerr << "Error:" << e.what() << std::endl;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
catch(...) {
|
|
||||||
std::cerr << "Error" << std::endl;
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string iniFile = vm["ini"].as<std::string>();
|
std::string iniFile = vm["ini"].as<std::string>();
|
||||||
std::string cfgFile = vm["cfg"].as<std::string>();
|
std::string cfgFile = vm["cfg"].as<std::string>();
|
||||||
|
@ -58,21 +53,22 @@ int main(int argc, char *argv[]) {
|
||||||
std::cerr << "cfg file does not exist" << std::endl;
|
std::cerr << "cfg file does not exist" << std::endl;
|
||||||
return -4;
|
return -4;
|
||||||
}
|
}
|
||||||
|
|
||||||
MwIniImporter importer;
|
MwIniImporter importer;
|
||||||
importer.setVerbose(vm.count("verbose"));
|
importer.setVerbose(vm.count("verbose"));
|
||||||
boost::iostreams::stream<boost::iostreams::file_sink> file(outputFile);
|
|
||||||
|
|
||||||
MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile);
|
MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile);
|
||||||
MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile);
|
MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile);
|
||||||
|
|
||||||
importer.merge(cfg, ini);
|
importer.merge(cfg, ini);
|
||||||
|
importer.mergeFallback(cfg, ini);
|
||||||
|
|
||||||
if(vm.count("game-files")) {
|
if(vm.count("game-files")) {
|
||||||
importer.importGameFiles(cfg, ini);
|
importer.importGameFiles(cfg, ini);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "write to: " << outputFile << std::endl;
|
std::cout << "write to: " << outputFile << std::endl;
|
||||||
|
boost::iostreams::stream<boost::iostreams::file_sink> file(outputFile);
|
||||||
importer.writeToFile(file, cfg);
|
importer.writeToFile(file, cfg);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -365,7 +365,7 @@ void OMW::Engine::go()
|
||||||
|
|
||||||
// Create the world
|
// Create the world
|
||||||
mEnvironment.mWorld = new MWWorld::World (*mOgre, mFileCollections, mMaster,
|
mEnvironment.mWorld = new MWWorld::World (*mOgre, mFileCollections, mMaster,
|
||||||
mResDir, mNewGame, mEnvironment, mEncoding);
|
mResDir, mNewGame, mEnvironment, mEncoding, mFallbackMap);
|
||||||
|
|
||||||
// Create window manager - this manages all the MW-specific GUI windows
|
// Create window manager - this manages all the MW-specific GUI windows
|
||||||
MWScript::registerExtensions (mExtensions);
|
MWScript::registerExtensions (mExtensions);
|
||||||
|
@ -535,3 +535,8 @@ void OMW::Engine::setEncoding(const std::string& encoding)
|
||||||
{
|
{
|
||||||
mEncoding = encoding;
|
mEncoding = encoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OMW::Engine::setFallbackValues(std::map<std::string,std::string> fallbackMap)
|
||||||
|
{
|
||||||
|
mFallbackMap = fallbackMap;
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ namespace OMW
|
||||||
bool mReportFocus;
|
bool mReportFocus;
|
||||||
float mFocusTDiff;
|
float mFocusTDiff;
|
||||||
std::string mFocusName;
|
std::string mFocusName;
|
||||||
|
std::map<std::string,std::string> mFallbackMap;
|
||||||
|
|
||||||
MWWorld::Environment mEnvironment;
|
MWWorld::Environment mEnvironment;
|
||||||
Compiler::Extensions mExtensions;
|
Compiler::Extensions mExtensions;
|
||||||
|
@ -163,6 +164,8 @@ namespace OMW
|
||||||
|
|
||||||
void setAnimationVerbose(bool animverbose);
|
void setAnimationVerbose(bool animverbose);
|
||||||
|
|
||||||
|
void setFallbackValues(std::map<std::string,std::string> map);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Files::ConfigurationManager& mCfgMgr;
|
Files::ConfigurationManager& mCfgMgr;
|
||||||
};
|
};
|
||||||
|
|
|
@ -54,6 +54,41 @@ inline boost::filesystem::path lexical_cast<boost::filesystem::path, std::string
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
|
struct FallbackMap {
|
||||||
|
std::map<std::string,std::string> mMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
void validate(boost::any &v, std::vector<std::string> const &tokens, FallbackMap*, int)
|
||||||
|
{
|
||||||
|
if(v.empty())
|
||||||
|
{
|
||||||
|
v = boost::any(FallbackMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
FallbackMap *map = boost::any_cast<FallbackMap>(&v);
|
||||||
|
|
||||||
|
std::map<std::string,std::string>::iterator mapIt;
|
||||||
|
for(std::vector<std::string>::const_iterator it=tokens.begin(); it != tokens.end(); it++)
|
||||||
|
{
|
||||||
|
int sep = it->find(",");
|
||||||
|
if(sep < 1 || sep == (int)it->length()-1)
|
||||||
|
#if (BOOST_VERSION < 104200)
|
||||||
|
throw boost::program_options::validation_error("invalid value");
|
||||||
|
#else
|
||||||
|
throw boost::program_options::validation_error(boost::program_options::validation_error::invalid_option_value);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string key(it->substr(0,sep));
|
||||||
|
std::string value(it->substr(sep+1));
|
||||||
|
|
||||||
|
if((mapIt = map->mMap.find(key)) == map->mMap.end())
|
||||||
|
{
|
||||||
|
map->mMap.insert(std::make_pair<std::string,std::string>(key,value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Parses application command line and calls \ref Cfg::ConfigurationManager
|
* \brief Parses application command line and calls \ref Cfg::ConfigurationManager
|
||||||
* to parse configuration files.
|
* to parse configuration files.
|
||||||
|
@ -92,36 +127,40 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
|
||||||
("plugin", bpo::value<StringsVector>()->default_value(StringsVector(), "")
|
("plugin", bpo::value<StringsVector>()->default_value(StringsVector(), "")
|
||||||
->multitoken(), "plugin file(s)")
|
->multitoken(), "plugin file(s)")
|
||||||
|
|
||||||
("anim-verbose", boost::program_options::value<bool>()->implicit_value(true)
|
("anim-verbose", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "output animation indices files")
|
->default_value(false), "output animation indices files")
|
||||||
|
|
||||||
("debug", boost::program_options::value<bool>()->implicit_value(true)
|
("debug", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "debug mode")
|
->default_value(false), "debug mode")
|
||||||
|
|
||||||
("nosound", boost::program_options::value<bool>()->implicit_value(true)
|
("nosound", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "disable all sounds")
|
->default_value(false), "disable all sounds")
|
||||||
|
|
||||||
("script-verbose", boost::program_options::value<bool>()->implicit_value(true)
|
("script-verbose", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "verbose script output")
|
->default_value(false), "verbose script output")
|
||||||
|
|
||||||
("new-game", boost::program_options::value<bool>()->implicit_value(true)
|
("new-game", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "activate char gen/new game mechanics")
|
->default_value(false), "activate char gen/new game mechanics")
|
||||||
|
|
||||||
("script-all", boost::program_options::value<bool>()->implicit_value(true)
|
("script-all", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "compile all scripts (excluding dialogue scripts) at startup")
|
->default_value(false), "compile all scripts (excluding dialogue scripts) at startup")
|
||||||
|
|
||||||
("fs-strict", boost::program_options::value<bool>()->implicit_value(true)
|
("fs-strict", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "strict file system handling (no case folding)")
|
->default_value(false), "strict file system handling (no case folding)")
|
||||||
|
|
||||||
( "encoding", boost::program_options::value<std::string>()->
|
( "encoding", bpo::value<std::string>()->
|
||||||
default_value("win1252"),
|
default_value("win1252"),
|
||||||
"Character encoding used in OpenMW game messages:\n"
|
"Character encoding used in OpenMW game messages:\n"
|
||||||
"\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n"
|
"\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n"
|
||||||
"\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n"
|
"\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n"
|
||||||
"\n\twin1252 - Western European (Latin) alphabet, used by default")
|
"\n\twin1252 - Western European (Latin) alphabet, used by default")
|
||||||
|
|
||||||
("report-focus", boost::program_options::value<bool>()->implicit_value(true)
|
("report-focus", bpo::value<bool>()->implicit_value(true)
|
||||||
->default_value(false), "write name of focussed object to cout")
|
->default_value(false), "write name of focussed object to cout")
|
||||||
|
|
||||||
|
("fallback", bpo::value<FallbackMap>()->default_value(FallbackMap(), "")
|
||||||
|
->multitoken()->composing(), "fallback values")
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv)
|
bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv)
|
||||||
|
@ -228,6 +267,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
|
||||||
engine.setCompileAll(variables["script-all"].as<bool>());
|
engine.setCompileAll(variables["script-all"].as<bool>());
|
||||||
engine.setReportFocus(variables["report-focus"].as<bool>());
|
engine.setReportFocus(variables["report-focus"].as<bool>());
|
||||||
engine.setAnimationVerbose(variables["anim-verbose"].as<bool>());
|
engine.setAnimationVerbose(variables["anim-verbose"].as<bool>());
|
||||||
|
engine.setFallbackValues(variables["fallback"].as<FallbackMap>().mMap);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,10 +152,30 @@ namespace MWWorld
|
||||||
mRendering->skyDisable();
|
mRendering->skyDisable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void World::setFallbackValues(std::map<std::string,std::string> fallbackMap)
|
||||||
|
{
|
||||||
|
mFallback = fallbackMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string World::getFallback(std::string key)
|
||||||
|
{
|
||||||
|
return getFallback(key, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string World::getFallback(std::string key, std::string def)
|
||||||
|
{
|
||||||
|
std::map<std::string,std::string>::iterator it;
|
||||||
|
if((it = mFallback.find(key)) == mFallback.end())
|
||||||
|
{
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
World::World (OEngine::Render::OgreRenderer& renderer,
|
World::World (OEngine::Render::OgreRenderer& renderer,
|
||||||
const Files::Collections& fileCollections,
|
const Files::Collections& fileCollections,
|
||||||
const std::string& master, const boost::filesystem::path& resDir,
|
const std::string& master, const boost::filesystem::path& resDir,
|
||||||
bool newGame, Environment& environment, const std::string& encoding)
|
bool newGame, Environment& environment, const std::string& encoding, std::map<std::string,std::string> fallbackMap)
|
||||||
: mPlayer (0), mLocalScripts (mStore), mGlobalVariables (0),
|
: mPlayer (0), mLocalScripts (mStore), mGlobalVariables (0),
|
||||||
mSky (true), mEnvironment (environment), mNextDynamicRecord (0), mCells (mStore, mEsm, *this),
|
mSky (true), mEnvironment (environment), mNextDynamicRecord (0), mCells (mStore, mEsm, *this),
|
||||||
mNumFacing(0)
|
mNumFacing(0)
|
||||||
|
@ -191,6 +211,8 @@ namespace MWWorld
|
||||||
|
|
||||||
mWorldScene = new Scene(environment, this, *mRendering, mPhysics);
|
mWorldScene = new Scene(environment, this, *mRendering, mPhysics);
|
||||||
|
|
||||||
|
setFallbackValues(fallbackMap);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,7 @@ namespace MWWorld
|
||||||
std::string mFaced1Name;
|
std::string mFaced1Name;
|
||||||
std::string mFaced2Name;
|
std::string mFaced2Name;
|
||||||
int mNumFacing;
|
int mNumFacing;
|
||||||
|
std::map<std::string,std::string> mFallback;
|
||||||
|
|
||||||
int getDaysPerMonth (int month) const;
|
int getDaysPerMonth (int month) const;
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ namespace MWWorld
|
||||||
World (OEngine::Render::OgreRenderer& renderer,
|
World (OEngine::Render::OgreRenderer& renderer,
|
||||||
const Files::Collections& fileCollections,
|
const Files::Collections& fileCollections,
|
||||||
const std::string& master, const boost::filesystem::path& resDir, bool newGame,
|
const std::string& master, const boost::filesystem::path& resDir, bool newGame,
|
||||||
Environment& environment, const std::string& encoding);
|
Environment& environment, const std::string& encoding, std::map<std::string,std::string> fallbackMap);
|
||||||
|
|
||||||
~World();
|
~World();
|
||||||
|
|
||||||
|
@ -125,6 +126,12 @@ namespace MWWorld
|
||||||
|
|
||||||
void adjustSky();
|
void adjustSky();
|
||||||
|
|
||||||
|
void setFallbackValues(std::map<std::string,std::string> fallbackMap);
|
||||||
|
|
||||||
|
std::string getFallback(std::string key);
|
||||||
|
|
||||||
|
std::string getFallback(std::string key, std::string def);
|
||||||
|
|
||||||
MWWorld::Player& getPlayer();
|
MWWorld::Player& getPlayer();
|
||||||
|
|
||||||
const ESMS::ESMStore& getStore() const;
|
const ESMS::ESMStore& getStore() const;
|
||||||
|
|
Loading…
Reference in a new issue