1
0
Fork 0

Compare commits

...

6 Commits

Author SHA1 Message Date
Michał Janiszewski 49c022a8f5
Merge e6b45731c0 into a8650c6b06 2025-07-19 22:51:25 +00:00
Peter Nelson a8650c6b06
Codechange: Make SpriteCacheCtrlFlags an enum bit set. (#14462)
Due to header dependencies, this requires types to split from the spritecache header.
2025-07-19 23:49:15 +01:00
Michał Janiszewski e6b45731c0 Change: Limit gamepad scroll only to windows that don't follow vehicles 2025-07-17 00:06:31 +02:00
Michał Janiszewski 24f173e7a8 Add: Select viewport under the cursor for gamepad scrolling 2025-07-17 00:06:31 +02:00
Michał Janiszewski ccfb40944b Feature: Win32 driver support for gamepad scrolling 2025-07-17 00:06:31 +02:00
Michał Janiszewski 5b0e9f82d5 Feature: Add gamepad scrolling support (SDL2) 2025-07-17 00:06:31 +02:00
22 changed files with 482 additions and 33 deletions

View File

@ -449,6 +449,7 @@ if(WIN32)
psapi psapi
winhttp winhttp
bcrypt bcrypt
xinput
) )
endif() endif()

View File

@ -454,6 +454,7 @@ add_files(
spritecache.cpp spritecache.cpp
spritecache.h spritecache.h
spritecache_internal.h spritecache_internal.h
spritecache_type.h
station.cpp station.cpp
station_base.h station_base.h
station_cmd.cpp station_cmd.cpp

View File

@ -75,6 +75,7 @@ void HandleTextInput(std::string_view str, bool marked = false,
std::optional<size_t> insert_location = std::nullopt, std::optional<size_t> replacement_end = std::nullopt); std::optional<size_t> insert_location = std::nullopt, std::optional<size_t> replacement_end = std::nullopt);
void HandleCtrlChanged(); void HandleCtrlChanged();
void HandleMouseEvents(); void HandleMouseEvents();
void HandleGamepadScrolling(int stick_x, int stick_y, int max_axis_value);
void UpdateWindows(); void UpdateWindows();
void ChangeGameSpeed(bool enable_fast_forward); void ChangeGameSpeed(bool enable_fast_forward);

View File

@ -1698,6 +1698,25 @@ STR_CONFIG_SETTING_SCROLLWHEEL_ZOOM :Zoom map
STR_CONFIG_SETTING_SCROLLWHEEL_SCROLL :Scroll map STR_CONFIG_SETTING_SCROLLWHEEL_SCROLL :Scroll map
STR_CONFIG_SETTING_SCROLLWHEEL_OFF :Off STR_CONFIG_SETTING_SCROLLWHEEL_OFF :Off
STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION :Gamepad stick for scrolling: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION_HELPTEXT :Select which analog stick to use for map scrolling
###length 3
STR_CONFIG_SETTING_GAMEPAD_STICK_DISABLED :Disabled
STR_CONFIG_SETTING_GAMEPAD_STICK_LEFT :Left stick
STR_CONFIG_SETTING_GAMEPAD_STICK_RIGHT :Right stick
STR_CONFIG_SETTING_GAMEPAD_DEADZONE :Gamepad deadzone: {STRING2}%
STR_CONFIG_SETTING_GAMEPAD_DEADZONE_HELPTEXT :Minimum stick movement required before scrolling starts (0-100%)
STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY :Gamepad sensitivity: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY_HELPTEXT :Control the sensitivity of gamepad analog stick scrolling
STR_CONFIG_SETTING_GAMEPAD_INVERT_X :Invert gamepad X-axis: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_INVERT_X_HELPTEXT :Invert the horizontal axis movement of the gamepad analog stick
STR_CONFIG_SETTING_GAMEPAD_INVERT_Y :Invert gamepad Y-axis: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_INVERT_Y_HELPTEXT :Invert the vertical axis movement of the gamepad analog stick
STR_CONFIG_SETTING_OSK_ACTIVATION :On screen keyboard: {STRING2} STR_CONFIG_SETTING_OSK_ACTIVATION :On screen keyboard: {STRING2}
STR_CONFIG_SETTING_OSK_ACTIVATION_HELPTEXT :Select the method to open the on screen keyboard for entering text into editboxes only using the pointing device. This is meant for small devices without actual keyboard STR_CONFIG_SETTING_OSK_ACTIVATION_HELPTEXT :Select the method to open the on screen keyboard for entering text into editboxes only using the pointing device. This is meant for small devices without actual keyboard
###length 4 ###length 4

View File

@ -681,6 +681,11 @@ SettingsContainer &GetSettingsTree()
* Since it's also able to completely disable the scrollwheel will we display it on all platforms anyway */ * Since it's also able to completely disable the scrollwheel will we display it on all platforms anyway */
viewports->Add(new SettingEntry("gui.scrollwheel_scrolling")); viewports->Add(new SettingEntry("gui.scrollwheel_scrolling"));
viewports->Add(new SettingEntry("gui.scrollwheel_multiplier")); viewports->Add(new SettingEntry("gui.scrollwheel_multiplier"));
viewports->Add(new SettingEntry("gui.gamepad_stick_selection"));
viewports->Add(new SettingEntry("gui.gamepad_deadzone"));
viewports->Add(new SettingEntry("gui.gamepad_sensitivity"));
viewports->Add(new SettingEntry("gui.gamepad_invert_x"));
viewports->Add(new SettingEntry("gui.gamepad_invert_y"));
#ifdef __APPLE__ #ifdef __APPLE__
/* We might need to emulate a right mouse button on mac */ /* We might need to emulate a right mouse button on mac */
viewports->Add(new SettingEntry("gui.right_mouse_btn_emulation")); viewports->Add(new SettingEntry("gui.right_mouse_btn_emulation"));

