Merge remote branch 'upstream/master'

actorid
Armin Preiml 15 years ago
commit 927879d23e

@ -41,6 +41,14 @@ set(GAMEGUI
)
source_group(apps\\openmw\\mwgui FILES ${GAMEGUI_HEADER} ${GAMEGUI})
set(GAMEDIALOGUE_HEADER
mwdialogue/dialoguemanager.hpp
)
set(GAMEDIALOGUE
mwdialogue/dialoguemanager.cpp
)
source_group(apps\\openmw\\mwdialogue FILES ${GAMEDIALOGUE_HEADER} ${GAMEDIALOGUE})
set(GAMESCRIPT
mwscript/scriptmanager.cpp
mwscript/compilercontext.cpp
@ -97,6 +105,7 @@ set(GAMEWORLD_HEADER
mwworld/action.hpp
mwworld/nullaction.hpp
mwworld/actionteleport.hpp
mwworld/containerstore.hpp
mwworld/actiontalk.hpp
mwworld/actiontake.hpp
mwworld/containerstore.hpp
@ -164,11 +173,11 @@ set(GAMEMECHANICS_HEADER
source_group(apps\\openmw\\mwmechanics FILES ${GAMEMECHANICS} ${GAMEMECHANICS_HEADER})
set(OPENMW_CPP ${GAME} ${GAMEREND} ${GAMEINPUT} ${GAMESCRIPT} ${GAMESOUND} ${GAMEGUI} ${GAMEWORLD}
${GAMECLASS} ${GAMEMECHANICS}
${GAMECLASS} ${GAMEMECHANICS} ${GAMEDIALOGUE}
)
set(OPENMW_HEADER ${GAME_HEADER} ${GAMEREND_HEADER} ${GAMEINPUT_HEADER} ${GAMESCRIPT_HEADER}
${GAMESOUND_HEADER} ${GAMEGUI_HEADER} ${GAMEWORLD_HEADER} ${GAMECLASS_HEADER}
${GAMEMECHANICS_HEADER}
${GAMEMECHANICS_HEADER} ${GAMEDIALOG_HEADERUE}
)
# Main executable

@ -28,6 +28,8 @@
#include "mwclass/classes.hpp"
#include "mwdialogue/dialoguemanager.hpp"
#include "mwmechanics/mechanicsmanager.hpp"
#include <OgreRoot.h>
@ -104,6 +106,7 @@ OMW::Engine::Engine()
: mDebug (false)
, mVerboseScripts (false)
, mNewGame (false)
, mUseSound (true)
, mScriptManager (0)
, mScriptContext (0)
{
@ -117,6 +120,7 @@ OMW::Engine::~Engine()
delete mEnvironment.mSoundManager;
delete mEnvironment.mGlobalScripts;
delete mEnvironment.mMechanicsManager;
delete mEnvironment.mDialogueManager;
delete mScriptManager;
delete mScriptContext;
}
@ -236,7 +240,8 @@ void OMW::Engine::go()
mEnvironment.mSoundManager = new MWSound::SoundManager(mOgre.getRoot(),
mOgre.getCamera(),
mEnvironment.mWorld->getStore(),
(mDataDir / "Sound").file_string());
(mDataDir / "Sound").file_string(),
mUseSound);
// Create script system
mScriptContext = new MWScript::CompilerContext (MWScript::CompilerContext::Type_Full,
@ -253,6 +258,9 @@ void OMW::Engine::go()
mEnvironment.mMechanicsManager = new MWMechanics::MechanicsManager (
mEnvironment.mWorld->getStore(), *mEnvironment.mWindowManager);
// Create dialog system
mEnvironment.mDialogueManager = new MWDialogue::DialogueManager (mEnvironment);
// load cell
ESM::Position pos;
pos.pos[0] = pos.pos[1] = pos.pos[2] = 0;

@ -59,6 +59,7 @@ namespace OMW
bool mDebug;
bool mVerboseScripts;
bool mNewGame;
bool mUseSound;
MWWorld::Environment mEnvironment;
MWScript::ScriptManager *mScriptManager;
@ -115,6 +116,9 @@ namespace OMW
/// Enable verbose script output
void enableVerboseScripts();
/// Disable all sound
void disableSound() { mUseSound = false; }
/// Start as a new game.
void setNewGame();

@ -31,6 +31,7 @@ bool parseOptions (int argc, char**argv, OMW::Engine& engine)
("master", bpo::value<std::string>()->default_value ("Morrowind"),
"master file")
( "debug", "debug mode" )
( "nosound", "disable all sound" )
( "script-verbose", "verbose script output" )
( "new-game", "activate char gen/new game mechanics" )
;
@ -64,6 +65,9 @@ bool parseOptions (int argc, char**argv, OMW::Engine& engine)
if (variables.count ("debug"))
engine.enableDebugMode();
if (variables.count ("nosound"))
engine.disableSound();
if (variables.count ("script-verbose"))
engine.enableVerboseScripts();

@ -15,6 +15,14 @@
namespace MWClass
{
std::string Creature::getId (const MWWorld::Ptr& ptr) const
{
ESMS::LiveCellRef<ESM::Creature, MWWorld::RefData> *ref =
ptr.get<ESM::Creature>();
return ref->base->mId;
}
void Creature::insertObj (const MWWorld::Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const
{
@ -96,7 +104,7 @@ namespace MWClass
}
return *ptr.getRefData().getContainerStore();
}
}
std::string Creature::getScript (const MWWorld::Ptr& ptr) const
{

@ -9,6 +9,9 @@ namespace MWClass
{
public:
virtual std::string getId (const MWWorld::Ptr& ptr) const;
///< Return ID of \a ptr
virtual void insertObj (const MWWorld::Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const;
///< Add reference into a cell for rendering

@ -16,6 +16,14 @@
namespace MWClass
{
std::string Npc::getId (const MWWorld::Ptr& ptr) const
{
ESMS::LiveCellRef<ESM::NPC, MWWorld::RefData> *ref =
ptr.get<ESM::NPC>();
return ref->base->mId;
}
void Npc::insertObj (const MWWorld::Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const
{

@ -9,6 +9,9 @@ namespace MWClass
{
public:
virtual std::string getId (const MWWorld::Ptr& ptr) const;
///< Return ID of \a ptr
virtual void insertObj (const MWWorld::Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const;
///< Add reference into a cell for rendering
@ -26,14 +29,14 @@ namespace MWClass
virtual MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const;
///< Return creature stats
virtual boost::shared_ptr<MWWorld::Action> activate (const MWWorld::Ptr& ptr,
const MWWorld::Ptr& actor, const MWWorld::Environment& environment) const;
///< Generate action for activation
virtual MWWorld::ContainerStore<MWWorld::RefData>& getContainerStore (
const MWWorld::Ptr& ptr) const;
///< Return container store
virtual boost::shared_ptr<MWWorld::Action> activate (const MWWorld::Ptr& ptr,
const MWWorld::Ptr& actor, const MWWorld::Environment& environment) const;
///< Generate action for activation
virtual std::string getScript (const MWWorld::Ptr& ptr) const;
///< Return name of the script attached to ptr

@ -0,0 +1,281 @@
#include "dialoguemanager.hpp"
#include <cctype>
#include <algorithm>
#include <iterator>
#include <components/esm/loaddial.hpp>
#include <components/esm_store/store.hpp>
#include "../mwworld/class.hpp"
#include "../mwworld/environment.hpp"
#include "../mwworld/world.hpp"
#include "../mwworld/refdata.hpp"
#include "../mwgui/window_manager.hpp"
#include <iostream>
namespace
{
std::string toLower (const std::string& name)
{
std::string lowerCase;
std::transform (name.begin(), name.end(), std::back_inserter (lowerCase),
(int(*)(int)) std::tolower);
return lowerCase;
}
template<typename T1, typename T2>
bool selectCompare (char comp, T1 value1, T2 value2)
{
switch (comp)
{
case '0': return value1==value2;
case '1': return value1!=value2;
case '2': return value1>value2;
case '3': return value1>=value2;
case '4': return value1<value2;
case '5': return value1<=value2;
}
throw std::runtime_error ("unknown compare type in dialogue info select");
}
template<typename T>
bool checkLocal (char comp, const std::string& name, T value, const MWWorld::Ptr& actor,
const ESMS::ESMStore& store)
{
std::string scriptName = MWWorld::Class::get (actor).getScript (actor);
if (scriptName.empty())
return false; // no script
const ESM::Script *script = store.scripts.find (scriptName);
int i = 0;
for (; i<static_cast<int> (script->varNames.size()); ++i)
if (script->varNames[i]==name)
break;
if (i>=static_cast<int> (script->varNames.size()))
return false; // script does not have a variable of this name
const MWScript::Locals& locals = actor.getRefData().getLocals();
if (i<script->data.numShorts)
return selectCompare (comp, locals.mShorts[i], value);
else
i -= script->data.numShorts;
if (i<script->data.numLongs)
return selectCompare (comp, locals.mLongs[i], value);
else
i -= script->data.numShorts;
return selectCompare (comp, locals.mFloats.at (i), value);
}
template<typename T>
bool checkGlobal (char comp, const std::string& name, T value, MWWorld::World& world)
{
switch (world.getGlobalVariableType (name))
{
case 's':
return selectCompare (comp, value, world.getGlobalVariable (name).mShort);
case 'l':
return selectCompare (comp, value, world.getGlobalVariable (name).mLong);
case 'f':
return selectCompare (comp, value, world.getGlobalVariable (name).mFloat);
case ' ':
world.getGlobalVariable (name); // trigger exception
break;
default:
throw std::runtime_error ("unsupported gobal variable type");
}
return false;
}
}
namespace MWDialogue
{
bool DialogueManager::isMatching (const MWWorld::Ptr& actor,
const ESM::DialInfo::SelectStruct& select) const
{
char type = select.selectRule[1];
if (type!='0')
{
char comp = select.selectRule[4];
std::string name = select.selectRule.substr (5);
// TODO types 4, 5, 6, 7, 8, 9, A, B, C
switch (type)
{
case '1': // function
return false; // TODO implement functions
case '2': // global
if (select.type==ESM::VT_Short || select.type==ESM::VT_Int ||
select.type==ESM::VT_Long)
{
if (!checkGlobal (comp, toLower (name), select.i, *mEnvironment.mWorld))
return false;
}
else if (select.type==ESM::VT_Float)
{
if (!checkGlobal (comp, toLower (name), select.f, *mEnvironment.mWorld))
return false;
}
else
throw std::runtime_error (
"unsupported variable type in dialogue info select");
return true;
case '3': // local
if (select.type==ESM::VT_Short || select.type==ESM::VT_Int ||
select.type==ESM::VT_Long)
{
if (!checkLocal (comp, toLower (name), select.i, actor,
mEnvironment.mWorld->getStore()))
return false;
}
else if (select.type==ESM::VT_Float)
{
if (!checkLocal (comp, toLower (name), select.f, actor,
mEnvironment.mWorld->getStore()))
return false;
}
else
throw std::runtime_error (
"unsupported variable type in dialogue info select");
return true;
default:
std::cout << "unchecked select: " << type << " " << comp << " " << name << std::endl;
}
}
return true;
}
bool DialogueManager::isMatching (const MWWorld::Ptr& actor, const ESM::DialInfo& info) const
{
// actor id
if (!info.actor.empty())
if (toLower (info.actor)!=MWWorld::Class::get (actor).getId (actor))
return false;
if (!info.race.empty())
{
ESMS::LiveCellRef<ESM::NPC, MWWorld::RefData> *cellRef = actor.get<ESM::NPC>();
if (!cellRef)
return false;
if (toLower (info.race)!=toLower (cellRef->base->race))
return false;
}
if (!info.clas.empty())
{
ESMS::LiveCellRef<ESM::NPC, MWWorld::RefData> *cellRef = actor.get<ESM::NPC>();
if (!cellRef)
return false;
if (toLower (info.clas)!=toLower (cellRef->base->cls))
return false;
}
if (!info.npcFaction.empty())
{
ESMS::LiveCellRef<ESM::NPC, MWWorld::RefData> *cellRef = actor.get<ESM::NPC>();
if (!cellRef)
return false;
if (toLower (info.npcFaction)!=toLower (cellRef->base->faction))
return false;
}
// TODO check player faction
// check cell
if (!info.cell.empty())
if (mEnvironment.mWorld->getPlayerPos().getPlayer().getCell()->cell->name != info.cell)
return false;
// TODO check DATAstruct
for (std::vector<ESM::DialInfo::SelectStruct>::const_iterator iter (info.selects.begin());
iter != info.selects.end(); ++iter)
if (!isMatching (actor, *iter))
return false;
std::cout
<< "unchecked entries:" << std::endl
<< " player faction: " << info.pcFaction << std::endl
<< " DATAstruct" << std::endl;
return true;
}
DialogueManager::DialogueManager (MWWorld::Environment& environment) : mEnvironment (environment) {}
void DialogueManager::startDialogue (const MWWorld::Ptr& actor)
{
std::cout << "talking with " << MWWorld::Class::get (actor).getName (actor) << std::endl;
const ESM::Dialogue *dialogue = mEnvironment.mWorld->getStore().dialogs.find ("hello");
for (std::vector<ESM::DialInfo>::const_iterator iter (dialogue->mInfo.begin());
iter!=dialogue->mInfo.end(); ++iter)
{
if (isMatching (actor, *iter))
{
// start dialogue
std::cout << "found matching info record" << std::endl;
std::cout << "response: " << iter->response << std::endl;
if (!iter->sound.empty())
{
// TODO play sound
}
if (!iter->resultScript.empty())
{
std::cout << "script: " << iter->resultScript << std::endl;
// TODO execute script
}
mEnvironment.mWindowManager->setMode (MWGui::GM_Dialogue);
break;
}
}
}
}

@ -0,0 +1,32 @@
#ifndef GAME_MMDIALOG_DIALOGUEMANAGER_H
#define GAME_MWDIALOG_DIALOGUEMANAGER_H
#include <components/esm/loadinfo.hpp>
#include "../mwworld/ptr.hpp"
namespace MWWorld
{
class Environment;
}
namespace MWDialogue
{
class DialogueManager
{
MWWorld::Environment& mEnvironment;
bool isMatching (const MWWorld::Ptr& actor, const ESM::DialInfo::SelectStruct& select) const;
bool isMatching (const MWWorld::Ptr& actor, const ESM::DialInfo& info) const;
public:
DialogueManager (MWWorld::Environment& environment);
void startDialogue (const MWWorld::Ptr& actor);
};
}
#endif

@ -20,16 +20,22 @@ using namespace std;
#ifdef OPENMW_USE_AUDIERE
#include <mangle/sound/filters/openal_audiere.hpp>
#define SOUND_FACTORY OpenAL_Audiere_Factory
#define SOUND_OUT "OpenAL"
#define SOUND_IN "Audiere"
#endif
#ifdef OPENMW_USE_FFMPEG
#include <mangle/sound/filters/openal_ffmpeg.hpp>
#define SOUND_FACTORY OpenAL_FFMpeg_Factory
#define SOUND_OUT "OpenAL"
#define SOUND_IN "FFmpeg"
#endif
#ifdef OPENMW_USE_MPG123
#include <mangle/sound/filters/openal_sndfile_mpg123.hpp>
#define SOUND_FACTORY OpenAL_SndFile_Mpg123_Factory
#define SOUND_OUT "OpenAL"
#define SOUND_IN "mpg123,sndfile"
#endif
using namespace Mangle::Sound;
@ -77,6 +83,9 @@ namespace MWSound
, cameraTracker(mgr)
, store(str)
{
cout << "Sound output: " << SOUND_OUT << endl;
cout << "Sound decoder: " << SOUND_IN << endl;
// Attach the camera to the camera tracker
cameraTracker.followCamera(camera);
@ -150,29 +159,37 @@ namespace MWSound
SoundManager::SoundManager(Ogre::Root *root, Ogre::Camera *camera,
const ESMS::ESMStore &store,
const std::string &soundDir)
const std::string &soundDir,
bool useSound)
: mData(NULL)
{
mData = new SoundImpl(root, camera, store, soundDir);
if(useSound)
mData = new SoundImpl(root, camera, store, soundDir);
}
SoundManager::~SoundManager()
{
delete mData;
if(mData)
delete mData;
}
void SoundManager::say (MWWorld::Ptr ptr, const std::string& filename)
{
// The range values are not tested
if(!mData) return;
mData->add(filename, ptr, "_say_sound", 1, 1, 100, 10000, false);
}
bool SoundManager::sayDone (MWWorld::Ptr ptr) const
{
if(!mData) return false;
return !mData->isPlaying(ptr, "_say_sound");
}
void SoundManager::streamMusic (const std::string& filename)
{
if(!mData) return;
// Play the sound and tell it to stream, if possible. TODO:
// Store the reference, the jukebox will need to check status,
// control volume etc.
@ -184,6 +201,8 @@ namespace MWSound
void SoundManager::playSound (const std::string& soundId, float volume, float pitch)
{
if(!mData) return;
// Play and forget
float min, max;
const std::string &file = mData->lookup(soundId, volume, min, max);
@ -199,6 +218,8 @@ namespace MWSound
void SoundManager::playSound3D (MWWorld::Ptr ptr, const std::string& soundId,
float volume, float pitch, bool loop)
{
if(!mData) return;
// Look up the sound in the ESM data
float min, max;
const std::string &file = mData->lookup(soundId, volume, min, max);
@ -208,21 +229,28 @@ namespace MWSound
void SoundManager::stopSound3D (MWWorld::Ptr ptr, const std::string& soundId)
{
if(!mData) return;
mData->remove(ptr, soundId);
}
void SoundManager::stopSound (MWWorld::Ptr::CellStore *cell)
{
if(!mData) return;
mData->removeCell(cell);
}
bool SoundManager::getSoundPlaying (MWWorld::Ptr ptr, const std::string& soundId) const
{
// Mark all sounds as playing, otherwise the scripts will just
// keep trying to play them every frame.
if(!mData) return true;
return mData->isPlaying(ptr, soundId);
}
void SoundManager::updateObject(MWWorld::Ptr ptr)
{
if(!mData) return;
mData->updatePositions(ptr);
}
}

@ -28,7 +28,7 @@ namespace MWSound
public:
SoundManager(Ogre::Root*, Ogre::Camera*, const ESMS::ESMStore &store,
const std::string &soundDir);
const std::string &soundDir, bool useSound);
~SoundManager();
void say (MWWorld::Ptr reference, const std::string& filename);

@ -3,7 +3,7 @@
#include "environment.hpp"
#include "../mwgui/window_manager.hpp"
#include "../mwdialogue/dialoguemanager.hpp"
namespace MWWorld
{
@ -11,6 +11,6 @@ namespace MWWorld
void ActionTalk::execute (Environment& environment)
{
environment.mWindowManager->setMode (MWGui::GM_Dialogue);
environment.mDialogueManager->startDialogue (mActor);
}
}

@ -13,6 +13,7 @@ namespace MWWorld
public:
ActionTalk (const Ptr& actor);
///< \param actor The actor the player is talking to
virtual void execute (Environment& environment);
};

@ -14,6 +14,11 @@ namespace MWWorld
Class::~Class() {}
std::string Class::getId (const Ptr& ptr) const
{
throw std::runtime_error ("class does not support ID retrieval");
}
void Class::insertObj (const Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const
{

@ -42,6 +42,10 @@ namespace MWWorld
virtual ~Class();
virtual std::string getId (const Ptr& ptr) const;
///< Return ID of \a ptr or throw an exception, if class does not support ID retrieval
/// (default implementation: throw an exception)
virtual void insertObj (const Ptr& ptr, MWRender::CellRenderImp& cellRender,
MWWorld::Environment& environment) const;
///< Add reference into a cell for rendering (default implementation: don't render anything).

@ -21,17 +21,22 @@ namespace MWMechanics
class MechanicsManager;
}
namespace MWDialogue
{
class DialogueManager;
}
namespace MWWorld
{
class World;
///< Collection of script-accessable sub-systems
class Environment
{
{
public:
Environment()
: mWorld (0), mSoundManager (0), mGlobalScripts (0), mWindowManager (0),
mMechanicsManager (0), mFrameDuration (0)
mMechanicsManager (0), mDialogueManager (0), mFrameDuration (0)
{}
World *mWorld;
@ -39,9 +44,9 @@ namespace MWWorld
MWScript::GlobalScripts *mGlobalScripts;
MWGui::WindowManager *mWindowManager;
MWMechanics::MechanicsManager *mMechanicsManager;
MWDialogue::DialogueManager *mDialogueManager;
float mFrameDuration;
};
}
#endif

@ -1,7 +1,10 @@
#ifndef _ESM_DIAL_H
#define _ESM_DIAL_H
#include <vector>
#include "esm_reader.hpp"
#include "loadinfo.hpp"
namespace ESM {
@ -23,6 +26,7 @@ struct Dialogue
};
char type;
std::vector<DialInfo> mInfo;
void load(ESMReader &esm)
{

@ -14,13 +14,13 @@
#include "loadcont.hpp"
#include "loadcrea.hpp"
#include "loadcrec.hpp"
#include "loadinfo.hpp"
#include "loaddial.hpp"
#include "loaddoor.hpp"
#include "loadench.hpp"
#include "loadfact.hpp"
#include "loadglob.hpp"
#include "loadgmst.hpp"
#include "loadinfo.hpp"
#include "loadingr.hpp"
#include "loadland.hpp"
#include "loadlevlist.hpp"

@ -18,32 +18,70 @@ static string toStr(int i)
void ESMStore::load(ESMReader &esm)
{
set<string> missing;
set<string> missing;
// Loop through all records
while(esm.hasMoreRecs())
ESM::Dialogue *dialogue = 0;
// Loop through all records
while(esm.hasMoreRecs())
{
NAME n = esm.getRecName();
esm.getRecHeader();
NAME n = esm.getRecName();
esm.getRecHeader();
// Look up the record type.
RecListList::iterator it = recLists.find(n.val);
// Look up the record type.
RecListList::iterator it = recLists.find(n.val);
if(it == recLists.end())
if(it == recLists.end())
{
// Not found (this would be an error later)
esm.skipRecord();
missing.insert(n.toString());
continue;
if (n.val==ESM::REC_INFO)
{
if (dialogue)
{
ESM::DialInfo info;
info.load (esm);
dialogue->mInfo.push_back (info);
}
else
{
std::cerr << "error: info record without dialog" << std::endl;
esm.skipRecord();
continue;
}
}
else
{
// Not found (this would be an error later)
esm.skipRecord();
missing.insert(n.toString());
continue;
}
}
else
{
// Load it
std::string id = esm.getHNOString("NAME");
it->second->load(esm, id);
if (n.val==ESM::REC_DIAL)
{
RecListT<Dialogue>& recList = static_cast<RecListT<Dialogue>& > (*it->second);
// Load it
std::string id = esm.getHNOString("NAME");
it->second->load(esm, id);
id = recList.toLower (id);
// Insert the reference into the global lookup
if(!id.empty())
all[id] = n.val;
RecListT<Dialogue>::MapType::iterator iter = recList.list.find (id);
assert (iter!=recList.list.end());
dialogue = &iter->second;
}
else
dialogue = 0;
// Insert the reference into the global lookup
if(!id.empty())
all[id] = n.val;
}
}
/* This information isn't needed on screen. But keep the code around

@ -69,7 +69,6 @@ namespace ESMS
// Lists that need special rules
CellList cells;
RecIDListT<GameSetting> gameSettings;
//RecListT<DialInfo> dialInfos;
//RecListT<Land> lands;
//RecListT<LandTexture> landTexts;
//RecListT<MagicEffect> magicEffects;
@ -92,7 +91,6 @@ namespace ESMS
ESMStore()
{
recLists[REC_ACTI] = &activators;
recLists[REC_ACTI] = &activators;
recLists[REC_ALCH] = &potions;
recLists[REC_APPA] = &appas;
@ -113,7 +111,6 @@ namespace ESMS
recLists[REC_FACT] = &factions;
recLists[REC_GLOB] = &globals;
recLists[REC_GMST] = &gameSettings;
//recLists[REC_INFO] = &dialInfos;
recLists[REC_INGR] = &ingreds;
//recLists[REC_LAND] = &lands;
recLists[REC_LEVC] = &creatureLists;

@ -1 +1 @@
Subproject commit cd4ed4e6bfb23d736c4b1f30d6096ec164ba937b
Subproject commit 200fab03efaa2cbe1b8fce4a742b0195f8912295
Loading…
Cancel
Save