Browse Source

Finish split of cursors files

Ivan Savenko 2 years ago
parent
commit
3f1b1095e2
5 changed files with 1627 additions and 0 deletions
  1. 458 0
      client/gui/CursorHardware.cpp
  2. 237 0
      client/gui/CursorHardware.h
  3. 458 0
      client/gui/CursorSoftware.cpp
  4. 237 0
      client/gui/CursorSoftware.h
  5. 237 0
      client/gui/ICursor.h

+ 458 - 0
client/gui/CursorHardware.cpp

@@ -0,0 +1,458 @@
+/*
+ * CCursorHandler.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 "CursorHandler.h"
+
+#include "SDL_Extensions.h"
+#include "CGuiHandler.h"
+#include "CAnimation.h"
+#include "../../lib/CConfigHandler.h"
+
+#include <SDL_render.h>
+#include <SDL_events.h>
+
+#ifdef VCMI_APPLE
+#include <dispatch/dispatch.h>
+#endif
+
+std::unique_ptr<ICursor> CursorHandler::createCursor()
+{
+	if (settings["video"]["cursor"].String() == "auto")
+	{
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+		return std::make_unique<CursorSoftware>();
+#else
+		return std::make_unique<CursorHardware>();
+#endif
+	}
+
+	if (settings["video"]["cursor"].String() == "hardware")
+		return std::make_unique<CursorHardware>();
+
+	assert(settings["video"]["cursor"].String() == "software");
+	return std::make_unique<CursorSoftware>();
+}
+
+CursorHandler::CursorHandler()
+	: cursor(createCursor())
+	, frameTime(0.f)
+	, showing(false)
+	, pos(0,0)
+{
+
+	type = Cursor::Type::DEFAULT;
+	dndObject = nullptr;
+
+	cursors =
+	{
+		std::make_unique<CAnimation>("CRADVNTR"),
+		std::make_unique<CAnimation>("CRCOMBAT"),
+		std::make_unique<CAnimation>("CRDEFLT"),
+		std::make_unique<CAnimation>("CRSPELL")
+	};
+
+	for (auto & cursor : cursors)
+		cursor->preload();
+
+	set(Cursor::Map::POINTER);
+}
+
+Point CursorHandler::position() const
+{
+	return pos;
+}
+
+void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+{
+	assert(dndObject == nullptr);
+
+	if (type == this->type && index == this->frame)
+		return;
+
+	this->type = type;
+	this->frame = index;
+
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
+void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
+{
+	dndObject = image;
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::dragAndDropCursor (std::string path, size_t index)
+{
+	CAnimation anim(path);
+	anim.load(index);
+	dragAndDropCursor(anim.getImage(index));
+}
+
+void CursorHandler::cursorMove(const int & x, const int & y)
+{
+	pos.x = x;
+	pos.y = y;
+
+	cursor->setCursorPosition(pos);
+}
+
+Point CursorHandler::getPivotOffsetDefault(size_t index)
+{
+	return {0, 0};
+}
+
+Point CursorHandler::getPivotOffsetMap(size_t index)
+{
+	static const std::array<Point, 43> offsets = {{
+		{  0,  0}, // POINTER          =  0,
+		{  0,  0}, // HOURGLASS        =  1,
+		{ 12, 10}, // HERO             =  2,
+		{ 12, 12}, // TOWN             =  3,
+
+		{ 15, 13}, // T1_MOVE          =  4,
+		{ 13, 13}, // T1_ATTACK        =  5,
+		{ 16, 32}, // T1_SAIL          =  6,
+		{ 13, 20}, // T1_DISEMBARK     =  7,
+		{  8,  9}, // T1_EXCHANGE      =  8,
+		{ 14, 16}, // T1_VISIT         =  9,
+
+		{ 15, 13}, // T2_MOVE          = 10,
+		{ 13, 13}, // T2_ATTACK        = 11,
+		{ 16, 32}, // T2_SAIL          = 12,
+		{ 13, 20}, // T2_DISEMBARK     = 13,
+		{  8,  9}, // T2_EXCHANGE      = 14,
+		{ 14, 16}, // T2_VISIT         = 15,
+
+		{ 15, 13}, // T3_MOVE          = 16,
+		{ 13, 13}, // T3_ATTACK        = 17,
+		{ 16, 32}, // T3_SAIL          = 18,
+		{ 13, 20}, // T3_DISEMBARK     = 19,
+		{  8,  9}, // T3_EXCHANGE      = 20,
+		{ 14, 16}, // T3_VISIT         = 21,
+
+		{ 15, 13}, // T4_MOVE          = 22,
+		{ 13, 13}, // T4_ATTACK        = 23,
+		{ 16, 32}, // T4_SAIL          = 24,
+		{ 13, 20}, // T4_DISEMBARK     = 25,
+		{  8,  9}, // T4_EXCHANGE      = 26,
+		{ 14, 16}, // T4_VISIT         = 27,
+
+		{ 16, 32}, // T1_SAIL_VISIT    = 28,
+		{ 16, 32}, // T2_SAIL_VISIT    = 29,
+		{ 16, 32}, // T3_SAIL_VISIT    = 30,
+		{ 16, 32}, // T4_SAIL_VISIT    = 31,
+
+		{  6,  1}, // SCROLL_NORTH     = 32,
+		{ 16,  2}, // SCROLL_NORTHEAST = 33,
+		{ 21,  6}, // SCROLL_EAST      = 34,
+		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
+		{  6, 21}, // SCROLL_SOUTH     = 36,
+		{  1, 16}, // SCROLL_SOUTHWEST = 37,
+		{  1,  5}, // SCROLL_WEST      = 38,
+		{  2,  1}, // SCROLL_NORTHWEST = 39,
+
+		{  0,  0}, // POINTER_COPY     = 40,
+		{ 14, 16}, // TELEPORT         = 41,
+		{ 20, 20}, // SCUTTLE_BOAT     = 42
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetCombat(size_t index)
+{
+	static const std::array<Point, 20> offsets = {{
+		{ 12, 12 }, // BLOCKED        = 0,
+		{ 10, 14 }, // MOVE           = 1,
+		{ 14, 14 }, // FLY            = 2,
+		{ 12, 12 }, // SHOOT          = 3,
+		{ 12, 12 }, // HERO           = 4,
+		{  8, 12 }, // QUERY          = 5,
+		{  0,  0 }, // POINTER        = 6,
+		{ 21,  0 }, // HIT_NORTHEAST  = 7,
+		{ 31,  5 }, // HIT_EAST       = 8,
+		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
+		{  0, 21 }, // HIT_SOUTHWEST  = 10,
+		{  0,  5 }, // HIT_WEST       = 11,
+		{  0,  0 }, // HIT_NORTHWEST  = 12,
+		{  6,  0 }, // HIT_NORTH      = 13,
+		{  6, 31 }, // HIT_SOUTH      = 14,
+		{ 14,  0 }, // SHOOT_PENALTY  = 15,
+		{ 12, 12 }, // SHOOT_CATAPULT = 16,
+		{ 12, 12 }, // HEAL           = 17,
+		{ 12, 12 }, // SACRIFICE      = 18,
+		{ 14, 20 }, // TELEPORT       = 19
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetSpellcast()
+{
+	return { 18, 28};
+}
+
+Point CursorHandler::getPivotOffset()
+{
+	if (dndObject)
+		return dndObject->dimensions() / 2;
+
+	switch (type) {
+	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
+	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
+	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
+	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
+	};
+
+	assert(0);
+	return {0, 0};
+}
+
+std::shared_ptr<IImage> CursorHandler::getCurrentImage()
+{
+	if (dndObject)
+		return dndObject;
+
+	return cursors[static_cast<size_t>(type)]->getImage(frame);
+}
+
+void CursorHandler::centerCursor()
+{
+	Point screenSize {screen->w, screen->h};
+	pos = screenSize / 2 - getPivotOffset();
+
+	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
+	CSDL_Ext::warpMouse(pos.x, pos.y);
+	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+
+	cursor->setCursorPosition(pos);
+}
+
+void CursorHandler::updateSpellcastCursor()
+{
+	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
+
+	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	size_t newFrame = frame;
+
+	while (frameTime >= frameDisplayDuration)
+	{
+		frameTime -= frameDisplayDuration;
+		newFrame++;
+	}
+
+	auto & animation = cursors.at(static_cast<size_t>(type));
+
+	while (newFrame >= animation->size())
+		newFrame -= animation->size();
+
+	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+}
+
+void CursorHandler::render()
+{
+	if(!showing)
+		return;
+
+	if (type == Cursor::Type::SPELLBOOK)
+		updateSpellcastCursor();
+
+	cursor->render();
+}
+
+void CursorHandler::hide()
+{
+	if (!showing)
+		return;
+
+	showing = false;
+	cursor->setVisible(false);
+}
+
+void CursorHandler::show()
+{
+	if (showing)
+		return;
+
+	showing = true;
+	cursor->setVisible(true);
+}
+
+void CursorSoftware::render()
+{
+	//texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads
+	if (needUpdate)
+		updateTexture();
+
+	Point renderPos = pos - pivot;
+
+	SDL_Rect destRect;
+	destRect.x = renderPos.x;
+	destRect.y = renderPos.y;
+	destRect.w = 40;
+	destRect.h = 40;
+
+	SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect);
+}
+
+void CursorSoftware::createTexture(const Point & dimensions)
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+	cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y);
+	cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+
+	SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE);
+	SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND);
+}
+
+void CursorSoftware::updateTexture()
+{
+	Point dimensions(-1, -1);
+
+	if (!cursorSurface ||  Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions())
+		createTexture(cursorImage->dimensions());
+
+	CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY);
+
+	cursorImage->draw(cursorSurface);
+	SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch);
+	needUpdate = false;
+}
+
+void CursorSoftware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	assert(image != nullptr);
+	cursorImage = image;
+	pivot = pivotOffset;
+	needUpdate = true;
+}
+
+void CursorSoftware::setCursorPosition( const Point & newPos )
+{
+	pos = newPos;
+}
+
+void CursorSoftware::setVisible(bool on)
+{
+	visible = on;
+}
+
+CursorSoftware::CursorSoftware():
+	cursorTexture(nullptr),
+	cursorSurface(nullptr),
+	needUpdate(false),
+	visible(false),
+	pivot(0,0)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorSoftware::~CursorSoftware()
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+}
+
+CursorHardware::CursorHardware():
+	cursor(nullptr)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorHardware::~CursorHardware()
+{
+	if(cursor)
+		SDL_FreeCursor(cursor);
+}
+
+void CursorHardware::setVisible(bool on)
+{
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+	if (on)
+		SDL_ShowCursor(SDL_ENABLE);
+	else
+		SDL_ShowCursor(SDL_DISABLE);
+#ifdef VCMI_APPLE
+	});
+#endif
+}
+
+void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y);
+
+	CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY);
+
+	image->draw(cursorSurface);
+
+	auto oldCursor = cursor;
+	cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y);
+
+	if (!cursor)
+		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
+
+	SDL_FreeSurface(cursorSurface);
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+	SDL_SetCursor(cursor);
+
+	if (oldCursor)
+		SDL_FreeCursor(oldCursor);
+#ifdef VCMI_APPLE
+	});
+#endif
+}
+
+void CursorHardware::setCursorPosition( const Point & newPos )
+{
+	//no-op
+}
+
+void CursorHardware::render()
+{
+	//no-op
+}

+ 237 - 0
client/gui/CursorHardware.h

@@ -0,0 +1,237 @@
+/*
+ * CCursorHandler.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
+
+class CAnimation;
+class IImage;
+struct SDL_Surface;
+struct SDL_Texture;
+struct SDL_Cursor;
+
+#include "../../lib/Point.h"
+
+namespace Cursor
+{
+	enum class Type {
+		ADVENTURE, // set of various cursors for adventure map
+		COMBAT,    // set of various cursors for combat
+		DEFAULT,   // default arrow and hourglass cursors
+		SPELLBOOK  // animated cursor for spellcasting
+	};
+
+	enum class Default {
+		POINTER      = 0,
+		//ARROW_COPY = 1, // probably unused
+		HOURGLASS  = 2,
+	};
+
+	enum class Combat {
+		INVALID        = -1,
+
+		BLOCKED        = 0,
+		MOVE           = 1,
+		FLY            = 2,
+		SHOOT          = 3,
+		HERO           = 4,
+		QUERY          = 5,
+		POINTER        = 6,
+		HIT_NORTHEAST  = 7,
+		HIT_EAST       = 8,
+		HIT_SOUTHEAST  = 9,
+		HIT_SOUTHWEST  = 10,
+		HIT_WEST       = 11,
+		HIT_NORTHWEST  = 12,
+		HIT_NORTH      = 13,
+		HIT_SOUTH      = 14,
+		SHOOT_PENALTY  = 15,
+		SHOOT_CATAPULT = 16,
+		HEAL           = 17,
+		SACRIFICE      = 18,
+		TELEPORT       = 19,
+
+		COUNT
+	};
+
+	enum class Map {
+		POINTER          =  0,
+		HOURGLASS        =  1,
+		HERO             =  2,
+		TOWN             =  3,
+		T1_MOVE          =  4,
+		T1_ATTACK        =  5,
+		T1_SAIL          =  6,
+		T1_DISEMBARK     =  7,
+		T1_EXCHANGE      =  8,
+		T1_VISIT         =  9,
+		T2_MOVE          = 10,
+		T2_ATTACK        = 11,
+		T2_SAIL          = 12,
+		T2_DISEMBARK     = 13,
+		T2_EXCHANGE      = 14,
+		T2_VISIT         = 15,
+		T3_MOVE          = 16,
+		T3_ATTACK        = 17,
+		T3_SAIL          = 18,
+		T3_DISEMBARK     = 19,
+		T3_EXCHANGE      = 20,
+		T3_VISIT         = 21,
+		T4_MOVE          = 22,
+		T4_ATTACK        = 23,
+		T4_SAIL          = 24,
+		T4_DISEMBARK     = 25,
+		T4_EXCHANGE      = 26,
+		T4_VISIT         = 27,
+		T1_SAIL_VISIT    = 28,
+		T2_SAIL_VISIT    = 29,
+		T3_SAIL_VISIT    = 30,
+		T4_SAIL_VISIT    = 31,
+		SCROLL_NORTH     = 32,
+		SCROLL_NORTHEAST = 33,
+		SCROLL_EAST      = 34,
+		SCROLL_SOUTHEAST = 35,
+		SCROLL_SOUTH     = 36,
+		SCROLL_SOUTHWEST = 37,
+		SCROLL_WEST      = 38,
+		SCROLL_NORTHWEST = 39,
+		//POINTER_COPY       = 40, // probably unused
+		TELEPORT         = 41,
+		SCUTTLE_BOAT     = 42,
+
+		COUNT
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
+}
+
+class ICursor
+{
+public:
+	virtual ~ICursor() = default;
+
+	virtual void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) = 0;
+	virtual void setCursorPosition( const Point & newPos ) = 0;
+	virtual void render() = 0;
+	virtual void setVisible( bool on) = 0;
+};
+
+class CursorHardware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Cursor * cursor;
+
+public:
+	CursorHardware();
+	~CursorHardware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+class CursorSoftware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Texture * cursorTexture;
+	SDL_Surface * cursorSurface;
+
+	Point pos;
+	Point pivot;
+	bool needUpdate;
+	bool visible;
+
+	void createTexture(const Point & dimensions);
+	void updateTexture();
+public:
+	CursorSoftware();
+	~CursorSoftware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::unique_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
+
+	/// Current cursor
+	Cursor::Type type;
+	size_t frame;
+	float frameTime;
+	Point pos;
+
+	void changeGraphic(Cursor::Type type, size_t index);
+
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
+
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
+public:
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(std::string path, size_t index);
+
+	/// Returns current position of the cursor
+	Point position() const;
+
+	/// Changes cursor to specified index
+	void set(Cursor::Default index);
+	void set(Cursor::Map index);
+	void set(Cursor::Combat index);
+	void set(Cursor::Spellcast index);
+
+	/// Returns current index of cursor
+	template<typename Index>
+	Index get()
+	{
+		assert((std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT );
+		assert((std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE );
+		assert((std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT );
+		assert((std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK );
+
+		return static_cast<Index>(frame);
+	}
+
+	void render();
+
+	void hide();
+	void show();
+
+	/// change cursor's positions to (x, y)
+	void cursorMove(const int & x, const int & y);
+	/// Move cursor to screen center
+	void centerCursor();
+
+};

+ 458 - 0
client/gui/CursorSoftware.cpp

@@ -0,0 +1,458 @@
+/*
+ * CCursorHandler.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 "CursorHandler.h"
+
+#include "SDL_Extensions.h"
+#include "CGuiHandler.h"
+#include "CAnimation.h"
+#include "../../lib/CConfigHandler.h"
+
+#include <SDL_render.h>
+#include <SDL_events.h>
+
+#ifdef VCMI_APPLE
+#include <dispatch/dispatch.h>
+#endif
+
+std::unique_ptr<ICursor> CursorHandler::createCursor()
+{
+	if (settings["video"]["cursor"].String() == "auto")
+	{
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+		return std::make_unique<CursorSoftware>();
+#else
+		return std::make_unique<CursorHardware>();
+#endif
+	}
+
+	if (settings["video"]["cursor"].String() == "hardware")
+		return std::make_unique<CursorHardware>();
+
+	assert(settings["video"]["cursor"].String() == "software");
+	return std::make_unique<CursorSoftware>();
+}
+
+CursorHandler::CursorHandler()
+	: cursor(createCursor())
+	, frameTime(0.f)
+	, showing(false)
+	, pos(0,0)
+{
+
+	type = Cursor::Type::DEFAULT;
+	dndObject = nullptr;
+
+	cursors =
+	{
+		std::make_unique<CAnimation>("CRADVNTR"),
+		std::make_unique<CAnimation>("CRCOMBAT"),
+		std::make_unique<CAnimation>("CRDEFLT"),
+		std::make_unique<CAnimation>("CRSPELL")
+	};
+
+	for (auto & cursor : cursors)
+		cursor->preload();
+
+	set(Cursor::Map::POINTER);
+}
+
+Point CursorHandler::position() const
+{
+	return pos;
+}
+
+void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+{
+	assert(dndObject == nullptr);
+
+	if (type == this->type && index == this->frame)
+		return;
+
+	this->type = type;
+	this->frame = index;
+
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
+void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
+{
+	dndObject = image;
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::dragAndDropCursor (std::string path, size_t index)
+{
+	CAnimation anim(path);
+	anim.load(index);
+	dragAndDropCursor(anim.getImage(index));
+}
+
+void CursorHandler::cursorMove(const int & x, const int & y)
+{
+	pos.x = x;
+	pos.y = y;
+
+	cursor->setCursorPosition(pos);
+}
+
+Point CursorHandler::getPivotOffsetDefault(size_t index)
+{
+	return {0, 0};
+}
+
+Point CursorHandler::getPivotOffsetMap(size_t index)
+{
+	static const std::array<Point, 43> offsets = {{
+		{  0,  0}, // POINTER          =  0,
+		{  0,  0}, // HOURGLASS        =  1,
+		{ 12, 10}, // HERO             =  2,
+		{ 12, 12}, // TOWN             =  3,
+
+		{ 15, 13}, // T1_MOVE          =  4,
+		{ 13, 13}, // T1_ATTACK        =  5,
+		{ 16, 32}, // T1_SAIL          =  6,
+		{ 13, 20}, // T1_DISEMBARK     =  7,
+		{  8,  9}, // T1_EXCHANGE      =  8,
+		{ 14, 16}, // T1_VISIT         =  9,
+
+		{ 15, 13}, // T2_MOVE          = 10,
+		{ 13, 13}, // T2_ATTACK        = 11,
+		{ 16, 32}, // T2_SAIL          = 12,
+		{ 13, 20}, // T2_DISEMBARK     = 13,
+		{  8,  9}, // T2_EXCHANGE      = 14,
+		{ 14, 16}, // T2_VISIT         = 15,
+
+		{ 15, 13}, // T3_MOVE          = 16,
+		{ 13, 13}, // T3_ATTACK        = 17,
+		{ 16, 32}, // T3_SAIL          = 18,
+		{ 13, 20}, // T3_DISEMBARK     = 19,
+		{  8,  9}, // T3_EXCHANGE      = 20,
+		{ 14, 16}, // T3_VISIT         = 21,
+
+		{ 15, 13}, // T4_MOVE          = 22,
+		{ 13, 13}, // T4_ATTACK        = 23,
+		{ 16, 32}, // T4_SAIL          = 24,
+		{ 13, 20}, // T4_DISEMBARK     = 25,
+		{  8,  9}, // T4_EXCHANGE      = 26,
+		{ 14, 16}, // T4_VISIT         = 27,
+
+		{ 16, 32}, // T1_SAIL_VISIT    = 28,
+		{ 16, 32}, // T2_SAIL_VISIT    = 29,
+		{ 16, 32}, // T3_SAIL_VISIT    = 30,
+		{ 16, 32}, // T4_SAIL_VISIT    = 31,
+
+		{  6,  1}, // SCROLL_NORTH     = 32,
+		{ 16,  2}, // SCROLL_NORTHEAST = 33,
+		{ 21,  6}, // SCROLL_EAST      = 34,
+		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
+		{  6, 21}, // SCROLL_SOUTH     = 36,
+		{  1, 16}, // SCROLL_SOUTHWEST = 37,
+		{  1,  5}, // SCROLL_WEST      = 38,
+		{  2,  1}, // SCROLL_NORTHWEST = 39,
+
+		{  0,  0}, // POINTER_COPY     = 40,
+		{ 14, 16}, // TELEPORT         = 41,
+		{ 20, 20}, // SCUTTLE_BOAT     = 42
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetCombat(size_t index)
+{
+	static const std::array<Point, 20> offsets = {{
+		{ 12, 12 }, // BLOCKED        = 0,
+		{ 10, 14 }, // MOVE           = 1,
+		{ 14, 14 }, // FLY            = 2,
+		{ 12, 12 }, // SHOOT          = 3,
+		{ 12, 12 }, // HERO           = 4,
+		{  8, 12 }, // QUERY          = 5,
+		{  0,  0 }, // POINTER        = 6,
+		{ 21,  0 }, // HIT_NORTHEAST  = 7,
+		{ 31,  5 }, // HIT_EAST       = 8,
+		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
+		{  0, 21 }, // HIT_SOUTHWEST  = 10,
+		{  0,  5 }, // HIT_WEST       = 11,
+		{  0,  0 }, // HIT_NORTHWEST  = 12,
+		{  6,  0 }, // HIT_NORTH      = 13,
+		{  6, 31 }, // HIT_SOUTH      = 14,
+		{ 14,  0 }, // SHOOT_PENALTY  = 15,
+		{ 12, 12 }, // SHOOT_CATAPULT = 16,
+		{ 12, 12 }, // HEAL           = 17,
+		{ 12, 12 }, // SACRIFICE      = 18,
+		{ 14, 20 }, // TELEPORT       = 19
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetSpellcast()
+{
+	return { 18, 28};
+}
+
+Point CursorHandler::getPivotOffset()
+{
+	if (dndObject)
+		return dndObject->dimensions() / 2;
+
+	switch (type) {
+	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
+	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
+	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
+	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
+	};
+
+	assert(0);
+	return {0, 0};
+}
+
+std::shared_ptr<IImage> CursorHandler::getCurrentImage()
+{
+	if (dndObject)
+		return dndObject;
+
+	return cursors[static_cast<size_t>(type)]->getImage(frame);
+}
+
+void CursorHandler::centerCursor()
+{
+	Point screenSize {screen->w, screen->h};
+	pos = screenSize / 2 - getPivotOffset();
+
+	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
+	CSDL_Ext::warpMouse(pos.x, pos.y);
+	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+
+	cursor->setCursorPosition(pos);
+}
+
+void CursorHandler::updateSpellcastCursor()
+{
+	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
+
+	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	size_t newFrame = frame;
+
+	while (frameTime >= frameDisplayDuration)
+	{
+		frameTime -= frameDisplayDuration;
+		newFrame++;
+	}
+
+	auto & animation = cursors.at(static_cast<size_t>(type));
+
+	while (newFrame >= animation->size())
+		newFrame -= animation->size();
+
+	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+}
+
+void CursorHandler::render()
+{
+	if(!showing)
+		return;
+
+	if (type == Cursor::Type::SPELLBOOK)
+		updateSpellcastCursor();
+
+	cursor->render();
+}
+
+void CursorHandler::hide()
+{
+	if (!showing)
+		return;
+
+	showing = false;
+	cursor->setVisible(false);
+}
+
+void CursorHandler::show()
+{
+	if (showing)
+		return;
+
+	showing = true;
+	cursor->setVisible(true);
+}
+
+void CursorSoftware::render()
+{
+	//texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads
+	if (needUpdate)
+		updateTexture();
+
+	Point renderPos = pos - pivot;
+
+	SDL_Rect destRect;
+	destRect.x = renderPos.x;
+	destRect.y = renderPos.y;
+	destRect.w = 40;
+	destRect.h = 40;
+
+	SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect);
+}
+
+void CursorSoftware::createTexture(const Point & dimensions)
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+	cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y);
+	cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+
+	SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE);
+	SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND);
+}
+
+void CursorSoftware::updateTexture()
+{
+	Point dimensions(-1, -1);
+
+	if (!cursorSurface ||  Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions())
+		createTexture(cursorImage->dimensions());
+
+	CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY);
+
+	cursorImage->draw(cursorSurface);
+	SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch);
+	needUpdate = false;
+}
+
+void CursorSoftware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	assert(image != nullptr);
+	cursorImage = image;
+	pivot = pivotOffset;
+	needUpdate = true;
+}
+
+void CursorSoftware::setCursorPosition( const Point & newPos )
+{
+	pos = newPos;
+}
+
+void CursorSoftware::setVisible(bool on)
+{
+	visible = on;
+}
+
+CursorSoftware::CursorSoftware():
+	cursorTexture(nullptr),
+	cursorSurface(nullptr),
+	needUpdate(false),
+	visible(false),
+	pivot(0,0)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorSoftware::~CursorSoftware()
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+}
+
+CursorHardware::CursorHardware():
+	cursor(nullptr)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorHardware::~CursorHardware()
+{
+	if(cursor)
+		SDL_FreeCursor(cursor);
+}
+
+void CursorHardware::setVisible(bool on)
+{
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+	if (on)
+		SDL_ShowCursor(SDL_ENABLE);
+	else
+		SDL_ShowCursor(SDL_DISABLE);
+#ifdef VCMI_APPLE
+	});
+#endif
+}
+
+void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y);
+
+	CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY);
+
+	image->draw(cursorSurface);
+
+	auto oldCursor = cursor;
+	cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y);
+
+	if (!cursor)
+		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
+
+	SDL_FreeSurface(cursorSurface);
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+	SDL_SetCursor(cursor);
+
+	if (oldCursor)
+		SDL_FreeCursor(oldCursor);
+#ifdef VCMI_APPLE
+	});
+#endif
+}
+
+void CursorHardware::setCursorPosition( const Point & newPos )
+{
+	//no-op
+}
+
+void CursorHardware::render()
+{
+	//no-op
+}

+ 237 - 0
client/gui/CursorSoftware.h

@@ -0,0 +1,237 @@
+/*
+ * CCursorHandler.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
+
+class CAnimation;
+class IImage;
+struct SDL_Surface;
+struct SDL_Texture;
+struct SDL_Cursor;
+
+#include "../../lib/Point.h"
+
+namespace Cursor
+{
+	enum class Type {
+		ADVENTURE, // set of various cursors for adventure map
+		COMBAT,    // set of various cursors for combat
+		DEFAULT,   // default arrow and hourglass cursors
+		SPELLBOOK  // animated cursor for spellcasting
+	};
+
+	enum class Default {
+		POINTER      = 0,
+		//ARROW_COPY = 1, // probably unused
+		HOURGLASS  = 2,
+	};
+
+	enum class Combat {
+		INVALID        = -1,
+
+		BLOCKED        = 0,
+		MOVE           = 1,
+		FLY            = 2,
+		SHOOT          = 3,
+		HERO           = 4,
+		QUERY          = 5,
+		POINTER        = 6,
+		HIT_NORTHEAST  = 7,
+		HIT_EAST       = 8,
+		HIT_SOUTHEAST  = 9,
+		HIT_SOUTHWEST  = 10,
+		HIT_WEST       = 11,
+		HIT_NORTHWEST  = 12,
+		HIT_NORTH      = 13,
+		HIT_SOUTH      = 14,
+		SHOOT_PENALTY  = 15,
+		SHOOT_CATAPULT = 16,
+		HEAL           = 17,
+		SACRIFICE      = 18,
+		TELEPORT       = 19,
+
+		COUNT
+	};
+
+	enum class Map {
+		POINTER          =  0,
+		HOURGLASS        =  1,
+		HERO             =  2,
+		TOWN             =  3,
+		T1_MOVE          =  4,
+		T1_ATTACK        =  5,
+		T1_SAIL          =  6,
+		T1_DISEMBARK     =  7,
+		T1_EXCHANGE      =  8,
+		T1_VISIT         =  9,
+		T2_MOVE          = 10,
+		T2_ATTACK        = 11,
+		T2_SAIL          = 12,
+		T2_DISEMBARK     = 13,
+		T2_EXCHANGE      = 14,
+		T2_VISIT         = 15,
+		T3_MOVE          = 16,
+		T3_ATTACK        = 17,
+		T3_SAIL          = 18,
+		T3_DISEMBARK     = 19,
+		T3_EXCHANGE      = 20,
+		T3_VISIT         = 21,
+		T4_MOVE          = 22,
+		T4_ATTACK        = 23,
+		T4_SAIL          = 24,
+		T4_DISEMBARK     = 25,
+		T4_EXCHANGE      = 26,
+		T4_VISIT         = 27,
+		T1_SAIL_VISIT    = 28,
+		T2_SAIL_VISIT    = 29,
+		T3_SAIL_VISIT    = 30,
+		T4_SAIL_VISIT    = 31,
+		SCROLL_NORTH     = 32,
+		SCROLL_NORTHEAST = 33,
+		SCROLL_EAST      = 34,
+		SCROLL_SOUTHEAST = 35,
+		SCROLL_SOUTH     = 36,
+		SCROLL_SOUTHWEST = 37,
+		SCROLL_WEST      = 38,
+		SCROLL_NORTHWEST = 39,
+		//POINTER_COPY       = 40, // probably unused
+		TELEPORT         = 41,
+		SCUTTLE_BOAT     = 42,
+
+		COUNT
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
+}
+
+class ICursor
+{
+public:
+	virtual ~ICursor() = default;
+
+	virtual void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) = 0;
+	virtual void setCursorPosition( const Point & newPos ) = 0;
+	virtual void render() = 0;
+	virtual void setVisible( bool on) = 0;
+};
+
+class CursorHardware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Cursor * cursor;
+
+public:
+	CursorHardware();
+	~CursorHardware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+class CursorSoftware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Texture * cursorTexture;
+	SDL_Surface * cursorSurface;
+
+	Point pos;
+	Point pivot;
+	bool needUpdate;
+	bool visible;
+
+	void createTexture(const Point & dimensions);
+	void updateTexture();
+public:
+	CursorSoftware();
+	~CursorSoftware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::unique_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
+
+	/// Current cursor
+	Cursor::Type type;
+	size_t frame;
+	float frameTime;
+	Point pos;
+
+	void changeGraphic(Cursor::Type type, size_t index);
+
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
+
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
+public:
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(std::string path, size_t index);
+
+	/// Returns current position of the cursor
+	Point position() const;
+
+	/// Changes cursor to specified index
+	void set(Cursor::Default index);
+	void set(Cursor::Map index);
+	void set(Cursor::Combat index);
+	void set(Cursor::Spellcast index);
+
+	/// Returns current index of cursor
+	template<typename Index>
+	Index get()
+	{
+		assert((std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT );
+		assert((std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE );
+		assert((std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT );
+		assert((std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK );
+
+		return static_cast<Index>(frame);
+	}
+
+	void render();
+
+	void hide();
+	void show();
+
+	/// change cursor's positions to (x, y)
+	void cursorMove(const int & x, const int & y);
+	/// Move cursor to screen center
+	void centerCursor();
+
+};

+ 237 - 0
client/gui/ICursor.h

@@ -0,0 +1,237 @@
+/*
+ * CCursorHandler.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
+
+class CAnimation;
+class IImage;
+struct SDL_Surface;
+struct SDL_Texture;
+struct SDL_Cursor;
+
+#include "../../lib/Point.h"
+
+namespace Cursor
+{
+	enum class Type {
+		ADVENTURE, // set of various cursors for adventure map
+		COMBAT,    // set of various cursors for combat
+		DEFAULT,   // default arrow and hourglass cursors
+		SPELLBOOK  // animated cursor for spellcasting
+	};
+
+	enum class Default {
+		POINTER      = 0,
+		//ARROW_COPY = 1, // probably unused
+		HOURGLASS  = 2,
+	};
+
+	enum class Combat {
+		INVALID        = -1,
+
+		BLOCKED        = 0,
+		MOVE           = 1,
+		FLY            = 2,
+		SHOOT          = 3,
+		HERO           = 4,
+		QUERY          = 5,
+		POINTER        = 6,
+		HIT_NORTHEAST  = 7,
+		HIT_EAST       = 8,
+		HIT_SOUTHEAST  = 9,
+		HIT_SOUTHWEST  = 10,
+		HIT_WEST       = 11,
+		HIT_NORTHWEST  = 12,
+		HIT_NORTH      = 13,
+		HIT_SOUTH      = 14,
+		SHOOT_PENALTY  = 15,
+		SHOOT_CATAPULT = 16,
+		HEAL           = 17,
+		SACRIFICE      = 18,
+		TELEPORT       = 19,
+
+		COUNT
+	};
+
+	enum class Map {
+		POINTER          =  0,
+		HOURGLASS        =  1,
+		HERO             =  2,
+		TOWN             =  3,
+		T1_MOVE          =  4,
+		T1_ATTACK        =  5,
+		T1_SAIL          =  6,
+		T1_DISEMBARK     =  7,
+		T1_EXCHANGE      =  8,
+		T1_VISIT         =  9,
+		T2_MOVE          = 10,
+		T2_ATTACK        = 11,
+		T2_SAIL          = 12,
+		T2_DISEMBARK     = 13,
+		T2_EXCHANGE      = 14,
+		T2_VISIT         = 15,
+		T3_MOVE          = 16,
+		T3_ATTACK        = 17,
+		T3_SAIL          = 18,
+		T3_DISEMBARK     = 19,
+		T3_EXCHANGE      = 20,
+		T3_VISIT         = 21,
+		T4_MOVE          = 22,
+		T4_ATTACK        = 23,
+		T4_SAIL          = 24,
+		T4_DISEMBARK     = 25,
+		T4_EXCHANGE      = 26,
+		T4_VISIT         = 27,
+		T1_SAIL_VISIT    = 28,
+		T2_SAIL_VISIT    = 29,
+		T3_SAIL_VISIT    = 30,
+		T4_SAIL_VISIT    = 31,
+		SCROLL_NORTH     = 32,
+		SCROLL_NORTHEAST = 33,
+		SCROLL_EAST      = 34,
+		SCROLL_SOUTHEAST = 35,
+		SCROLL_SOUTH     = 36,
+		SCROLL_SOUTHWEST = 37,
+		SCROLL_WEST      = 38,
+		SCROLL_NORTHWEST = 39,
+		//POINTER_COPY       = 40, // probably unused
+		TELEPORT         = 41,
+		SCUTTLE_BOAT     = 42,
+
+		COUNT
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
+}
+
+class ICursor
+{
+public:
+	virtual ~ICursor() = default;
+
+	virtual void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) = 0;
+	virtual void setCursorPosition( const Point & newPos ) = 0;
+	virtual void render() = 0;
+	virtual void setVisible( bool on) = 0;
+};
+
+class CursorHardware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Cursor * cursor;
+
+public:
+	CursorHardware();
+	~CursorHardware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+class CursorSoftware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Texture * cursorTexture;
+	SDL_Surface * cursorSurface;
+
+	Point pos;
+	Point pivot;
+	bool needUpdate;
+	bool visible;
+
+	void createTexture(const Point & dimensions);
+	void updateTexture();
+public:
+	CursorSoftware();
+	~CursorSoftware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+	void setVisible( bool on) override;
+};
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::unique_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
+
+	/// Current cursor
+	Cursor::Type type;
+	size_t frame;
+	float frameTime;
+	Point pos;
+
+	void changeGraphic(Cursor::Type type, size_t index);
+
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
+
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
+public:
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(std::string path, size_t index);
+
+	/// Returns current position of the cursor
+	Point position() const;
+
+	/// Changes cursor to specified index
+	void set(Cursor::Default index);
+	void set(Cursor::Map index);
+	void set(Cursor::Combat index);
+	void set(Cursor::Spellcast index);
+
+	/// Returns current index of cursor
+	template<typename Index>
+	Index get()
+	{
+		assert((std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT );
+		assert((std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE );
+		assert((std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT );
+		assert((std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK );
+
+		return static_cast<Index>(frame);
+	}
+
+	void render();
+
+	void hide();
+	void show();
+
+	/// change cursor's positions to (x, y)
+	void cursorMove(const int & x, const int & y);
+	/// Move cursor to screen center
+	void centerCursor();
+
+};