View File

@ -140,6 +140,13 @@ enum ScrollWheelScrollingSetting : uint8_t {
SWS_OFF = 2 ///< Scroll wheel has no effect. SWS_OFF = 2 ///< Scroll wheel has no effect.
}; };
/** Settings related to gamepad stick selection. */
enum GamepadStickSelection : uint8_t {
GSS_DISABLED = 0, ///< Gamepad scrolling disabled.
GSS_LEFT_STICK = 1, ///< Use left analog stick for scrolling.
GSS_RIGHT_STICK = 2, ///< Use right analog stick for scrolling.
};
/** Settings related to the GUI and other stuff that is not saved in the savegame. */ /** Settings related to the GUI and other stuff that is not saved in the savegame. */
struct GUISettings { struct GUISettings {
bool sg_full_load_any; ///< new full load calculation, any cargo must be full read from pre v93 savegames bool sg_full_load_any; ///< new full load calculation, any cargo must be full read from pre v93 savegames
@ -183,6 +190,11 @@ struct GUISettings {
uint8_t right_mouse_btn_emulation; ///< should we emulate right mouse clicking? uint8_t right_mouse_btn_emulation; ///< should we emulate right mouse clicking?
uint8_t scrollwheel_scrolling; ///< scrolling using the scroll wheel? uint8_t scrollwheel_scrolling; ///< scrolling using the scroll wheel?
uint8_t scrollwheel_multiplier; ///< how much 'wheel' per incoming event from the OS? uint8_t scrollwheel_multiplier; ///< how much 'wheel' per incoming event from the OS?
uint8_t gamepad_deadzone; ///< deadzone for gamepad analog sticks (0-100)
uint8_t gamepad_sensitivity; ///< sensitivity multiplier for gamepad scrolling
bool gamepad_invert_x; ///< invert X axis for gamepad scrolling?
bool gamepad_invert_y; ///< invert Y axis for gamepad scrolling?
uint8_t gamepad_stick_selection; ///< which stick to use for scrolling (left/right/disabled)
bool timetable_arrival_departure; ///< show arrivals and departures in vehicle timetables bool timetable_arrival_departure; ///< show arrivals and departures in vehicle timetables
RightClickClose right_click_wnd_close; ///< close window with right click RightClickClose right_click_wnd_close; ///< close window with right click
bool pause_on_newgame; ///< whether to start new games paused or not bool pause_on_newgame; ///< whether to start new games paused or not

View File

@ -535,7 +535,7 @@ static void *ReadSprite(const SpriteCache *sc, SpriteID id, SpriteType sprite_ty
struct GrfSpriteOffset { struct GrfSpriteOffset {
size_t file_pos; size_t file_pos;
uint8_t control_flags; SpriteCacheCtrlFlags control_flags{};
}; };
/** Map from sprite numbers to position in the GRF file. */ /** Map from sprite numbers to position in the GRF file. */
@ -565,7 +565,7 @@ void ReadGRFSpriteOffsets(SpriteFile &file)
size_t old_pos = file.GetPos(); size_t old_pos = file.GetPos();
file.SeekTo(data_offset, SEEK_CUR); file.SeekTo(data_offset, SEEK_CUR);
GrfSpriteOffset offset = { 0, 0 }; GrfSpriteOffset offset{0};
/* Loop over all sprite section entries and store the file /* Loop over all sprite section entries and store the file
* offset for each newly encountered ID. */ * offset for each newly encountered ID. */
@ -574,7 +574,6 @@ void ReadGRFSpriteOffsets(SpriteFile &file)
if (id != prev_id) { if (id != prev_id) {
_grf_sprite_offsets[prev_id] = offset; _grf_sprite_offsets[prev_id] = offset;
offset.file_pos = file.GetPos() - 4; offset.file_pos = file.GetPos() - 4;
offset.control_flags = 0;
} }
prev_id = id; prev_id = id;
uint length = file.ReadDword(); uint length = file.ReadDword();
@ -585,11 +584,11 @@ void ReadGRFSpriteOffsets(SpriteFile &file)
uint8_t zoom = file.ReadByte(); uint8_t zoom = file.ReadByte();
length--; length--;
if (colour.Any() && zoom == 0) { // ZoomLevel::Normal (normal zoom) if (colour.Any() && zoom == 0) { // ZoomLevel::Normal (normal zoom)
SetBit(offset.control_flags, (colour != SpriteComponent::Palette) ? SCCF_ALLOW_ZOOM_MIN_1X_32BPP : SCCF_ALLOW_ZOOM_MIN_1X_PAL); offset.control_flags.Set((colour != SpriteComponent::Palette) ? SpriteCacheCtrlFlag::AllowZoomMin1x32bpp : SpriteCacheCtrlFlag::AllowZoomMin1xPal);
SetBit(offset.control_flags, (colour != SpriteComponent::Palette) ? SCCF_ALLOW_ZOOM_MIN_2X_32BPP : SCCF_ALLOW_ZOOM_MIN_2X_PAL); offset.control_flags.Set((colour != SpriteComponent::Palette) ? SpriteCacheCtrlFlag::AllowZoomMin2x32bpp : SpriteCacheCtrlFlag::AllowZoomMin2xPal);
} }
if (colour.Any() && zoom == 2) { // ZoomLevel::In2x (2x zoomed in) if (colour.Any() && zoom == 2) { // ZoomLevel::In2x (2x zoomed in)
SetBit(offset.control_flags, (colour != SpriteComponent::Palette) ? SCCF_ALLOW_ZOOM_MIN_2X_32BPP : SCCF_ALLOW_ZOOM_MIN_2X_PAL); offset.control_flags.Set((colour != SpriteComponent::Palette) ? SpriteCacheCtrlFlag::AllowZoomMin2x32bpp : SpriteCacheCtrlFlag::AllowZoomMin2xPal);
} }
} }
} }
@ -621,7 +620,7 @@ bool LoadNextSprite(SpriteID load_index, SpriteFile &file, uint file_sprite_id)
uint8_t grf_type = file.ReadByte(); uint8_t grf_type = file.ReadByte();
SpriteType type; SpriteType type;
uint8_t control_flags = 0; SpriteCacheCtrlFlags control_flags;
if (grf_type == 0xFF) { if (grf_type == 0xFF) {
/* Some NewGRF files have "empty" pseudo-sprites which are 1 /* Some NewGRF files have "empty" pseudo-sprites which are 1
* byte long. Catch these so the sprites won't be displayed. */ * byte long. Catch these so the sprites won't be displayed. */

View File

@ -11,24 +11,9 @@
#define SPRITECACHE_H #define SPRITECACHE_H
#include "gfx_type.h" #include "gfx_type.h"
#include "spritecache_type.h"
#include "spriteloader/spriteloader.hpp" #include "spriteloader/spriteloader.hpp"
/** Data structure describing a sprite. */
struct Sprite {
uint16_t height; ///< Height of the sprite.
uint16_t width; ///< Width of the sprite.
int16_t x_offs; ///< Number of pixels to shift the sprite to the right.
int16_t y_offs; ///< Number of pixels to shift the sprite downwards.
std::byte data[]; ///< Sprite data.
};
enum SpriteCacheCtrlFlags : uint8_t {
SCCF_ALLOW_ZOOM_MIN_1X_PAL = 0, ///< Allow use of sprite min zoom setting at 1x in palette mode.
SCCF_ALLOW_ZOOM_MIN_1X_32BPP = 1, ///< Allow use of sprite min zoom setting at 1x in 32bpp mode.
SCCF_ALLOW_ZOOM_MIN_2X_PAL = 2, ///< Allow use of sprite min zoom setting at 2x in palette mode.
SCCF_ALLOW_ZOOM_MIN_2X_32BPP = 3, ///< Allow use of sprite min zoom setting at 2x in 32bpp mode.
};
extern uint _sprite_cache_size; extern uint _sprite_cache_size;
/** SpriteAllocator that allocates memory via a unique_ptr array. */ /** SpriteAllocator that allocates memory via a unique_ptr array. */

View File

@ -12,6 +12,7 @@
#include "core/math_func.hpp" #include "core/math_func.hpp"
#include "gfx_type.h" #include "gfx_type.h"
#include "spritecache_type.h"
#include "spriteloader/spriteloader.hpp" #include "spriteloader/spriteloader.hpp"
#include "table/sprites.h" #include "table/sprites.h"
@ -27,7 +28,7 @@ struct SpriteCache {
uint32_t lru = 0; uint32_t lru = 0;
SpriteType type = SpriteType::Invalid; ///< In some cases a single sprite is misused by two NewGRFs. Once as real sprite and once as recolour sprite. If the recolour sprite gets into the cache it might be drawn as real sprite which causes enormous trouble. SpriteType type = SpriteType::Invalid; ///< In some cases a single sprite is misused by two NewGRFs. Once as real sprite and once as recolour sprite. If the recolour sprite gets into the cache it might be drawn as real sprite which causes enormous trouble.
bool warned = false; ///< True iff the user has been warned about incorrect use of this sprite bool warned = false; ///< True iff the user has been warned about incorrect use of this sprite
uint8_t control_flags = 0; ///< Control flags, see SpriteCacheCtrlFlags SpriteCacheCtrlFlags control_flags{}; ///< Control flags, see SpriteCacheCtrlFlags
void ClearSpriteData(); void ClearSpriteData();
}; };

View File

@ -0,0 +1,33 @@
/*
* This file is part of OpenTTD.
* OpenTTD 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, version 2.
* OpenTTD 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 OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file spritecache_type.h Types related to the sprite cache. */
#ifndef SPRITECACHE_TYPE_H
#define SPRITECACHE_TYPE_H
#include "core/enum_type.hpp"
/** Data structure describing a sprite. */
struct Sprite {
uint16_t height; ///< Height of the sprite.
uint16_t width; ///< Width of the sprite.
int16_t x_offs; ///< Number of pixels to shift the sprite to the right.
int16_t y_offs; ///< Number of pixels to shift the sprite downwards.
std::byte data[]; ///< Sprite data.
};
enum class SpriteCacheCtrlFlag : uint8_t {
AllowZoomMin1xPal, ///< Allow use of sprite min zoom setting at 1x in palette mode.
AllowZoomMin1x32bpp, ///< Allow use of sprite min zoom setting at 1x in 32bpp mode.
AllowZoomMin2xPal, ///< Allow use of sprite min zoom setting at 2x in palette mode.
AllowZoomMin2x32bpp, ///< Allow use of sprite min zoom setting at 2x in 32bpp mode.
};
using SpriteCacheCtrlFlags = EnumBitSet<SpriteCacheCtrlFlag, uint8_t>;
#endif /* SPRITECACHE_TYPE_H */

View File

@ -256,7 +256,7 @@ static ZoomLevels LoadSpriteV1(SpriteLoader::SpriteCollection &sprite, SpriteFil
return {}; return {};
} }
static ZoomLevels LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) static ZoomLevels LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp)
{ {
static const ZoomLevel zoom_lvl_map[6] = {ZoomLevel::Normal, ZoomLevel::In4x, ZoomLevel::In2x, ZoomLevel::Out2x, ZoomLevel::Out4x, ZoomLevel::Out8x}; static const ZoomLevel zoom_lvl_map[6] = {ZoomLevel::Normal, ZoomLevel::In4x, ZoomLevel::In2x, ZoomLevel::Out2x, ZoomLevel::Out4x, ZoomLevel::Out8x};
@ -295,11 +295,11 @@ static ZoomLevels LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, SpriteFil
is_wanted_zoom_lvl = true; is_wanted_zoom_lvl = true;
ZoomLevel zoom_min = sprite_type == SpriteType::Font ? ZoomLevel::Min : _settings_client.gui.sprite_zoom_min; ZoomLevel zoom_min = sprite_type == SpriteType::Font ? ZoomLevel::Min : _settings_client.gui.sprite_zoom_min;
if (zoom_min >= ZoomLevel::In2x && if (zoom_min >= ZoomLevel::In2x &&
HasBit(control_flags, load_32bpp ? SCCF_ALLOW_ZOOM_MIN_2X_32BPP : SCCF_ALLOW_ZOOM_MIN_2X_PAL) && zoom_lvl < ZoomLevel::In2x) { control_flags.Test(load_32bpp ? SpriteCacheCtrlFlag::AllowZoomMin2x32bpp : SpriteCacheCtrlFlag::AllowZoomMin2xPal) && zoom_lvl < ZoomLevel::In2x) {
is_wanted_zoom_lvl = false; is_wanted_zoom_lvl = false;
} }
if (zoom_min >= ZoomLevel::Normal && if (zoom_min >= ZoomLevel::Normal &&
HasBit(control_flags, load_32bpp ? SCCF_ALLOW_ZOOM_MIN_1X_32BPP : SCCF_ALLOW_ZOOM_MIN_1X_PAL) && zoom_lvl < ZoomLevel::Normal) { control_flags.Test(load_32bpp ? SpriteCacheCtrlFlag::AllowZoomMin1x32bpp : SpriteCacheCtrlFlag::AllowZoomMin1xPal) && zoom_lvl < ZoomLevel::Normal) {
is_wanted_zoom_lvl = false; is_wanted_zoom_lvl = false;
} }
} else { } else {
@ -359,7 +359,7 @@ static ZoomLevels LoadSpriteV2(SpriteLoader::SpriteCollection &sprite, SpriteFil
return loaded_sprites; return loaded_sprites;
} }
ZoomLevels SpriteLoaderGrf::LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) ZoomLevels SpriteLoaderGrf::LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp)
{ {
if (this->container_ver >= 2) { if (this->container_ver >= 2) {
return LoadSpriteV2(sprite, file, file_pos, sprite_type, load_32bpp, control_flags, avail_8bpp, avail_32bpp); return LoadSpriteV2(sprite, file, file_pos, sprite_type, load_32bpp, control_flags, avail_8bpp, avail_32bpp);

View File

@ -17,7 +17,7 @@ class SpriteLoaderGrf : public SpriteLoader {
uint8_t container_ver; uint8_t container_ver;
public: public:
SpriteLoaderGrf(uint8_t container_ver) : container_ver(container_ver) {} SpriteLoaderGrf(uint8_t container_ver) : container_ver(container_ver) {}
ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) override; ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) override;
}; };
#endif /* SPRITELOADER_GRF_HPP */ #endif /* SPRITELOADER_GRF_HPP */

View File

@ -48,7 +48,7 @@ static void Convert32bppTo8bpp(SpriteLoader::Sprite &sprite)
} }
} }
ZoomLevels SpriteLoaderMakeIndexed::LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) ZoomLevels SpriteLoaderMakeIndexed::LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp)
{ {
ZoomLevels avail = this->baseloader.LoadSprite(sprite, file, file_pos, sprite_type, true, control_flags, avail_8bpp, avail_32bpp); ZoomLevels avail = this->baseloader.LoadSprite(sprite, file, file_pos, sprite_type, true, control_flags, avail_8bpp, avail_32bpp);

View File

@ -17,7 +17,7 @@ class SpriteLoaderMakeIndexed : public SpriteLoader {
SpriteLoader &baseloader; SpriteLoader &baseloader;
public: public:
SpriteLoaderMakeIndexed(SpriteLoader &baseloader) : baseloader(baseloader) {} SpriteLoaderMakeIndexed(SpriteLoader &baseloader) : baseloader(baseloader) {}
ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) override; ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) override;
}; };
#endif /* SPRITELOADER_MAKEINDEXED_H */ #endif /* SPRITELOADER_MAKEINDEXED_H */

