mirror of
https://github.com/vcmi/vcmi.git
synced 2025-09-16 09:26:28 +02:00
Showing objects
This commit is contained in:
@@ -62,7 +62,7 @@ QImage BitmapHandler::loadH3PCX(ui8 * pcx, size_t size)
|
|||||||
{
|
{
|
||||||
it = 0xC;
|
it = 0xC;
|
||||||
//auto bitmap = QBitmap::fromData(qsize, pcx + it);
|
//auto bitmap = QBitmap::fromData(qsize, pcx + it);
|
||||||
QImage image(pcx + it, width, height, QImage::Format_MonoLSB);
|
QImage image(pcx + it, width, height, QImage::Format_Indexed8);
|
||||||
|
|
||||||
//palette - last 256*3 bytes
|
//palette - last 256*3 bytes
|
||||||
QVector<QRgb> colorTable;
|
QVector<QRgb> colorTable;
|
||||||
|
@@ -8,6 +8,7 @@ set(editor_SRCS
|
|||||||
BitmapHandler.cpp
|
BitmapHandler.cpp
|
||||||
maphandler.cpp
|
maphandler.cpp
|
||||||
Animation.cpp
|
Animation.cpp
|
||||||
|
graphics.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(editor_HEADERS
|
set(editor_HEADERS
|
||||||
@@ -19,6 +20,7 @@ set(editor_HEADERS
|
|||||||
BitmapHandler.h
|
BitmapHandler.h
|
||||||
maphandler.h
|
maphandler.h
|
||||||
Animation.h
|
Animation.h
|
||||||
|
graphics.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(editor_FORMS
|
set(editor_FORMS
|
||||||
|
359
mapeditor/graphics.cpp
Normal file
359
mapeditor/graphics.cpp
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
* Graphics.cpp, part of VCMI engine
|
||||||
|
*
|
||||||
|
* Authors: listed in file AUTHORS in main folder
|
||||||
|
*
|
||||||
|
* License: GNU General Public License v2.0 or later
|
||||||
|
* Full text of license available in license.txt file, in main folder
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include "StdInc.h"
|
||||||
|
#include "graphics.h"
|
||||||
|
|
||||||
|
#include <vcmi/Entity.h>
|
||||||
|
#include <vcmi/ArtifactService.h>
|
||||||
|
#include <vcmi/CreatureService.h>
|
||||||
|
#include <vcmi/FactionService.h>
|
||||||
|
#include <vcmi/HeroTypeService.h>
|
||||||
|
#include <vcmi/SkillService.h>
|
||||||
|
#include <vcmi/spells/Service.h>
|
||||||
|
|
||||||
|
#include "../lib/filesystem/Filesystem.h"
|
||||||
|
#include "../lib/filesystem/CBinaryReader.h"
|
||||||
|
#include "Animation.h"
|
||||||
|
#include "../lib/CThreadHelper.h"
|
||||||
|
#include "../lib/CModHandler.h"
|
||||||
|
#include "../lib/VCMI_Lib.h"
|
||||||
|
#include "../CCallback.h"
|
||||||
|
#include "../lib/CGeneralTextHandler.h"
|
||||||
|
#include "BitmapHandler.h"
|
||||||
|
#include "../lib/CGameState.h"
|
||||||
|
#include "../lib/JsonNode.h"
|
||||||
|
#include "../lib/vcmi_endian.h"
|
||||||
|
#include "../lib/CStopWatch.h"
|
||||||
|
#include "../lib/mapObjects/CObjectClassesHandler.h"
|
||||||
|
#include "../lib/mapObjects/CObjectHandler.h"
|
||||||
|
#include "../lib/CHeroHandler.h"
|
||||||
|
#include "CGameInfo.h"
|
||||||
|
|
||||||
|
Graphics * graphics = nullptr;
|
||||||
|
|
||||||
|
void Graphics::loadPaletteAndColors()
|
||||||
|
{
|
||||||
|
auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll();
|
||||||
|
std::string pals((char*)textFile.first.get(), textFile.second);
|
||||||
|
|
||||||
|
playerColorPalette.resize(256);
|
||||||
|
playerColors.resize(PlayerColor::PLAYER_LIMIT_I);
|
||||||
|
int startPoint = 24; //beginning byte; used to read
|
||||||
|
for(int i=0; i<256; ++i)
|
||||||
|
{
|
||||||
|
QColor col;
|
||||||
|
col.setRed(pals[startPoint++]);
|
||||||
|
col.setGreen(pals[startPoint++]);
|
||||||
|
col.setBlue(pals[startPoint++]);
|
||||||
|
col.setAlpha(255);
|
||||||
|
startPoint++;
|
||||||
|
playerColorPalette[i] = col.rgba();
|
||||||
|
}
|
||||||
|
|
||||||
|
neutralColorPalette.resize(32);
|
||||||
|
|
||||||
|
auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL"));
|
||||||
|
CBinaryReader reader(stream.get());
|
||||||
|
|
||||||
|
for(int i=0; i<32; ++i)
|
||||||
|
{
|
||||||
|
QColor col;
|
||||||
|
col.setRed(reader.readUInt8());
|
||||||
|
col.setGreen(reader.readUInt8());
|
||||||
|
col.setBlue(reader.readUInt8());
|
||||||
|
col.setAlpha(255);
|
||||||
|
reader.readUInt8(); // this is "flags" entry, not alpha
|
||||||
|
neutralColorPalette[i] = col.rgba();
|
||||||
|
}
|
||||||
|
|
||||||
|
//colors initialization
|
||||||
|
QColor colors[] = {
|
||||||
|
{0xff,0, 0, 255},
|
||||||
|
{0x31,0x52,0xff,255},
|
||||||
|
{0x9c,0x73,0x52,255},
|
||||||
|
{0x42,0x94,0x29,255},
|
||||||
|
|
||||||
|
{0xff,0x84,0, 255},
|
||||||
|
{0x8c,0x29,0xa5,255},
|
||||||
|
{0x09,0x9c,0xa5,255},
|
||||||
|
{0xc6,0x7b,0x8c,255}};
|
||||||
|
|
||||||
|
for(int i=0;i<8;i++)
|
||||||
|
{
|
||||||
|
playerColors[i] = colors[i].rgba();
|
||||||
|
}
|
||||||
|
//gray
|
||||||
|
neutralColor = qRgba(0x84, 0x84, 0x84, 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics::Graphics()
|
||||||
|
{
|
||||||
|
#if 0
|
||||||
|
|
||||||
|
std::vector<Task> tasks; //preparing list of graphics to load
|
||||||
|
tasks += std::bind(&Graphics::loadFonts,this);
|
||||||
|
tasks += std::bind(&Graphics::loadPaletteAndColors,this);
|
||||||
|
tasks += std::bind(&Graphics::initializeBattleGraphics,this);
|
||||||
|
tasks += std::bind(&Graphics::loadErmuToPicture,this);
|
||||||
|
tasks += std::bind(&Graphics::initializeImageLists,this);
|
||||||
|
|
||||||
|
CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency()));
|
||||||
|
th.run();
|
||||||
|
#else
|
||||||
|
loadPaletteAndColors();
|
||||||
|
initializeImageLists();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//(!) do not load any CAnimation here
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics::~Graphics()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::load()
|
||||||
|
{
|
||||||
|
loadHeroAnimations();
|
||||||
|
loadHeroFlagAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::loadHeroAnimations()
|
||||||
|
{
|
||||||
|
for(auto & elem : CGI->heroh->classes.objects)
|
||||||
|
{
|
||||||
|
for (auto & templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates())
|
||||||
|
{
|
||||||
|
if (!heroAnimations.count(templ.animationFile))
|
||||||
|
heroAnimations[templ.animationFile] = loadHeroAnimation(templ.animationFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boatAnimations[0] = loadHeroAnimation("AB01_.DEF");
|
||||||
|
boatAnimations[1] = loadHeroAnimation("AB02_.DEF");
|
||||||
|
boatAnimations[2] = loadHeroAnimation("AB03_.DEF");
|
||||||
|
|
||||||
|
|
||||||
|
mapObjectAnimations["AB01_.DEF"] = boatAnimations[0];
|
||||||
|
mapObjectAnimations["AB02_.DEF"] = boatAnimations[1];
|
||||||
|
mapObjectAnimations["AB03_.DEF"] = boatAnimations[2];
|
||||||
|
}
|
||||||
|
void Graphics::loadHeroFlagAnimations()
|
||||||
|
{
|
||||||
|
static const std::vector<std::string> HERO_FLAG_ANIMATIONS =
|
||||||
|
{
|
||||||
|
"AF00", "AF01","AF02","AF03",
|
||||||
|
"AF04", "AF05","AF06","AF07"
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::vector< std::vector<std::string> > BOAT_FLAG_ANIMATIONS =
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"ABF01L", "ABF01G", "ABF01R", "ABF01D",
|
||||||
|
"ABF01B", "ABF01P", "ABF01W", "ABF01K"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ABF02L", "ABF02G", "ABF02R", "ABF02D",
|
||||||
|
"ABF02B", "ABF02P", "ABF02W", "ABF02K"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ABF03L", "ABF03G", "ABF03R", "ABF03D",
|
||||||
|
"ABF03B", "ABF03P", "ABF03W", "ABF03K"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for(const auto & name : HERO_FLAG_ANIMATIONS)
|
||||||
|
heroFlagAnimations.push_back(loadHeroFlagAnimation(name));
|
||||||
|
|
||||||
|
for(int i = 0; i < BOAT_FLAG_ANIMATIONS.size(); i++)
|
||||||
|
for(const auto & name : BOAT_FLAG_ANIMATIONS[i])
|
||||||
|
boatFlagAnimations[i].push_back(loadHeroFlagAnimation(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> Graphics::loadHeroFlagAnimation(const std::string & name)
|
||||||
|
{
|
||||||
|
//first - group number to be rotated, second - group number after rotation
|
||||||
|
static const std::vector<std::pair<int,int> > rotations =
|
||||||
|
{
|
||||||
|
{6,10}, {7,11}, {8,12}, {1,13},
|
||||||
|
{2,14}, {3,15}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> anim = std::make_shared<Animation>(name);
|
||||||
|
anim->preload();
|
||||||
|
|
||||||
|
for(const auto & rotation : rotations)
|
||||||
|
{
|
||||||
|
const int sourceGroup = rotation.first;
|
||||||
|
const int targetGroup = rotation.second;
|
||||||
|
|
||||||
|
anim->createFlippedGroup(sourceGroup, targetGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> Graphics::loadHeroAnimation(const std::string &name)
|
||||||
|
{
|
||||||
|
//first - group number to be rotated, second - group number after rotation
|
||||||
|
static const std::vector<std::pair<int,int> > rotations =
|
||||||
|
{
|
||||||
|
{6,10}, {7,11}, {8,12}, {1,13},
|
||||||
|
{2,14}, {3,15}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> anim = std::make_shared<Animation>(name);
|
||||||
|
anim->preload();
|
||||||
|
|
||||||
|
|
||||||
|
for(const auto & rotation : rotations)
|
||||||
|
{
|
||||||
|
const int sourceGroup = rotation.first;
|
||||||
|
const int targetGroup = rotation.second;
|
||||||
|
|
||||||
|
anim->createFlippedGroup(sourceGroup, targetGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player)
|
||||||
|
{
|
||||||
|
if(sur->format() == QImage::Format_Indexed8)
|
||||||
|
{
|
||||||
|
auto palette = sur->colorTable();
|
||||||
|
if(player < PlayerColor::PLAYER_LIMIT)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < 32; ++i)
|
||||||
|
palette[224 + i] = playerColorPalette[player.getNum() * 32 + i];
|
||||||
|
}
|
||||||
|
else if(player == PlayerColor::NEUTRAL)
|
||||||
|
{
|
||||||
|
palette = neutralColorPalette;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//FIXME: not all player colored images have player palette at last 32 indexes
|
||||||
|
//NOTE: following code is much more correct but still not perfect (bugged with status bar)
|
||||||
|
sur->setColorTable(palette);
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
|
||||||
|
SDL_Color * bluePalette = playerColorPalette + 32;
|
||||||
|
|
||||||
|
SDL_Palette * oldPalette = sur->format->palette;
|
||||||
|
|
||||||
|
SDL_Palette * newPalette = SDL_AllocPalette(256);
|
||||||
|
|
||||||
|
for(size_t destIndex = 0; destIndex < 256; destIndex++)
|
||||||
|
{
|
||||||
|
SDL_Color old = oldPalette->colors[destIndex];
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for(size_t srcIndex = 0; srcIndex < 32; srcIndex++)
|
||||||
|
{
|
||||||
|
if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
newPalette->colors[destIndex] = palette[srcIndex];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
newPalette->colors[destIndex] = old;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetSurfacePalette(sur, newPalette);
|
||||||
|
|
||||||
|
SDL_FreePalette(newPalette);
|
||||||
|
|
||||||
|
#endif // 0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//TODO: implement. H3 method works only for images with palettes.
|
||||||
|
// Add some kind of player-colored overlay?
|
||||||
|
// Or keep palette approach here and replace only colors of specific value(s)
|
||||||
|
// Or just wait for OpenGL support?
|
||||||
|
logGlobal->warn("Image must have palette to be player-colored!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> Graphics::getAnimation(const CGObjectInstance* obj)
|
||||||
|
{
|
||||||
|
return getAnimation(obj->appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> Graphics::getAnimation(const ObjectTemplate & info)
|
||||||
|
{
|
||||||
|
//the only(?) invisible object
|
||||||
|
if(info.id == Obj::EVENT)
|
||||||
|
{
|
||||||
|
return std::shared_ptr<Animation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(info.animationFile.empty())
|
||||||
|
{
|
||||||
|
logGlobal->warn("Def name for obj (%d,%d) is empty!", info.id, info.subid);
|
||||||
|
return std::shared_ptr<Animation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> ret = mapObjectAnimations[info.animationFile];
|
||||||
|
|
||||||
|
//already loaded
|
||||||
|
if(ret)
|
||||||
|
{
|
||||||
|
ret->preload();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = std::make_shared<Animation>(info.animationFile);
|
||||||
|
mapObjectAnimations[info.animationFile] = ret;
|
||||||
|
|
||||||
|
ret->preload();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::addImageListEntry(size_t index, const std::string & listName, const std::string & imageName)
|
||||||
|
{
|
||||||
|
if (!imageName.empty())
|
||||||
|
{
|
||||||
|
JsonNode entry;
|
||||||
|
entry["frame"].Integer() = index;
|
||||||
|
entry["file"].String() = imageName;
|
||||||
|
|
||||||
|
imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::addImageListEntries(const EntityService * service)
|
||||||
|
{
|
||||||
|
auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3);
|
||||||
|
|
||||||
|
auto loopCb = [&](const Entity * entity, bool & stop)
|
||||||
|
{
|
||||||
|
entity->registerIcons(cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
service->forEachBase(loopCb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Graphics::initializeImageLists()
|
||||||
|
{
|
||||||
|
addImageListEntries(CGI->creatures());
|
||||||
|
addImageListEntries(CGI->heroTypes());
|
||||||
|
addImageListEntries(CGI->artifacts());
|
||||||
|
addImageListEntries(CGI->factions());
|
||||||
|
addImageListEntries(CGI->spells());
|
||||||
|
addImageListEntries(CGI->skills());
|
||||||
|
}
|
83
mapeditor/graphics.h
Normal file
83
mapeditor/graphics.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Graphics.h, part of VCMI engine
|
||||||
|
*
|
||||||
|
* Authors: listed in file AUTHORS in main folder
|
||||||
|
*
|
||||||
|
* License: GNU General Public License v2.0 or later
|
||||||
|
* Full text of license available in license.txt file, in main folder
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../lib/GameConstants.h"
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class CGHeroInstance;
|
||||||
|
class CGTownInstance;
|
||||||
|
class CHeroClass;
|
||||||
|
struct InfoAboutHero;
|
||||||
|
struct InfoAboutTown;
|
||||||
|
class CGObjectInstance;
|
||||||
|
class ObjectTemplate;
|
||||||
|
class Animation;
|
||||||
|
class EntityService;
|
||||||
|
class JsonNode;
|
||||||
|
|
||||||
|
/// Handles fonts, hero images, town images, various graphics
|
||||||
|
class Graphics
|
||||||
|
{
|
||||||
|
void addImageListEntry(size_t index, const std::string & listName, const std::string & imageName);
|
||||||
|
|
||||||
|
void addImageListEntries(const EntityService * service);
|
||||||
|
|
||||||
|
void initializeBattleGraphics();
|
||||||
|
void loadPaletteAndColors();
|
||||||
|
|
||||||
|
void loadHeroAnimations();
|
||||||
|
//loads animation and adds required rotated frames
|
||||||
|
std::shared_ptr<Animation> loadHeroAnimation(const std::string &name);
|
||||||
|
|
||||||
|
void loadHeroFlagAnimations();
|
||||||
|
|
||||||
|
//loads animation and adds required rotated frames
|
||||||
|
std::shared_ptr<Animation> loadHeroFlagAnimation(const std::string &name);
|
||||||
|
|
||||||
|
void loadErmuToPicture();
|
||||||
|
void loadFogOfWar();
|
||||||
|
void loadFonts();
|
||||||
|
void initializeImageLists();
|
||||||
|
|
||||||
|
public:
|
||||||
|
//various graphics
|
||||||
|
QVector<QRgb> playerColors; //array [8]
|
||||||
|
QRgb neutralColor;
|
||||||
|
QVector<QRgb> playerColorPalette; //palette to make interface colors good - array of size [256]
|
||||||
|
QVector<QRgb> neutralColorPalette;
|
||||||
|
|
||||||
|
// [hero class def name] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing
|
||||||
|
std::map< std::string, std::shared_ptr<Animation> > heroAnimations;
|
||||||
|
std::vector< std::shared_ptr<Animation> > heroFlagAnimations;
|
||||||
|
|
||||||
|
// [boat type: 0 .. 2] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing
|
||||||
|
std::array< std::shared_ptr<Animation>, 3> boatAnimations;
|
||||||
|
|
||||||
|
std::array< std::vector<std::shared_ptr<Animation> >, 3> boatFlagAnimations;
|
||||||
|
|
||||||
|
//all other objects (not hero or boat)
|
||||||
|
std::map< std::string, std::shared_ptr<Animation> > mapObjectAnimations;
|
||||||
|
|
||||||
|
std::map<std::string, JsonNode> imageLists;
|
||||||
|
|
||||||
|
//functions
|
||||||
|
Graphics();
|
||||||
|
~Graphics();
|
||||||
|
|
||||||
|
void load();
|
||||||
|
|
||||||
|
void blueToPlayersAdv(QImage * sur, PlayerColor player); //replaces blue interface colour with a color of player
|
||||||
|
|
||||||
|
std::shared_ptr<Animation> getAnimation(const CGObjectInstance * obj);
|
||||||
|
std::shared_ptr<Animation> getAnimation(const ObjectTemplate & info);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Graphics * graphics;
|
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "CGameInfo.h"
|
#include "CGameInfo.h"
|
||||||
#include "maphandler.h"
|
#include "maphandler.h"
|
||||||
|
#include "graphics.h"
|
||||||
|
|
||||||
static CBasicLogConfigurator * logConfig;
|
static CBasicLogConfigurator * logConfig;
|
||||||
|
|
||||||
@@ -72,6 +73,9 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||||||
CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.)
|
CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.)
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
graphics = new Graphics(); // should be before curh->init()
|
||||||
|
graphics->load();//must be after Content loading but should be in main thread
|
||||||
|
|
||||||
|
|
||||||
if(!testFile("DATA/new-menu/Background.png", "Cannot find file"))
|
if(!testFile("DATA/new-menu/Background.png", "Cannot find file"))
|
||||||
{
|
{
|
||||||
@@ -128,7 +132,15 @@ void MainWindow::on_actionOpen_triggered()
|
|||||||
{
|
{
|
||||||
for(int i = 0; i < map->width; ++i)
|
for(int i = 0; i < map->width; ++i)
|
||||||
{
|
{
|
||||||
mapHandler.drawTerrainTile(i, j, map->getTile((int3(i, j, 0))));
|
mapHandler.drawTerrainTile(i, j, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int j = 0; j < map->height; ++j)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < map->width; ++i)
|
||||||
|
{
|
||||||
|
mapHandler.drawObjects(i, j, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,23 @@
|
|||||||
#include "StdInc.h"
|
#include "StdInc.h"
|
||||||
#include "maphandler.h"
|
#include "maphandler.h"
|
||||||
|
#include "graphics.h"
|
||||||
#include "../lib/mapping/CMap.h"
|
#include "../lib/mapping/CMap.h"
|
||||||
|
#include "../lib/mapObjects/CGHeroInstance.h"
|
||||||
|
#include "../lib/mapObjects/CObjectClassesHandler.h"
|
||||||
|
#include "../lib/CHeroHandler.h"
|
||||||
|
#include "../lib/CTownHandler.h"
|
||||||
|
#include "../lib/CModHandler.h"
|
||||||
|
#include "../lib/mapping/CMap.h"
|
||||||
|
#include "../lib/GameConstants.h"
|
||||||
|
#include "../lib/JsonDetail.h"
|
||||||
|
|
||||||
const int tileSize = 32;
|
const int tileSize = 32;
|
||||||
|
|
||||||
|
static bool objectBlitOrderSorter(const TerrainTileObject & a, const TerrainTileObject & b)
|
||||||
|
{
|
||||||
|
return MapHandler::compareObjectBlitOrder(a.obj, b.obj);
|
||||||
|
}
|
||||||
|
|
||||||
MapHandler::MapHandler(const CMap * Map):
|
MapHandler::MapHandler(const CMap * Map):
|
||||||
map(Map), surface(Map->width * tileSize, Map->height * tileSize), painter(&surface)
|
map(Map), surface(Map->width * tileSize, Map->height * tileSize), painter(&surface)
|
||||||
{
|
{
|
||||||
@@ -64,10 +78,13 @@ void MapHandler::initTerrainGraphics()
|
|||||||
loadFlipped(terrainAnimations, terrainImages, terrainFiles);
|
loadFlipped(terrainAnimations, terrainImages, terrainFiles);
|
||||||
loadFlipped(roadAnimations, roadImages, ROAD_FILES);
|
loadFlipped(roadAnimations, roadImages, ROAD_FILES);
|
||||||
loadFlipped(riverAnimations, riverImages, RIVER_FILES);
|
loadFlipped(riverAnimations, riverImages, RIVER_FILES);
|
||||||
|
|
||||||
|
ttiles.resize(sizes.x * sizes.y * sizes.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapHandler::drawTerrainTile(int x, int y, const TerrainTile & tinfo)
|
void MapHandler::drawTerrainTile(int x, int y, int z)
|
||||||
{
|
{
|
||||||
|
auto & tinfo = map->getTile(int3(x, y, z));
|
||||||
//Rect destRect(realTileRect);
|
//Rect destRect(realTileRect);
|
||||||
|
|
||||||
ui8 rotation = tinfo.extTileFlags % 4;
|
ui8 rotation = tinfo.extTileFlags % 4;
|
||||||
@@ -83,7 +100,7 @@ void MapHandler::drawTerrainTile(int x, int y, const TerrainTile & tinfo)
|
|||||||
void MapHandler::initObjectRects()
|
void MapHandler::initObjectRects()
|
||||||
{
|
{
|
||||||
//initializing objects / rects
|
//initializing objects / rects
|
||||||
/*for(auto & elem : map->objects)
|
for(auto & elem : map->objects)
|
||||||
{
|
{
|
||||||
const CGObjectInstance *obj = elem;
|
const CGObjectInstance *obj = elem;
|
||||||
if( !obj
|
if( !obj
|
||||||
@@ -104,40 +121,255 @@ void MapHandler::initObjectRects()
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto image = animation->getImage(0,0);
|
auto image = animation->getImage(0,0);
|
||||||
|
bool real = true;
|
||||||
for(int fx=0; fx < obj->getWidth(); ++fx)
|
for(int fx=0; fx < obj->getWidth(); ++fx)
|
||||||
{
|
{
|
||||||
for(int fy=0; fy < obj->getHeight(); ++fy)
|
for(int fy=0; fy < obj->getHeight(); ++fy)
|
||||||
{
|
{
|
||||||
int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
|
int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
|
||||||
SDL_Rect cr;
|
QRect cr(image->width() - fx * 32 - 32, image->height() - fy * 32 - 32, image->width(), image->height());
|
||||||
cr.w = 32;
|
TerrainTileObject toAdd(obj, cr, real/*obj->visitableAt(currTile.x, currTile.y)*/);
|
||||||
cr.h = 32;
|
real = false;
|
||||||
cr.x = image->width() - fx * 32 - 32;
|
|
||||||
cr.y = image->height() - fy * 32 - 32;
|
|
||||||
TerrainTileObject toAdd(obj, cr, obj->visitableAt(currTile.x, currTile.y));
|
|
||||||
|
|
||||||
|
|
||||||
if( map->isInTheMap(currTile) && // within map
|
if( map->isInTheMap(currTile) && // within map
|
||||||
cr.x + cr.w > 0 && // image has data on this tile
|
cr.x() + cr.width() > 0 && // image has data on this tile
|
||||||
cr.y + cr.h > 0 &&
|
cr.y() + cr.height() > 0 &&
|
||||||
obj->coveringAt(currTile.x, currTile.y) // object is visible here
|
obj->coveringAt(currTile.x, currTile.y) // object is visible here
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ttiles[currTile.x][currTile.y][currTile.z].objects.push_back(toAdd);
|
ttiles[currTile.z * (sizes.x * sizes.y) + currTile.y * sizes.x + currTile.x].objects.push_back(toAdd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(int ix=0; ix<ttiles.size()-frameW; ++ix)
|
for(auto & tt : ttiles)
|
||||||
{
|
{
|
||||||
for(int iy=0; iy<ttiles[0].size()-frameH; ++iy)
|
stable_sort(tt.objects.begin(), tt.objects.end(), objectBlitOrderSorter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b)
|
||||||
|
{
|
||||||
|
if (!a)
|
||||||
|
return true;
|
||||||
|
if (!b)
|
||||||
|
return false;
|
||||||
|
if (a->appearance.printPriority != b->appearance.printPriority)
|
||||||
|
return a->appearance.printPriority > b->appearance.printPriority;
|
||||||
|
|
||||||
|
if(a->pos.y != b->pos.y)
|
||||||
|
return a->pos.y < b->pos.y;
|
||||||
|
|
||||||
|
if(b->ID==Obj::HERO && a->ID!=Obj::HERO)
|
||||||
|
return true;
|
||||||
|
if(b->ID!=Obj::HERO && a->ID==Obj::HERO)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(!a->isVisitable() && b->isVisitable())
|
||||||
|
return true;
|
||||||
|
if(!b->isVisitable() && a->isVisitable())
|
||||||
|
return false;
|
||||||
|
if(a->pos.x < b->pos.x)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerrainTileObject::TerrainTileObject(const CGObjectInstance * obj_, QRect rect_, bool real_)
|
||||||
|
: obj(obj_),
|
||||||
|
rect(rect_),
|
||||||
|
real(real_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TerrainTileObject::~TerrainTileObject()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ui8 MapHandler::getHeroFrameGroup(ui8 dir, bool isMoving) const
|
||||||
|
{
|
||||||
|
if(isMoving)
|
||||||
|
{
|
||||||
|
static const ui8 frame [] = {0xff, 10, 5, 6, 7, 8, 9, 12, 11};
|
||||||
|
return frame[dir];
|
||||||
|
}
|
||||||
|
else //if(isMoving)
|
||||||
|
{
|
||||||
|
static const ui8 frame [] = {0xff, 13, 0, 1, 2, 3, 4, 15, 14};
|
||||||
|
return frame[dir];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui8 MapHandler::getPhaseShift(const CGObjectInstance *object) const
|
||||||
|
{
|
||||||
|
auto i = animationPhase.find(object);
|
||||||
|
if(i == animationPhase.end())
|
||||||
|
{
|
||||||
|
ui8 ret = CRandomGenerator::getDefault().nextInt(254);
|
||||||
|
animationPhase[object] = ret;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
MapHandler::AnimBitmapHolder MapHandler::findHeroBitmap(const CGHeroInstance * hero, int anim) const
|
||||||
|
{
|
||||||
|
if(hero && hero->moveDir && hero->type) //it's hero or boat
|
||||||
|
{
|
||||||
|
if(hero->tempOwner >= PlayerColor::PLAYER_LIMIT) //Neutral hero?
|
||||||
{
|
{
|
||||||
for(int iz=0; iz<ttiles[0][0].size(); ++iz)
|
logGlobal->error("A neutral hero (%s) at %s. Should not happen!", hero->name, hero->pos.toString());
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
//pick graphics of hero (or boat if hero is sailing)
|
||||||
|
std::shared_ptr<Animation> animation;
|
||||||
|
if (hero->boat)
|
||||||
|
animation = graphics->boatAnimations[hero->boat->subID];
|
||||||
|
else
|
||||||
|
animation = graphics->heroAnimations[hero->appearance.animationFile];
|
||||||
|
|
||||||
|
bool moving = !hero->isStanding;
|
||||||
|
int group = getHeroFrameGroup(hero->moveDir, moving);
|
||||||
|
|
||||||
|
if(animation->size(group) > 0)
|
||||||
|
{
|
||||||
|
int frame = anim % animation->size(group);
|
||||||
|
auto heroImage = animation->getImage(frame, group);
|
||||||
|
|
||||||
|
//get flag overlay only if we have main image
|
||||||
|
auto flagImage = findFlagBitmap(hero, anim, &hero->tempOwner, group);
|
||||||
|
|
||||||
|
return MapHandler::AnimBitmapHolder(heroImage, flagImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
MapHandler::AnimBitmapHolder MapHandler::findBoatBitmap(const CGBoat * boat, int anim) const
|
||||||
|
{
|
||||||
|
auto animation = graphics->boatAnimations.at(boat->subID);
|
||||||
|
int group = getHeroFrameGroup(boat->direction, false);
|
||||||
|
if(animation->size(group) > 0)
|
||||||
|
return MapHandler::AnimBitmapHolder(animation->getImage(anim % animation->size(group), group));
|
||||||
|
else
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<QImage> MapHandler::findFlagBitmap(const CGHeroInstance * hero, int anim, const PlayerColor * color, int group) const
|
||||||
|
{
|
||||||
|
if(!hero)
|
||||||
|
return std::shared_ptr<QImage>();
|
||||||
|
|
||||||
|
if(hero->boat)
|
||||||
|
return findBoatFlagBitmap(hero->boat, anim, color, group, hero->moveDir);
|
||||||
|
return findHeroFlagBitmap(hero, anim, color, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<QImage> MapHandler::findHeroFlagBitmap(const CGHeroInstance * hero, int anim, const PlayerColor * color, int group) const
|
||||||
|
{
|
||||||
|
return findFlagBitmapInternal(graphics->heroFlagAnimations.at(color->getNum()), anim, group, hero->moveDir, !hero->isStanding);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<QImage> MapHandler::findBoatFlagBitmap(const CGBoat * boat, int anim, const PlayerColor * color, int group, ui8 dir) const
|
||||||
|
{
|
||||||
|
int boatType = boat->subID;
|
||||||
|
if(boatType < 0 || boatType >= graphics->boatFlagAnimations.size())
|
||||||
|
{
|
||||||
|
logGlobal->error("Not supported boat subtype: %d", boat->subID);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto & subtypeFlags = graphics->boatFlagAnimations.at(boatType);
|
||||||
|
|
||||||
|
int colorIndex = color->getNum();
|
||||||
|
|
||||||
|
if(colorIndex < 0 || colorIndex >= subtypeFlags.size())
|
||||||
|
{
|
||||||
|
logGlobal->error("Invalid player color %d", colorIndex);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findFlagBitmapInternal(subtypeFlags.at(colorIndex), anim, group, dir, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<QImage> MapHandler::findFlagBitmapInternal(std::shared_ptr<Animation> animation, int anim, int group, ui8 dir, bool moving) const
|
||||||
|
{
|
||||||
|
size_t groupSize = animation->size(group);
|
||||||
|
if(groupSize == 0)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if(moving)
|
||||||
|
return animation->getImage(anim % groupSize, group);
|
||||||
|
else
|
||||||
|
return animation->getImage((anim / 4) % groupSize, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MapHandler::AnimBitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim) const
|
||||||
|
{
|
||||||
|
if (!obj)
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
if (obj->ID == Obj::HERO)
|
||||||
|
return findHeroBitmap(static_cast<const CGHeroInstance*>(obj), anim);
|
||||||
|
if (obj->ID == Obj::BOAT)
|
||||||
|
return findBoatBitmap(static_cast<const CGBoat*>(obj), anim);
|
||||||
|
|
||||||
|
// normal object
|
||||||
|
std::shared_ptr<Animation> animation = graphics->getAnimation(obj);
|
||||||
|
size_t groupSize = animation->size();
|
||||||
|
if(groupSize == 0)
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
|
||||||
|
animation->playerColored(obj->tempOwner);
|
||||||
|
auto bitmap = animation->getImage((anim + getPhaseShift(obj)) % groupSize);
|
||||||
|
if(!bitmap)
|
||||||
|
return MapHandler::AnimBitmapHolder();
|
||||||
|
|
||||||
|
return MapHandler::AnimBitmapHolder(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MapHandler::drawObjects(int x, int y, int z)
|
||||||
|
{
|
||||||
|
auto & objects = ttiles[z * (sizes.x * sizes.y) + y * sizes.x + x].objects;
|
||||||
|
for(auto & object : objects)
|
||||||
|
{
|
||||||
|
if(!object.real)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//if(object.visi)
|
||||||
|
const CGObjectInstance * obj = object.obj;
|
||||||
|
if (!obj)
|
||||||
|
{
|
||||||
|
logGlobal->error("Stray map object that isn't fading");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t animationFrame = 0;
|
||||||
|
|
||||||
|
auto objData = findObjectBitmap(obj, animationFrame);
|
||||||
|
if (objData.objBitmap)
|
||||||
|
{
|
||||||
|
QRect srcRect(object.rect.x(), object.rect.y(), tileSize, tileSize);
|
||||||
|
|
||||||
|
painter.drawImage(x * tileSize, y * tileSize, *objData.objBitmap);
|
||||||
|
//drawObject(targetSurf, objData.objBitmap, &srcRect, objData.isMoving);
|
||||||
|
if (objData.flagBitmap)
|
||||||
{
|
{
|
||||||
stable_sort(ttiles[ix][iy][iz].objects.begin(), ttiles[ix][iy][iz].objects.end(), objectBlitOrderSorter);
|
/*if (objData.isMoving)
|
||||||
|
{
|
||||||
|
srcRect.y += FRAMES_PER_MOVE_ANIM_GROUP * 2 - tileSize;
|
||||||
|
Rect dstRect(realPos.x, realPos.y - tileSize / 2, tileSize, tileSize);
|
||||||
|
drawHeroFlag(targetSurf, objData.flagBitmap, &srcRect, &dstRect, true);
|
||||||
|
}
|
||||||
|
else if (obj->pos.x == pos.x && obj->pos.y == pos.y)
|
||||||
|
{
|
||||||
|
Rect dstRect(realPos.x - 2 * tileSize, realPos.y - tileSize, 3 * tileSize, 2 * tileSize);
|
||||||
|
drawHeroFlag(targetSurf, objData.flagBitmap, nullptr, &dstRect, false);
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,58 @@
|
|||||||
|
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
class CGObjectInstance;
|
||||||
|
class CGBoat;
|
||||||
|
struct PlayerColor;
|
||||||
|
|
||||||
|
struct TerrainTileObject
|
||||||
|
{
|
||||||
|
const CGObjectInstance *obj;
|
||||||
|
QRect rect;
|
||||||
|
bool real;
|
||||||
|
|
||||||
|
TerrainTileObject(const CGObjectInstance *obj_, QRect rect_, bool visitablePos = false);
|
||||||
|
~TerrainTileObject();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TerrainTile2
|
||||||
|
{
|
||||||
|
std::vector<TerrainTileObject> objects; //pointers to objects being on this tile with rects to be easier to blit this tile on screen
|
||||||
|
};
|
||||||
|
|
||||||
class MapHandler
|
class MapHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct AnimBitmapHolder
|
||||||
|
{
|
||||||
|
std::shared_ptr<QImage> objBitmap; // main object bitmap
|
||||||
|
std::shared_ptr<QImage> flagBitmap; // flag bitmap for the object (probably only for heroes and boats with heroes)
|
||||||
|
|
||||||
|
AnimBitmapHolder(std::shared_ptr<QImage> objBitmap_ = nullptr, std::shared_ptr<QImage> flagBitmap_ = nullptr)
|
||||||
|
: objBitmap(objBitmap_),
|
||||||
|
flagBitmap(flagBitmap_)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
ui8 getHeroFrameGroup(ui8 dir, bool isMoving) const;
|
||||||
|
ui8 getPhaseShift(const CGObjectInstance *object) const;
|
||||||
|
|
||||||
|
// internal helper methods to choose correct bitmap(s) for object; called internally by findObjectBitmap
|
||||||
|
AnimBitmapHolder findHeroBitmap(const CGHeroInstance * hero, int anim) const;
|
||||||
|
AnimBitmapHolder findBoatBitmap(const CGBoat * hero, int anim) const;
|
||||||
|
std::shared_ptr<QImage> findFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor * color, int group) const;
|
||||||
|
std::shared_ptr<QImage> findHeroFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor * color, int group) const;
|
||||||
|
std::shared_ptr<QImage> findBoatFlagBitmap(const CGBoat * obj, int anim, const PlayerColor * color, int group, ui8 dir) const;
|
||||||
|
std::shared_ptr<QImage> findFlagBitmapInternal(std::shared_ptr<Animation> animation, int anim, int group, ui8 dir, bool moving) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
AnimBitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim) const;
|
||||||
|
|
||||||
enum class EMapCacheType : char
|
enum class EMapCacheType : char
|
||||||
{
|
{
|
||||||
TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST
|
TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST
|
||||||
@@ -19,6 +67,7 @@ public:
|
|||||||
void initObjectRects();
|
void initObjectRects();
|
||||||
void initTerrainGraphics();
|
void initTerrainGraphics();
|
||||||
|
|
||||||
|
std::vector<TerrainTile2> ttiles; //informations about map tiles
|
||||||
int3 sizes; //map size (x = width, y = height, z = number of levels)
|
int3 sizes; //map size (x = width, y = height, z = number of levels)
|
||||||
const CMap * map;
|
const CMap * map;
|
||||||
QPixmap surface;
|
QPixmap surface;
|
||||||
@@ -39,7 +88,15 @@ public:
|
|||||||
TFlippedAnimations riverAnimations;//[river type, rotation]
|
TFlippedAnimations riverAnimations;//[river type, rotation]
|
||||||
TFlippedCache riverImages;//[river type, view type, rotation]
|
TFlippedCache riverImages;//[river type, view type, rotation]
|
||||||
|
|
||||||
void drawTerrainTile(int x, int y, const TerrainTile & tinfo);
|
void drawTerrainTile(int x, int y, int z);
|
||||||
|
/// draws a river segment on current tile
|
||||||
|
//void drawRiver(const TerrainTile & tinfo) const;
|
||||||
|
/// draws a road segment on current tile
|
||||||
|
//void drawRoad(const TerrainTile & tinfo, const TerrainTile * tinfoUpper) const;
|
||||||
|
/// draws all objects on current tile (higher-level logic, unlike other draw*** methods)
|
||||||
|
void drawObjects(int x, int y, int z);
|
||||||
|
//void drawObject(SDL_Surface * targetSurf, std::shared_ptr<IImage> source, SDL_Rect * sourceRect, bool moving) const;
|
||||||
|
//void drawHeroFlag(SDL_Surface * targetSurf, std::shared_ptr<IImage> source, SDL_Rect * sourceRect, SDL_Rect * destRect, bool moving) const;
|
||||||
|
|
||||||
mutable std::map<const CGObjectInstance*, ui8> animationPhase;
|
mutable std::map<const CGObjectInstance*, ui8> animationPhase;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user