mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
603 lines
16 KiB
C++
603 lines
16 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "common/formats/ini-file.h"
|
|
#include "common/stream.h"
|
|
#include "common/system.h"
|
|
#include "common/events.h"
|
|
|
|
#include "graphics/surface.h"
|
|
|
|
#include "petka/big_dialogue.h"
|
|
#include "petka/flc.h"
|
|
#include "petka/sound.h"
|
|
#include "petka/petka.h"
|
|
#include "petka/video.h"
|
|
#include "petka/q_system.h"
|
|
#include "petka/q_manager.h"
|
|
#include "petka/objects/object_star.h"
|
|
#include "petka/objects/object_cursor.h"
|
|
#include "petka/interfaces/main.h"
|
|
#include "petka/objects/heroes.h"
|
|
#include "petka/objects/object_case.h"
|
|
|
|
namespace Petka {
|
|
|
|
QReaction *createReaction(QMessage *messages, QMessage *end) {
|
|
QReaction *reaction = new QReaction();
|
|
while (messages != end) {
|
|
reaction->messages.push_back(*messages++);
|
|
}
|
|
return reaction;
|
|
}
|
|
|
|
QVisibleObject::QVisibleObject()
|
|
: _resourceId(-1), _z(240) {}
|
|
|
|
QMessageObject::QMessageObject() {
|
|
_id = (uint16)-1;
|
|
_status = 0;
|
|
_time = 0;
|
|
_dialogColor = -1;
|
|
_animate = true;
|
|
_isShown = true;
|
|
_isActive = true;
|
|
_updateZ = false;
|
|
_holdMessages = false;
|
|
_loopedSound = false;
|
|
_startSound = false;
|
|
_reaction = nullptr;
|
|
|
|
_x = _y = _walkX = _walkY = 0;
|
|
_frame = 0;
|
|
_sound = nullptr;
|
|
_reactionId = 0;
|
|
}
|
|
|
|
void QMessageObject::processMessage(const QMessage &msg) {
|
|
bool reacted = false;
|
|
int opcode = (msg.opcode == kObjectUse) ? (msg.sender->_id << 16) | kObjectUse : msg.opcode;
|
|
for (uint i = 0; i < _reactions.size(); ++i) {
|
|
QReaction *r = &_reactions[i];
|
|
if (r->opcode != msg.opcode ||
|
|
(r->status != -1 && r->status != _status) ||
|
|
(r->senderId != -1 && r->senderId != msg.sender->_id)) {
|
|
continue;
|
|
}
|
|
bool fallback;
|
|
if (g_vm->getBigDialogue()->findHandler(_id, opcode, &fallback) && !fallback) {
|
|
g_vm->getBigDialogue()->setHandler(_id, opcode);
|
|
g_vm->getQSystem()->_mainInterface->_dialog.setSender(this);
|
|
}
|
|
processReaction(r, &msg);
|
|
reacted = true;
|
|
}
|
|
|
|
if (reacted || !g_vm->getBigDialogue()->findHandler(_id, opcode, nullptr)) {
|
|
switch (msg.opcode) {
|
|
case kAddInv:
|
|
g_vm->getQSystem()->getCase()->addItem(msg.objId);
|
|
// original bug fix
|
|
g_vm->pushMouseMoveEvent();
|
|
break;
|
|
case kDelInv:
|
|
g_vm->getQSystem()->getCase()->removeItem(msg.objId);
|
|
break;
|
|
case kSetInv:
|
|
g_vm->getQSystem()->getCase()->transformItem(msg.sender->_id, msg.objId);
|
|
break;
|
|
case kAvi: {
|
|
Common::String videoName = g_vm->resMgr()->findResourceName((uint16) msg.arg1);
|
|
g_vm->playVideo(g_vm->openFile(videoName, false));
|
|
break;
|
|
}
|
|
case kContinue:
|
|
g_vm->getQSystem()->_mainInterface->_dialog.endUserMsg();
|
|
break;
|
|
case kCursor:
|
|
g_vm->getQSystem()->getCursor()->setInvItem(this, msg.arg1);
|
|
g_vm->videoSystem()->makeAllDirty();
|
|
break;
|
|
case kDialog:
|
|
g_vm->getQSystem()->_mainInterface->_dialog.start(msg.arg1, this);
|
|
break;
|
|
case kSetPos:
|
|
setPos(Common::Point(msg.arg1, msg.arg2), false);
|
|
break;
|
|
case kSet:
|
|
case kPlay:
|
|
play(msg.arg1, msg.arg2);
|
|
break;
|
|
case kAnimate:
|
|
_animate = msg.arg1;
|
|
break;
|
|
case kEnd:
|
|
if (_reaction && _reactionId == msg.arg1) {
|
|
QReaction *reaction = _reaction;
|
|
_reaction = nullptr;
|
|
processReaction(reaction);
|
|
}
|
|
break;
|
|
case kStatus:
|
|
_status = (int8) msg.arg1;
|
|
break;
|
|
case kOn:
|
|
_isActive = true;
|
|
show(true);
|
|
break;
|
|
case kOff:
|
|
_isActive = false;
|
|
show(false);
|
|
break;
|
|
case kStop:
|
|
g_vm->getQSystem()->getCursor()->show(msg.arg1);
|
|
g_vm->getQSystem()->getStar()->_isActive = msg.arg1;
|
|
break;
|
|
case kShow:
|
|
show(msg.arg1);
|
|
break;
|
|
case kShake:
|
|
g_vm->videoSystem()->setShake(msg.arg1);
|
|
break;
|
|
case kSystem:
|
|
switch (msg.arg1){
|
|
case 0:
|
|
g_vm->getQSystem()->getStar()->_isActive = false;
|
|
break;
|
|
case 1:
|
|
g_vm->getQSystem()->getStar()->_isActive = true;
|
|
break;
|
|
case 242:
|
|
Engine::quitGame();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case kHide:
|
|
show(false);
|
|
break;
|
|
case kZBuffer:
|
|
_updateZ = msg.arg1;
|
|
_z = (msg.arg2 != -1) ? msg.arg2 : _z;
|
|
break;
|
|
case kActive:
|
|
_isActive = msg.arg1;
|
|
break;
|
|
case kPassive:
|
|
_isActive = false;
|
|
break;
|
|
case kJump: {
|
|
Common::Point p;
|
|
p.x = (msg.arg1 == 0xffff ? _walkX : msg.arg1);
|
|
p.y = (msg.arg2 == -1 ? _walkY : msg.arg2);
|
|
g_vm->getQSystem()->getPetka()->setPos(p, false);
|
|
break;
|
|
}
|
|
case kJumpVich: {
|
|
Common::Point p;
|
|
p.x = (msg.arg1 == 0xffff ? _walkX : msg.arg1);
|
|
p.y = (msg.arg2 == -1 ? _walkY : msg.arg2);
|
|
g_vm->getQSystem()->getChapay()->setPos(p, false);
|
|
break;
|
|
}
|
|
case kWalk:
|
|
if (!reacted) {
|
|
if (_walkX == -1) {
|
|
g_vm->getQSystem()->getPetka()->walk(msg.arg1, msg.arg2);
|
|
|
|
} else {
|
|
g_vm->getQSystem()->getPetka()->walk(_walkX, _walkY);
|
|
}
|
|
}
|
|
break;
|
|
case kWalkTo: {
|
|
int destX = msg.arg1;
|
|
int destY = msg.arg2;
|
|
if (destX == -1 || destY == -1) {
|
|
destX = _walkX;
|
|
destY = _walkY;
|
|
}
|
|
if (destX != -1) {
|
|
g_vm->getQSystem()->getPetka()->walk(destX, destY);
|
|
QReaction *r = g_vm->getQSystem()->getPetka()->_heroReaction;
|
|
if (r) {
|
|
for (uint i = 0; i < r->messages.size(); ++i) {
|
|
if (r->messages[i].opcode == kGoTo) {
|
|
g_vm->getQSystem()->getChapay()->walk(destX, destY);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case kWalkVich: {
|
|
int destX = msg.arg1;
|
|
int destY = msg.arg2;
|
|
if (destX == -1 || destY == -1) {
|
|
destX = _walkX;
|
|
destY = _walkY;
|
|
}
|
|
if (destX != -1)
|
|
g_vm->getQSystem()->getChapay()->walk(destX, destY);
|
|
break;
|
|
}
|
|
case kDescription: {
|
|
Common::ScopedPtr<Common::SeekableReadStream> invStream(g_vm->openFile("invntr.txt", true));
|
|
if (invStream) {
|
|
Common::String desc;
|
|
Common::INIFile invIni;
|
|
|
|
invIni.allowNonEnglishCharacters();
|
|
invIni.loadFromStream(*invStream);
|
|
invIni.getKey(_name, "ALL", desc);
|
|
|
|
g_vm->getQSystem()->_mainInterface->setTextDescription(Common::convertToU32String(desc.c_str(), Common::kWindows1251), msg.arg1);
|
|
}
|
|
break;
|
|
}
|
|
case kPart:
|
|
g_vm->loadPartAtNextFrame(msg.arg1);
|
|
break;
|
|
case kChapter:
|
|
g_vm->loadChapter(msg.arg1);
|
|
break;
|
|
case kToMap:
|
|
g_vm->getQSystem()->toggleMapInterface();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
for (uint i = 0; i < _reactions.size(); ++i) {
|
|
QReaction &r = _reactions[i];
|
|
if (r.opcode != msg.opcode ||
|
|
(r.status != -1 && r.status != _status) ||
|
|
(r.senderId != -1 && r.senderId != msg.sender->_id)) {
|
|
continue;
|
|
}
|
|
g_vm->getQSystem()->_mainInterface->_dialog.setReaction(createReaction(r.messages.data(), r.messages.end()));
|
|
}
|
|
g_vm->getBigDialogue()->setHandler(_id, opcode);
|
|
g_vm->getQSystem()->_mainInterface->_dialog.start(msg.arg1, this);
|
|
}
|
|
|
|
}
|
|
|
|
void QMessageObject::show(bool v) {
|
|
_isShown = v;
|
|
}
|
|
|
|
void QMessageObject::setReaction(int16 id, QReaction *reaction) {
|
|
delete _reaction;
|
|
_reaction = reaction;
|
|
_reactionId = id;
|
|
}
|
|
|
|
void QMessageObject::processReaction(QReaction *r, const QMessage *msg) {
|
|
bool deleteReaction = (msg == nullptr);
|
|
for (uint j = 0; j < r->messages.size(); ++j) {
|
|
QMessage &rMsg = r->messages[j];
|
|
if (rMsg.opcode == kCheck && g_vm->getQSystem()->findObject(rMsg.objId)->_status != rMsg.arg1) {
|
|
break;
|
|
}
|
|
if (msg && rMsg.opcode == kIf &&
|
|
((rMsg.arg1 != 0xffff && rMsg.arg1 != msg->arg1) ||
|
|
(rMsg.arg2 != -1 && rMsg.arg2 != msg->arg2) ||
|
|
(rMsg.arg3 != -1 && rMsg.arg3 != msg->arg3))) {
|
|
break;
|
|
}
|
|
if (msg && rMsg.opcode == kRandom && rMsg.arg2 != -1) {
|
|
rMsg.arg1 = (int16) g_vm->getRnd().getRandomNumber((uint) (rMsg.arg2 - 1));
|
|
}
|
|
g_vm->getQSystem()->addMessage(rMsg.objId, rMsg.opcode, rMsg.arg1, rMsg.arg2, rMsg.arg3, rMsg.unk, this);
|
|
bool processed = true;
|
|
switch (rMsg.opcode) {
|
|
case kDialog: {
|
|
g_vm->getQSystem()->_mainInterface->_dialog.setReaction(createReaction(r->messages.data() + j + 1, r->messages.end()));
|
|
break;
|
|
}
|
|
case kPlay: {
|
|
QMessageObject *obj = g_vm->getQSystem()->findObject(rMsg.objId);
|
|
obj->setReaction(rMsg.arg1, createReaction(r->messages.data() + j + 1, r->messages.end()));
|
|
break;
|
|
}
|
|
case kWalk:
|
|
case kWalkTo:
|
|
g_vm->getQSystem()->getPetka()->setReactionAfterWalk(j, r, this, deleteReaction);
|
|
return;
|
|
case kWalkVich:
|
|
g_vm->getQSystem()->getChapay()->setReactionAfterWalk(j, r, this, deleteReaction);
|
|
return;
|
|
default:
|
|
processed = false;
|
|
break;
|
|
}
|
|
if (processed)
|
|
break;
|
|
}
|
|
if (deleteReaction)
|
|
delete r;
|
|
}
|
|
|
|
void QMessageObject::play(int id, int type) {
|
|
if (g_vm->getQSystem()->_totalInit) {
|
|
_resourceId = id;
|
|
_loopedSound = (type == 5);
|
|
return;
|
|
}
|
|
|
|
if (_loopedSound || g_vm->isDemo()) {
|
|
removeSound();
|
|
}
|
|
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (flc) {
|
|
g_vm->videoSystem()->addDirtyRect(Common::Point(_x, _y), *flc);
|
|
}
|
|
|
|
_resourceId = id;
|
|
|
|
loadSound();
|
|
|
|
flc = g_vm->resMgr()->getFlic(id);
|
|
flc->setFrame(1);
|
|
_time = 0;
|
|
_loopedSound = (type == 5);
|
|
}
|
|
|
|
void QMessageObject::loadSound() {
|
|
Common::String name = g_vm->resMgr()->findSoundName(_resourceId);
|
|
_sound = g_vm->soundMgr()->addSound(name, Audio::Mixer::kSFXSoundType);
|
|
_startSound = false;
|
|
}
|
|
|
|
void QMessageObject::removeSound() {
|
|
Common::String name = g_vm->resMgr()->findSoundName(_resourceId);
|
|
g_vm->soundMgr()->removeSound(name);
|
|
_sound = nullptr;
|
|
}
|
|
|
|
static Common::String readString(Common::ReadStream &readStream) {
|
|
uint32 stringSize = readStream.readUint32LE();
|
|
byte *data = (byte *)malloc(stringSize + 1);
|
|
readStream.read(data, stringSize);
|
|
data[stringSize] = '\0';
|
|
Common::String str((char *)data);
|
|
free(data);
|
|
return str;
|
|
}
|
|
|
|
void QMessageObject::readScriptData(Common::SeekableReadStream &stream) {
|
|
_id = stream.readUint16LE();
|
|
_name = readString(stream);
|
|
_reactions.resize(stream.readUint32LE());
|
|
|
|
for (uint i = 0; i < _reactions.size(); ++i) {
|
|
QReaction *reaction = &_reactions[i];
|
|
reaction->opcode = stream.readUint16LE();
|
|
reaction->status = stream.readByte();
|
|
reaction->senderId = stream.readUint16LE();
|
|
reaction->messages.resize(stream.readUint32LE());
|
|
for (uint j = 0; j < reaction->messages.size(); ++j) {
|
|
QMessage *msg = &reaction->messages[j];
|
|
msg->objId = stream.readUint16LE();
|
|
msg->opcode = stream.readUint16LE();
|
|
msg->arg1 = stream.readUint16LE();
|
|
msg->arg2 = stream.readUint16LE();
|
|
msg->arg3 = stream.readUint16LE();
|
|
}
|
|
}
|
|
}
|
|
|
|
void QMessageObject::readInisData(Common::INIFile &names, Common::INIFile &cast, Common::INIFile *bgs) {
|
|
names.getKey(_name, "all", _nameOnScreen);
|
|
Common::String rgbString;
|
|
if (cast.getKey(_name, "all", rgbString)) {
|
|
int r, g, b;
|
|
sscanf(rgbString.c_str(), "%d %d %d", &r, &g, &b);
|
|
_dialogColor = g_vm->_system->getScreenFormat().RGBToColor((byte)r, (byte)g, (byte)b);
|
|
}
|
|
}
|
|
|
|
QObject::QObject() {
|
|
_animate = true;
|
|
_updateZ = true;
|
|
_frame = 1;
|
|
_sound = nullptr;
|
|
_x = 0;
|
|
_y = 0;
|
|
_walkX = -1;
|
|
_walkY = -1;
|
|
}
|
|
|
|
bool QObject::isInPoint(Common::Point p) {
|
|
if (!_isActive)
|
|
return false;
|
|
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (!flc || !flc->getBounds().contains(p.x - _x, p.y - _y))
|
|
return false;
|
|
|
|
const Graphics::Surface *s = flc->getCurrentFrame();
|
|
auto format = g_system->getScreenFormat();
|
|
|
|
byte index = *(const byte *)s->getBasePtr(p.x - _x, p.y - _y);
|
|
const byte *pal = flc->getPalette();
|
|
|
|
return format.RGBToColor(pal[0], pal[1], pal[2]) != format.RGBToColor(pal[index * 3], pal[index * 3 + 1], pal[index * 3 + 2]);
|
|
}
|
|
|
|
void QObject::draw() {
|
|
if (!_isShown || _resourceId == -1) {
|
|
return;
|
|
}
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (!flc) {
|
|
return;
|
|
}
|
|
if (_animate && _startSound) {
|
|
if (_sound) {
|
|
_sound->play(_loopedSound);
|
|
if (_loopedSound) {
|
|
_sound = nullptr;
|
|
}
|
|
}
|
|
_startSound = false;
|
|
}
|
|
|
|
int xOff = g_vm->getQSystem()->_xOffset;
|
|
VideoSystem *videoSys = g_vm->videoSystem();
|
|
|
|
Common::Rect screen(640 + xOff, 480);
|
|
Common::Rect flcBounds(flc->getBounds());
|
|
Common::Rect objBounds(flcBounds);
|
|
|
|
objBounds.translate(_x, _y);
|
|
|
|
Common::Rect intersect(screen.findIntersectingRect(objBounds));
|
|
if (intersect.isEmpty())
|
|
return;
|
|
|
|
Graphics::Surface *surface = flc->getCurrentFrame()->getSubArea(flcBounds).convertTo(g_system->getScreenFormat(), flc->getPalette());
|
|
|
|
for (Common::Rect dirty : videoSys->rects()) {
|
|
dirty.translate(xOff, 0);
|
|
|
|
Common::Rect destRect(intersect.findIntersectingRect(dirty));
|
|
if (destRect.isEmpty())
|
|
continue;
|
|
|
|
Common::Rect srcRect(destRect);
|
|
|
|
srcRect.translate(-_x, -_y);
|
|
srcRect.translate(-flcBounds.left, -flcBounds.top);
|
|
|
|
destRect.translate(-xOff, 0);
|
|
videoSys->transBlitFrom(*surface, srcRect, destRect, flc->getTransColor(surface->format));
|
|
}
|
|
|
|
surface->free();
|
|
delete surface;
|
|
}
|
|
|
|
void QObject::updateZ() {
|
|
if (!_animate || !_isShown || !_updateZ)
|
|
return;
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (flc) {
|
|
_z = 1;
|
|
const Common::Array<Common::Rect> &rects = flc->getMskRects();
|
|
for (uint i = 0; i < rects.size(); ++i) {
|
|
if (_y + rects[i].bottom > _z)
|
|
_z = _y + rects[i].bottom;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void QObject::show(bool v) {
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (flc) {
|
|
g_vm->videoSystem()->addDirtyRect(Common::Point(_x, _y), *flc);
|
|
}
|
|
QMessageObject::show(v);
|
|
}
|
|
|
|
void QObject::update(int time) {
|
|
if (!_animate || !_isShown)
|
|
return;
|
|
_time += time;
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (flc && flc->getFrameCount() != 1) {
|
|
if (_sound) {
|
|
Common::Rect bounds = flc->getBounds();
|
|
_sound->setBalance(bounds.left + bounds.width() / 2 - g_vm->getQSystem()->_xOffset, 640);
|
|
}
|
|
|
|
while (_time >= (int32)flc->getDelay()) {
|
|
if (_sound && flc->getCurFrame() == 0) {
|
|
_startSound = true;
|
|
}
|
|
g_vm->videoSystem()->addDirtyRect(Common::Point(_x, _y), *flc);
|
|
flc->setFrame(-1);
|
|
if (flc->getCurFrame() == (int32)flc->getFrameCount() - 1) {
|
|
g_vm->getQSystem()->addMessage(_id, kEnd, _resourceId, 0, 0, 0, nullptr);
|
|
}
|
|
if (flc->getCurFrame() + 1 == (int32)flc->getFrameCount() / 2) {
|
|
g_vm->getQSystem()->addMessage(_id, kHalf, _resourceId, 0, 0, 0, nullptr);
|
|
}
|
|
g_vm->videoSystem()->addDirtyRect(Common::Point(_x, _y), *flc);
|
|
_time -= flc->getDelay();
|
|
}
|
|
}
|
|
}
|
|
|
|
void QObject::setPos(Common::Point p, bool) {
|
|
FlicDecoder *flc = g_vm->resMgr()->getFlic(_resourceId);
|
|
if (flc) {
|
|
g_vm->videoSystem()->addDirtyMskRects(Common::Point(_x, _y), *flc);
|
|
g_vm->videoSystem()->addDirtyMskRects(p, *flc);
|
|
_x = p.x;
|
|
_y = p.y;
|
|
}
|
|
}
|
|
|
|
void QObject::onClick(Common::Point p) {
|
|
QSystem *sys = g_vm->getQSystem();
|
|
QObjectCursor *cursor = g_vm->getQSystem()->getCursor();
|
|
|
|
sys->getPetka()->stopWalk();
|
|
sys->getChapay()->stopWalk();
|
|
|
|
switch (cursor->_actionType) {
|
|
case kActionLook:
|
|
g_vm->getQSystem()->addMessage(_id, kLook, 0, 0, 0, 0, this);
|
|
break;
|
|
case kActionWalk:
|
|
g_vm->getQSystem()->addMessage(_id, kWalk, p.x, p.y, 0, 0, this);
|
|
break;
|
|
case kActionUse:
|
|
g_vm->getQSystem()->addMessage(_id, kUse, 0, 0, 0, 0, this);
|
|
break;
|
|
case kActionTake:
|
|
g_vm->getQSystem()->addMessage(_id, kTake, 0, 0, 0, 0, this);
|
|
break;
|
|
case kActionTalk:
|
|
g_vm->getQSystem()->addMessage(_id, kTalk, 0, 0, 0, 0, this);
|
|
break;
|
|
case kActionObjUseChapayev:
|
|
g_vm->getQSystem()->addMessage(_id, kObjectUse, p.x, p.y, 0, 0, g_vm->getQSystem()->getChapay());
|
|
break;
|
|
case kActionObjUse:
|
|
g_vm->getQSystem()->addMessage(_id, kObjectUse, 0, 0, 0, 0, cursor->_invObj);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void QObject::onMouseMove(Common::Point p) {
|
|
g_vm->getQSystem()->_mainInterface->_objUnderCursor = this;
|
|
}
|
|
|
|
}
|