View File

@ -13,6 +13,7 @@
#include "../core/alloc_type.hpp" #include "../core/alloc_type.hpp"
#include "../core/enum_type.hpp" #include "../core/enum_type.hpp"
#include "../gfx_type.h" #include "../gfx_type.h"
#include "../spritecache_type.h"
#include "sprite_file_type.hpp" #include "sprite_file_type.hpp"
struct Sprite; struct Sprite;
@ -94,7 +95,7 @@ public:
* @param[out] avail_32bpp Available 32bpp sprites. * @param[out] avail_32bpp Available 32bpp sprites.
* @return Available sprites matching \a load_32bpp. * @return Available sprites matching \a load_32bpp.
*/ */
virtual ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, uint8_t control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) = 0; virtual ZoomLevels LoadSprite(SpriteLoader::SpriteCollection &sprite, SpriteFile &file, size_t file_pos, SpriteType sprite_type, bool load_32bpp, SpriteCacheCtrlFlags control_flags, ZoomLevels &avail_8bpp, ZoomLevels &avail_32bpp) = 0;
virtual ~SpriteLoader() = default; virtual ~SpriteLoader() = default;
}; };

View File

@ -912,3 +912,62 @@ post_cb = [](auto) { SetupWidgetDimensions(); ReInitAllWindows(true); }
cat = SC_BASIC cat = SC_BASIC
startup = true startup = true
[SDTC_VAR]
var = gui.gamepad_deadzone
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = 10
min = 0
max = 100
interval = 5
str = STR_CONFIG_SETTING_GAMEPAD_DEADZONE
strhelp = STR_CONFIG_SETTING_GAMEPAD_DEADZONE_HELPTEXT
strval = STR_JUST_COMMA
cat = SC_BASIC
startup = true
[SDTC_VAR]
var = gui.gamepad_sensitivity
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = 10
min = 1
max = 100
interval = 5
str = STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY
strhelp = STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY_HELPTEXT
strval = STR_JUST_COMMA
cat = SC_BASIC
startup = true
[SDTC_BOOL]
var = gui.gamepad_invert_x
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = false
str = STR_CONFIG_SETTING_GAMEPAD_INVERT_X
strhelp = STR_CONFIG_SETTING_GAMEPAD_INVERT_X_HELPTEXT
cat = SC_BASIC
startup = true
[SDTC_BOOL]
var = gui.gamepad_invert_y
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = false
str = STR_CONFIG_SETTING_GAMEPAD_INVERT_Y
strhelp = STR_CONFIG_SETTING_GAMEPAD_INVERT_Y_HELPTEXT
cat = SC_BASIC
startup = true
[SDTC_VAR]
var = gui.gamepad_stick_selection
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync, SettingFlag::GuiDropdown
def = GSS_LEFT_STICK
min = GSS_DISABLED
max = GSS_RIGHT_STICK
str = STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION
strhelp = STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION_HELPTEXT
strval = STR_CONFIG_SETTING_GAMEPAD_STICK_DISABLED
cat = SC_BASIC
startup = true

View File

@ -33,7 +33,7 @@ static bool MockLoadNextSprite(SpriteID load_index)
sc->id = 0; sc->id = 0;
sc->type = is_mapgen ? SpriteType::MapGen : SpriteType::Normal; sc->type = is_mapgen ? SpriteType::MapGen : SpriteType::Normal;
sc->warned = false; sc->warned = false;
sc->control_flags = 0; sc->control_flags = {};
/* Fill with empty sprites up until the default sprite count. */ /* Fill with empty sprites up until the default sprite count. */
return load_index < SPR_OPENTTD_BASE + OPENTTD_SPRITE_COUNT; return load_index < SPR_OPENTTD_BASE + OPENTTD_SPRITE_COUNT;

View File

@ -20,6 +20,7 @@
#include "../fileio_func.h" #include "../fileio_func.h"
#include "../framerate_type.h" #include "../framerate_type.h"
#include "../window_func.h" #include "../window_func.h"
#include "../zoom_func.h"
#include "sdl2_v.h" #include "sdl2_v.h"
#include <SDL.h> #include <SDL.h>
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
@ -524,6 +525,27 @@ bool VideoDriver_SDL_Base::PollEvent()
} }
break; break;
} }
case SDL_CONTROLLERDEVICEADDED: {
Debug(driver, 1, "SDL2: Gamepad device added, index: {}", ev.cdevice.which);
/* Try to open the newly connected gamepad */
if (this->gamepad == nullptr) {
this->OpenGamepad();
}
break;
}
case SDL_CONTROLLERDEVICEREMOVED: {
Debug(driver, 1, "SDL2: Gamepad device removed, instance ID: {}", ev.cdevice.which);
/* Close gamepad if it was removed */
if (this->gamepad != nullptr && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(this->gamepad))) {
Debug(driver, 1, "SDL2: Current gamepad was removed, closing and reopening");
this->CloseGamepad();
/* Try to open another gamepad if available */
this->OpenGamepad();
}
break;
}
} }
return true; return true;
@ -539,6 +561,12 @@ static std::optional<std::string_view> InitializeSDL()
#endif #endif
if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) return SDL_GetError(); if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) return SDL_GetError();
/* Initialize gamepad subsystem for gamepad scrolling support */
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
Debug(driver, 1, "SDL2: Failed to initialize gamepad subsystem: {}", SDL_GetError());
}
return std::nullopt; return std::nullopt;
} }
@ -590,6 +618,11 @@ std::optional<std::string_view> VideoDriver_SDL_Base::Start(const StringList &pa
SDL_StopTextInput(); SDL_StopTextInput();
this->edit_box_focused = false; this->edit_box_focused = false;
/* Initialize gamepad for scrolling */
Debug(driver, 1, "SDL2: Attempting to initialize gamepad support");
Debug(driver, 1, "SDL2: Gamepad stick selection setting: {}", _settings_client.gui.gamepad_stick_selection);
this->OpenGamepad();
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
this->is_game_threaded = false; this->is_game_threaded = false;
#else #else
@ -601,6 +634,8 @@ std::optional<std::string_view> VideoDriver_SDL_Base::Start(const StringList &pa
void VideoDriver_SDL_Base::Stop() void VideoDriver_SDL_Base::Stop()
{ {
this->CloseGamepad();
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (SDL_WasInit(SDL_INIT_EVERYTHING) == 0) { if (SDL_WasInit(SDL_INIT_EVERYTHING) == 0) {
SDL_Quit(); // If there's nothing left, quit SDL SDL_Quit(); // If there's nothing left, quit SDL
@ -629,6 +664,9 @@ void VideoDriver_SDL_Base::InputLoop()
(keys[SDL_SCANCODE_DOWN] ? 8 : 0); (keys[SDL_SCANCODE_DOWN] ? 8 : 0);
if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged(); if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged();
/* Process gamepad input for scrolling */
this->ProcessGamepadInput();
} }
void VideoDriver_SDL_Base::LoopOnce() void VideoDriver_SDL_Base::LoopOnce()
@ -755,3 +793,84 @@ void VideoDriver_SDL_Base::UnlockVideoBuffer()
this->buffer_locked = false; this->buffer_locked = false;
} }
void VideoDriver_SDL_Base::OpenGamepad()
{
/* Don't open gamepad if already open or if gamepad scrolling is disabled */
if (this->gamepad != nullptr) {
Debug(driver, 1, "SDL2: Gamepad already open, skipping");
return;
}
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
Debug(driver, 1, "SDL2: Gamepad scrolling disabled, not opening gamepad");
return;
}
/* Check if any gamepads are available */
int num_gamepads = SDL_NumJoysticks();
Debug(driver, 1, "SDL2: Found {} joystick(s)", num_gamepads);
for (int i = 0; i < num_gamepads; i++) {
if (SDL_IsGameController(i)) {
Debug(driver, 1, "SDL2: Joystick {} is a gamepad, attempting to open", i);
this->gamepad = SDL_GameControllerOpen(i);
if (this->gamepad != nullptr) {
Debug(driver, 1, "SDL2: Opened gamepad: {}", SDL_GameControllerName(this->gamepad));
break;
} else {
Debug(driver, 1, "SDL2: Failed to open gamepad {}: {}", i, SDL_GetError());
}
} else {
Debug(driver, 1, "SDL2: Joystick {} is not a gamepad", i);
}
}
if (this->gamepad == nullptr) {
Debug(driver, 1, "SDL2: No gamepad opened");
}
}
void VideoDriver_SDL_Base::CloseGamepad()
{
if (this->gamepad != nullptr) {
SDL_GameControllerClose(this->gamepad);
this->gamepad = nullptr;
Debug(driver, 1, "SDL2: Closed gamepad");
}
}
void VideoDriver_SDL_Base::ProcessGamepadInput()
{
/* Skip if gamepad is not available */
if (this->gamepad == nullptr) {
static bool logged_no_gamepad = false;
if (!logged_no_gamepad) {
Debug(driver, 2, "SDL2: No gamepad available for input processing");
logged_no_gamepad = true;
}
return;
}
/* Check if gamepad is still connected */
if (!SDL_GameControllerGetAttached(this->gamepad)) {
Debug(driver, 1, "SDL2: Gamepad disconnected, closing and reopening");
this->CloseGamepad();
return;
}
/* Get analog stick values based on stick selection */
Sint16 stick_x = 0, stick_y = 0;
if (_settings_client.gui.gamepad_stick_selection == GSS_LEFT_STICK) {
stick_x = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_LEFTX);
stick_y = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_LEFTY);
Debug(driver, 3, "SDL2: Left stick raw values: x={}, y={}", stick_x, stick_y);
} else if (_settings_client.gui.gamepad_stick_selection == GSS_RIGHT_STICK) {
stick_x = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_RIGHTX);
stick_y = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_RIGHTY);
Debug(driver, 3, "SDL2: Right stick raw values: x={}, y={}", stick_x, stick_y);
}
/* Use the common gamepad handling function */
HandleGamepadScrolling(stick_x, stick_y, 32767);
}

View File

@ -14,6 +14,10 @@
#include "video_driver.hpp" #include "video_driver.hpp"
/* Forward declaration of SDL_GameController */
struct _SDL_GameController;
typedef struct _SDL_GameController SDL_GameController;
/** The SDL video driver. */ /** The SDL video driver. */
class VideoDriver_SDL_Base : public VideoDriver { class VideoDriver_SDL_Base : public VideoDriver {
public: public:
@ -69,6 +73,13 @@ protected:
/** Create the main window. */ /** Create the main window. */
virtual bool CreateMainWindow(uint w, uint h, uint flags = 0); virtual bool CreateMainWindow(uint w, uint h, uint flags = 0);
protected:
/** Gamepad support for map scrolling */
SDL_GameController *gamepad = nullptr; ///< Currently opened gamepad.
void OpenGamepad();
void CloseGamepad();
void ProcessGamepadInput();
private: private:
void LoopOnce(); void LoopOnce();
void MainLoopCleanup(); void MainLoopCleanup();

View File

@ -28,6 +28,7 @@
#include <windows.h> #include <windows.h>
#include <imm.h> #include <imm.h>
#include <versionhelpers.h> #include <versionhelpers.h>
#include <xinput.h>
#if defined(_MSC_VER) && defined(NTDDI_WIN10_RS4) #if defined(_MSC_VER) && defined(NTDDI_WIN10_RS4)
#include <winrt/Windows.UI.ViewManagement.h> #include <winrt/Windows.UI.ViewManagement.h>
#endif #endif
@ -984,6 +985,9 @@ void VideoDriver_Win32Base::InputLoop()
} }
if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged(); if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged();
/* Process gamepad input for scrolling */
this->ProcessGamepadInput();
} }
bool VideoDriver_Win32Base::PollEvent() bool VideoDriver_Win32Base::PollEvent()
@ -1143,6 +1147,106 @@ void VideoDriver_Win32Base::UnlockVideoBuffer()
this->buffer_locked = false; this->buffer_locked = false;
} }
void VideoDriver_Win32Base::OpenGamepad()
{
/* Don't open gamepad if already open or if gamepad scrolling is disabled */
if (this->gamepad_user_index != XUSER_MAX_COUNT) {
Debug(driver, 1, "Win32: Gamepad already open, skipping");
return;
}
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
Debug(driver, 1, "Win32: Gamepad scrolling disabled, not opening gamepad");
return;
}
/* Check for any connected gamepads */
for (DWORD i = 0; i < XUSER_MAX_COUNT; i++) {
XINPUT_STATE state = {};
if (XInputGetState(i, &state) == ERROR_SUCCESS) {
this->gamepad_user_index = i;
Debug(driver, 1, "Win32: Opened gamepad at index {}", i);
return;
}
}
}
void VideoDriver_Win32Base::CloseGamepad()
{
if (this->gamepad_user_index != XUSER_MAX_COUNT) {
this->gamepad_user_index = XUSER_MAX_COUNT;
Debug(driver, 1, "Win32: Closed gamepad");
}
}
void VideoDriver_Win32Base::ProcessGamepadInput()
{
/* Skip if gamepad scrolling is disabled */
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
return;
}
/* If no gamepad is currently open, try to reconnect periodically */
if (this->gamepad_user_index == XUSER_MAX_COUNT) {
static bool logged_no_gamepad = false;
/* Only try to reconnect every 60 frames (~1 second at 60 FPS) to avoid spam */
if (this->gamepad_reconnect_timer > 0) {
this->gamepad_reconnect_timer--;
if (!logged_no_gamepad) {
Debug(driver, 2, "Win32: No gamepad available for input processing");
logged_no_gamepad = true;
}
return;
}
/* Try to open gamepad */
this->OpenGamepad();
/* If still no gamepad, set timer for next retry */
if (this->gamepad_user_index == XUSER_MAX_COUNT) {
this->gamepad_reconnect_timer = 60; /* Retry in ~1 second */
if (!logged_no_gamepad) {
Debug(driver, 2, "Win32: No gamepad available for input processing");
logged_no_gamepad = true;
}
return;
} else {
/* Successfully reconnected */
logged_no_gamepad = false;
}
}
/* Get gamepad state */
XINPUT_STATE state = {};
if (XInputGetState(this->gamepad_user_index, &state) != ERROR_SUCCESS) {
Debug(driver, 1, "Win32: Gamepad disconnected, closing and will retry connection");
this->CloseGamepad();
this->gamepad_reconnect_timer = 60; /* Start retry timer */
return;
}
/* Get analog stick values based on stick selection
* Note: XInput uses SHORT values for stick positions, but we have to extend to INT
* to avoid overflow when inverting the Y-axis value */
INT stick_x = 0, stick_y = 0;
if (_settings_client.gui.gamepad_stick_selection == GSS_LEFT_STICK) {
stick_x = state.Gamepad.sThumbLX;
stick_y = state.Gamepad.sThumbLY;
Debug(driver, 3, "Win32: Left stick raw values: x={}, y={}", stick_x, stick_y);
} else if (_settings_client.gui.gamepad_stick_selection == GSS_RIGHT_STICK) {
stick_x = state.Gamepad.sThumbRX;
stick_y = state.Gamepad.sThumbRY;
Debug(driver, 3, "Win32: Right stick raw values: x={}, y={}", stick_x, stick_y);
}
stick_y = -stick_y; // Xinput Y-axis is inverted from other libraries
/* Use the common gamepad handling function */
HandleGamepadScrolling(stick_x, stick_y, 32767);
}
static FVideoDriver_Win32GDI iFVideoDriver_Win32GDI; static FVideoDriver_Win32GDI iFVideoDriver_Win32GDI;
@ -1158,6 +1262,11 @@ std::optional<std::string_view> VideoDriver_Win32GDI::Start(const StringList &pa
MarkWholeScreenDirty(); MarkWholeScreenDirty();
/* Initialize gamepad for scrolling */
Debug(driver, 1, "Win32: Attempting to initialize gamepad support");
Debug(driver, 1, "Win32: Gamepad stick selection setting: {}", _settings_client.gui.gamepad_stick_selection);
this->OpenGamepad();
this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread");
return std::nullopt; return std::nullopt;
@ -1165,6 +1274,7 @@ std::optional<std::string_view> VideoDriver_Win32GDI::Start(const StringList &pa
void VideoDriver_Win32GDI::Stop() void VideoDriver_Win32GDI::Stop()
{ {
this->CloseGamepad();
DeleteObject(this->gdi_palette); DeleteObject(this->gdi_palette);
DeleteObject(this->dib_sect); DeleteObject(this->dib_sect);
@ -1472,6 +1582,11 @@ std::optional<std::string_view> VideoDriver_Win32OpenGL::Start(const StringList
MarkWholeScreenDirty(); MarkWholeScreenDirty();
/* Initialize gamepad for scrolling */
Debug(driver, 1, "Win32: Attempting to initialize gamepad support");
Debug(driver, 1, "Win32: Gamepad stick selection setting: {}", _settings_client.gui.gamepad_stick_selection);
this->OpenGamepad();
this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread"); this->is_game_threaded = !GetDriverParamBool(param, "no_threads") && !GetDriverParamBool(param, "no_thread");
return std::nullopt; return std::nullopt;
@ -1479,6 +1594,7 @@ std::optional<std::string_view> VideoDriver_Win32OpenGL::Start(const StringList
void VideoDriver_Win32OpenGL::Stop() void VideoDriver_Win32OpenGL::Stop()
{ {
this->CloseGamepad();
this->DestroyContext(); this->DestroyContext();
this->VideoDriver_Win32Base::Stop(); this->VideoDriver_Win32Base::Stop();
} }

View File

@ -14,6 +14,7 @@
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include <windows.h> #include <windows.h>
#include <xinput.h>
/** Base class for Windows video drivers. */ /** Base class for Windows video drivers. */
class VideoDriver_Win32Base : public VideoDriver { class VideoDriver_Win32Base : public VideoDriver {
@ -48,6 +49,13 @@ protected:
bool buffer_locked; ///< Video buffer was locked by the main thread. bool buffer_locked; ///< Video buffer was locked by the main thread.
/** Gamepad support for map scrolling */
DWORD gamepad_user_index = XUSER_MAX_COUNT; ///< Index of currently opened gamepad (XUSER_MAX_COUNT = no gamepad).
uint32_t gamepad_reconnect_timer = 0; ///< Timer for retrying gamepad connection after disconnect.
void OpenGamepad();
void CloseGamepad();
void ProcessGamepadInput();
Dimension GetScreenSize() const override; Dimension GetScreenSize() const override;
float GetDPIScale() override; float GetDPIScale() override;
void InputLoop() override; void InputLoop() override;

View File

@ -3572,3 +3572,81 @@ void PickerWindowBase::Close([[maybe_unused]] int data)
ResetObjectToPlace(); ResetObjectToPlace();
this->Window::Close(); this->Window::Close();
} }
/**
* Process gamepad analog stick input for viewport scrolling.
* This is a common function that can be used by any video driver that supports gamepads.
* @param stick_x Raw analog stick X value (typically -32768 to 32767 range)
* @param stick_y Raw analog stick Y value (typically -32768 to 32767 range)
* @param max_axis_value Maximum value for the analog stick axes (e.g., 32767 for SDL2)
*/
void HandleGamepadScrolling(int stick_x, int stick_y, int max_axis_value)
{
/* Skip if gamepad stick selection is disabled */
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
return;
}
/* Apply deadzone (convert percentage to axis range) */
const int deadzone = (_settings_client.gui.gamepad_deadzone * max_axis_value) / 100;
if (abs(stick_x) < deadzone) stick_x = 0;
if (abs(stick_y) < deadzone) stick_y = 0;
/* Skip if no movement after deadzone */
if (stick_x == 0 && stick_y == 0) {
return;
}
/* Calculate scroll delta with sensitivity */
float sensitivity = _settings_client.gui.gamepad_sensitivity / 10.0f;
int delta_x = (int)(stick_x * sensitivity / (max_axis_value / 16)); // Scale down from axis range
int delta_y = (int)(stick_y * sensitivity / (max_axis_value / 16));
/* Apply axis inversion */
if (_settings_client.gui.gamepad_invert_x) delta_x = -delta_x;
if (_settings_client.gui.gamepad_invert_y) delta_y = -delta_y;
/* Skip if deltas are too small */
if (abs(delta_x) < 1 && abs(delta_y) < 1) {
return;
}
/* Apply scrolling based on cursor position */
if (_game_mode != GM_MENU && _game_mode != GM_BOOTSTRAP) {
Window *target_window = nullptr;
/* Check if cursor is over a window with a viewport */
Window *w = FindWindowFromPt(_cursor.pos.x, _cursor.pos.y);
if (w != nullptr && w->viewport != nullptr) {
/* Check if cursor is actually over the viewport area within the window */
Point pt = { _cursor.pos.x - w->left, _cursor.pos.y - w->top };
if (pt.x >= w->viewport->left - w->left &&
pt.x < w->viewport->left - w->left + w->viewport->width &&
pt.y >= w->viewport->top - w->top &&
pt.y < w->viewport->top - w->top + w->viewport->height) {
target_window = w;
}
}
/* If no viewport under cursor, use main window */
if (target_window == nullptr) {
target_window = GetMainWindow();
}
/* Apply scrolling to the target viewport */
if (target_window != nullptr && target_window->viewport != nullptr) {
/* Check if the viewport is following a vehicle (similar to mouse scroll behavior) */
if (target_window == GetMainWindow() && target_window->viewport->follow_vehicle != VehicleID::Invalid()) {
/* If following a vehicle, center on it and stop following (like mouse scroll) */
const Vehicle *veh = Vehicle::Get(target_window->viewport->follow_vehicle);
ScrollMainWindowTo(veh->x_pos, veh->y_pos, veh->z_pos, true); // This also resets follow_vehicle
return; // Don't apply gamepad scroll, just like mouse scroll returns ES_NOT_HANDLED
}
/* Apply the scroll using the same method as keyboard scrolling */
target_window->viewport->dest_scrollpos_x += ScaleByZoom(delta_x, target_window->viewport->zoom);
target_window->viewport->dest_scrollpos_y += ScaleByZoom(delta_y, target_window->viewport->zoom);
}
}
}