mirror of
https://github.com/libretro/scummvm.git
synced 2024-11-30 04:40:39 +00:00
SWITCH: add nintendo switch support
This commit is contained in:
parent
29028731c6
commit
488bbb267a
6
.gitignore
vendored
6
.gitignore
vendored
@ -248,6 +248,12 @@ psp2pkg/
|
||||
*.velf
|
||||
*.vpk
|
||||
|
||||
#Ignore Switch files
|
||||
switch_release/
|
||||
scummvm.elf
|
||||
scummvm.nro
|
||||
scummvm_switch.zip
|
||||
|
||||
#Ignore gmon.out created by gprof
|
||||
gmon.out
|
||||
/scummvm_libs_2015
|
||||
|
432
backends/events/switchsdl/switchsdl-events.cpp
Normal file
432
backends/events/switchsdl/switchsdl-events.cpp
Normal file
@ -0,0 +1,432 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/scummsys.h"
|
||||
|
||||
#if defined(NINTENDO_SWITCH)
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include "backends/platform/sdl/switch/switch.h"
|
||||
#include "backends/events/switchsdl/switchsdl-events.h"
|
||||
#include "backends/timer/sdl/sdl-timer.h"
|
||||
#include "backends/platform/sdl/sdl.h"
|
||||
#include "engines/engine.h"
|
||||
|
||||
#include "common/util.h"
|
||||
#include "common/events.h"
|
||||
#include "common/config-manager.h"
|
||||
|
||||
SwitchEventSource::SwitchEventSource() {
|
||||
for (int port = 0; port < SCE_TOUCH_PORT_MAX_NUM; port++) {
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
_finger[port][i].id = -1;
|
||||
}
|
||||
_multiFingerDragging[port] = DRAG_NONE;
|
||||
}
|
||||
|
||||
for (int port = 0; port < SCE_TOUCH_PORT_MAX_NUM; port++) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
_simulatedClickStartTime[port][i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SwitchEventSource::pollEvent(Common::Event &event) {
|
||||
((DefaultTimerManager *) g_system->getTimerManager())->handler();
|
||||
finishSimulatedMouseClicks();
|
||||
return SdlEventSource::pollEvent(event);
|
||||
}
|
||||
|
||||
void SwitchEventSource::preprocessEvents(SDL_Event *event) {
|
||||
|
||||
// Supported touch gestures:
|
||||
// left mouse click: single finger short tap
|
||||
// right mouse click: second finger short tap while first finger is still down
|
||||
// pointer motion: single finger drag
|
||||
if (event->type == SDL_FINGERDOWN || event->type == SDL_FINGERUP || event->type == SDL_FINGERMOTION) {
|
||||
// front (0) or back (1) panel
|
||||
SDL_TouchID port = event->tfinger.touchId;
|
||||
//debug(0, "touch: %li\n", port);
|
||||
if (port < SCE_TOUCH_PORT_MAX_NUM && port >= 0) {
|
||||
// touchpad_mouse_mode off: use only front panel for direct touch control of pointer
|
||||
// touchpad_mouse_mode on: also enable rear touch with indirect touch control
|
||||
// where the finger can be somewhere else than the pointer and still move it
|
||||
if (port == 0 || ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
switch (event->type) {
|
||||
case SDL_FINGERDOWN:
|
||||
//debug(0, "down[%li]: %i %i", event->tfinger.fingerId, (int) event->tfinger.x, (int) event->tfinger.y);
|
||||
preprocessFingerDown(event);
|
||||
break;
|
||||
case SDL_FINGERUP:
|
||||
//debug(0, "up[%li]: %i %i", event->tfinger.fingerId, (int) event->tfinger.x, (int) event->tfinger.y);
|
||||
preprocessFingerUp(event);
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
//debug(0, "mov[%li]: %i %i", event->tfinger.fingerId, (int) event->tfinger.x, (int) event->tfinger.y);
|
||||
preprocessFingerMotion(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchEventSource::preprocessFingerDown(SDL_Event *event) {
|
||||
// front (0) or back (1) panel
|
||||
SDL_TouchID port = event->tfinger.touchId;
|
||||
// id (for multitouch)
|
||||
SDL_FingerID id = event->tfinger.fingerId;
|
||||
|
||||
int x = _km.x / MULTIPLIER;
|
||||
int y = _km.y / MULTIPLIER;
|
||||
|
||||
if (port == 0 && !ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
convertTouchXYToGameXY(event->tfinger.x, event->tfinger.y, &x, &y);
|
||||
}
|
||||
|
||||
// make sure each finger is not reported down multiple times
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == id) {
|
||||
_finger[port][i].id = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// we need the timestamps to decide later if the user performed a short tap (click)
|
||||
// or a long tap (drag)
|
||||
// we also need the last coordinates for each finger to keep track of dragging
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == -1) {
|
||||
_finger[port][i].id = id;
|
||||
_finger[port][i].timeLastDown = event->tfinger.timestamp;
|
||||
_finger[port][i].lastDownX = event->tfinger.x;
|
||||
_finger[port][i].lastDownY = event->tfinger.y;
|
||||
_finger[port][i].lastX = x;
|
||||
_finger[port][i].lastY = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchEventSource::preprocessFingerUp(SDL_Event *event) {
|
||||
// front (0) or back (1) panel
|
||||
SDL_TouchID port = event->tfinger.touchId;
|
||||
// id (for multitouch)
|
||||
SDL_FingerID id = event->tfinger.fingerId;
|
||||
|
||||
// find out how many fingers were down before this event
|
||||
int numFingersDown = 0;
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id >= 0) {
|
||||
numFingersDown++;
|
||||
}
|
||||
}
|
||||
|
||||
int x = _km.x / MULTIPLIER;
|
||||
int y = _km.y / MULTIPLIER;
|
||||
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == id) {
|
||||
_finger[port][i].id = -1;
|
||||
if (!_multiFingerDragging[port]) {
|
||||
if ((event->tfinger.timestamp - _finger[port][i].timeLastDown) <= MAX_TAP_TIME) {
|
||||
// short (<MAX_TAP_TIME ms) tap is interpreted as right/left mouse click depending on # fingers already down
|
||||
// but only if the finger hasn't moved since it was pressed down by more than MAX_TAP_MOTION_DISTANCE pixels
|
||||
float xrel = ((event->tfinger.x * (float) TOUCHSCREEN_WIDTH) - (_finger[port][i].lastDownX * (float) TOUCHSCREEN_WIDTH));
|
||||
float yrel = ((event->tfinger.y * (float) TOUCHSCREEN_HEIGHT) - (_finger[port][i].lastDownY * (float) TOUCHSCREEN_HEIGHT));
|
||||
float maxRSquared = (float) (MAX_TAP_MOTION_DISTANCE * MAX_TAP_MOTION_DISTANCE);
|
||||
if ((xrel * xrel + yrel * yrel) < maxRSquared) {
|
||||
if (numFingersDown == 2 || numFingersDown == 1) {
|
||||
uint8 simulatedButton = 0;
|
||||
if (numFingersDown == 2) {
|
||||
simulatedButton = SDL_BUTTON_RIGHT;
|
||||
// need to raise the button later
|
||||
_simulatedClickStartTime[port][1] = event->tfinger.timestamp;
|
||||
} else if (numFingersDown == 1) {
|
||||
simulatedButton = SDL_BUTTON_LEFT;
|
||||
// need to raise the button later
|
||||
_simulatedClickStartTime[port][0] = event->tfinger.timestamp;
|
||||
if (port == 0 && !ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
convertTouchXYToGameXY(event->tfinger.x, event->tfinger.y, &x, &y);
|
||||
}
|
||||
}
|
||||
|
||||
event->type = SDL_MOUSEBUTTONDOWN;
|
||||
event->button.button = simulatedButton;
|
||||
event->button.x = x;
|
||||
event->button.y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (numFingersDown == 1) {
|
||||
// when dragging, and the last finger is lifted, the drag is over
|
||||
if (port == 0 && !ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
convertTouchXYToGameXY(event->tfinger.x, event->tfinger.y, &x, &y);
|
||||
}
|
||||
uint8 simulatedButton = 0;
|
||||
if (_multiFingerDragging[port] == DRAG_THREE_FINGER)
|
||||
simulatedButton = SDL_BUTTON_RIGHT;
|
||||
else {
|
||||
simulatedButton = SDL_BUTTON_LEFT;
|
||||
}
|
||||
event->type = SDL_MOUSEBUTTONUP;
|
||||
event->button.button = simulatedButton;
|
||||
event->button.x = x;
|
||||
event->button.y = y;
|
||||
_multiFingerDragging[port] = DRAG_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchEventSource::preprocessFingerMotion(SDL_Event *event) {
|
||||
// front (0) or back (1) panel
|
||||
SDL_TouchID port = event->tfinger.touchId;
|
||||
// id (for multitouch)
|
||||
SDL_FingerID id = event->tfinger.fingerId;
|
||||
|
||||
// find out how many fingers were down before this event
|
||||
int numFingersDown = 0;
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id >= 0) {
|
||||
numFingersDown++;
|
||||
}
|
||||
}
|
||||
|
||||
if (numFingersDown >= 1) {
|
||||
int x = _km.x / MULTIPLIER;
|
||||
int y = _km.y / MULTIPLIER;
|
||||
|
||||
if (port == 0 && !ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
convertTouchXYToGameXY(event->tfinger.x, event->tfinger.y, &x, &y);
|
||||
} else {
|
||||
// for relative mode, use the pointer speed setting
|
||||
float speedFactor = 1.0;
|
||||
|
||||
switch (ConfMan.getInt("kbdmouse_speed")) {
|
||||
case 0:
|
||||
speedFactor = 0.25;
|
||||
break;
|
||||
case 1:
|
||||
speedFactor = 0.5;
|
||||
break;
|
||||
case 2:
|
||||
speedFactor = 0.75;
|
||||
break;
|
||||
case 3:
|
||||
speedFactor = 1.0;
|
||||
break;
|
||||
case 4:
|
||||
speedFactor = 1.25;
|
||||
break;
|
||||
case 5:
|
||||
speedFactor = 1.5;
|
||||
break;
|
||||
case 6:
|
||||
speedFactor = 1.75;
|
||||
break;
|
||||
case 7:
|
||||
speedFactor = 2.0;
|
||||
break;
|
||||
default:
|
||||
speedFactor = 1.0;
|
||||
}
|
||||
|
||||
// convert touch events to relative mouse pointer events
|
||||
// Whenever an SDL_event involving the mouse is processed,
|
||||
// _km.x/y are truncated from subpixel precision to regular pixel precision.
|
||||
// Therefore, there's no need here to deal with subpixel precision in _km.x/y.
|
||||
x = (_km.x / MULTIPLIER + (event->tfinger.dx * 1.25 * speedFactor * _km.x_max));
|
||||
y = (_km.y / MULTIPLIER + (event->tfinger.dy * 1.25 * speedFactor * _km.y_max));
|
||||
}
|
||||
|
||||
x = CLIP(x, 0, (int)_km.x_max);
|
||||
y = CLIP(y, 0, (int)_km.y_max);
|
||||
|
||||
// update the current finger's coordinates so we can track it later
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == id) {
|
||||
_finger[port][i].lastX = x;
|
||||
_finger[port][i].lastY = y;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are starting a multi-finger drag, start holding down the mouse button
|
||||
if (numFingersDown >= 2) {
|
||||
if (!_multiFingerDragging[port]) {
|
||||
// only start a multi-finger drag if at least two fingers have been down long enough
|
||||
int numFingersDownLong = 0;
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id >= 0) {
|
||||
if (event->tfinger.timestamp - _finger[port][i].timeLastDown > MAX_TAP_TIME) {
|
||||
numFingersDownLong++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFingersDownLong >= 2) {
|
||||
// starting drag, so push mouse down at current location (back)
|
||||
// or location of "oldest" finger (front)
|
||||
int mouseDownX = _km.x / MULTIPLIER;
|
||||
int mouseDownY = _km.y / MULTIPLIER;
|
||||
if (port == 0 && !ConfMan.getBool("touchpad_mouse_mode")) {
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == id) {
|
||||
uint32 earliestTime = _finger[port][i].timeLastDown;
|
||||
for (int j = 0; j < MAX_NUM_FINGERS; j++) {
|
||||
if (_finger[port][j].id >= 0 && (i != j) ) {
|
||||
if (_finger[port][j].timeLastDown < earliestTime) {
|
||||
mouseDownX = _finger[port][j].lastX;
|
||||
mouseDownY = _finger[port][j].lastY;
|
||||
earliestTime = _finger[port][j].timeLastDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint8 simulatedButton = 0;
|
||||
if (numFingersDownLong == 2) {
|
||||
simulatedButton = SDL_BUTTON_LEFT;
|
||||
_multiFingerDragging[port] = DRAG_TWO_FINGER;
|
||||
} else {
|
||||
simulatedButton = SDL_BUTTON_RIGHT;
|
||||
_multiFingerDragging[port] = DRAG_THREE_FINGER;
|
||||
}
|
||||
SDL_Event ev;
|
||||
ev.type = SDL_MOUSEBUTTONDOWN;
|
||||
ev.button.button = simulatedButton;
|
||||
ev.button.x = mouseDownX;
|
||||
ev.button.y = mouseDownY;
|
||||
SDL_PushEvent(&ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check if this is the "oldest" finger down (or the only finger down), otherwise it will not affect mouse motion
|
||||
bool updatePointer = true;
|
||||
if (numFingersDown > 1) {
|
||||
for (int i = 0; i < MAX_NUM_FINGERS; i++) {
|
||||
if (_finger[port][i].id == id) {
|
||||
for (int j = 0; j < MAX_NUM_FINGERS; j++) {
|
||||
if (_finger[port][j].id >= 0 && (i != j) ) {
|
||||
if (_finger[port][j].timeLastDown < _finger[port][i].timeLastDown) {
|
||||
updatePointer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatePointer) {
|
||||
event->type = SDL_MOUSEMOTION;
|
||||
event->motion.x = x;
|
||||
event->motion.y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchEventSource::convertTouchXYToGameXY(float touchX, float touchY, int *gameX, int *gameY) {
|
||||
int screenH = _km.y_max;
|
||||
int screenW = _km.x_max;
|
||||
|
||||
int windowH = g_system->getHeight();
|
||||
//int windowW = g_system->getWidth();
|
||||
|
||||
bool fullscreen = ConfMan.getBool("fullscreen");
|
||||
bool aspectRatioCorrection = ConfMan.getBool("aspect_ratio");
|
||||
|
||||
const int dispW = TOUCHSCREEN_WIDTH;
|
||||
const int dispH = TOUCHSCREEN_HEIGHT;
|
||||
|
||||
int x, y, w, h;
|
||||
float sx, sy;
|
||||
float ratio = (float)screenW / (float)screenH;
|
||||
|
||||
if (aspectRatioCorrection && (windowH == 200 || windowH == 400)) {
|
||||
ratio = 4.0f / 3.0f;
|
||||
}
|
||||
|
||||
if (fullscreen || screenH >= dispH) {
|
||||
h = dispH;
|
||||
if (aspectRatioCorrection && (windowH == 200 || windowH == 400)) {
|
||||
ratio = ratio * 1.1f;
|
||||
}
|
||||
w = h * ratio;
|
||||
} else {
|
||||
if (screenH <= dispH / 2 && screenW <= dispW / 2) {
|
||||
h = screenH * 2;
|
||||
w = screenW * 2;
|
||||
} else {
|
||||
h = screenH;
|
||||
w = screenW;
|
||||
}
|
||||
if (aspectRatioCorrection && (windowH == 200 || windowH == 400)) {
|
||||
// stretch the height only if it fits, otherwise make the width smaller
|
||||
if (((float)w * (1.0f / ratio)) <= (float)dispH) {
|
||||
h = w * (1.0f / ratio);
|
||||
} else {
|
||||
w = h * ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x = (dispW - w) / 2;
|
||||
y = (dispH - h) / 2;
|
||||
|
||||
sy = (float)h / (float)screenH;
|
||||
sx = (float)w / (float)screenW;
|
||||
|
||||
// Find touch coordinates in terms of screen pixels
|
||||
float dispTouchX = (touchX * (float)dispW);
|
||||
float dispTouchY = (touchY * (float)dispH);
|
||||
|
||||
*gameX = CLIP((int)((dispTouchX - x) / sx), 0, (int)_km.x_max);
|
||||
*gameY = CLIP((int)((dispTouchY - y) / sy), 0, (int)_km.y_max);
|
||||
}
|
||||
|
||||
void SwitchEventSource::finishSimulatedMouseClicks() {
|
||||
for (int port = 0; port < SCE_TOUCH_PORT_MAX_NUM; port++) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (_simulatedClickStartTime[port][i] != 0) {
|
||||
uint32 currentTime = SDL_GetTicks();
|
||||
if (currentTime - _simulatedClickStartTime[port][i] >= SIMULATED_CLICK_DURATION) {
|
||||
int simulatedButton;
|
||||
if (i == 0) {
|
||||
simulatedButton = SDL_BUTTON_LEFT;
|
||||
} else {
|
||||
simulatedButton = SDL_BUTTON_RIGHT;
|
||||
}
|
||||
SDL_Event ev;
|
||||
ev.type = SDL_MOUSEBUTTONUP;
|
||||
ev.button.button = simulatedButton;
|
||||
ev.button.x = _km.x / MULTIPLIER;
|
||||
ev.button.y = _km.y / MULTIPLIER;
|
||||
SDL_PushEvent(&ev);
|
||||
|
||||
_simulatedClickStartTime[port][i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
78
backends/events/switchsdl/switchsdl-events.h
Normal file
78
backends/events/switchsdl/switchsdl-events.h
Normal file
@ -0,0 +1,78 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#if !defined(DISABLE_DEFAULT_EVENTMANAGER)
|
||||
#define BACKEND_EVENTS_SWITCH_H
|
||||
|
||||
#include "backends/events/sdl/sdl-events.h"
|
||||
|
||||
#define SCE_TOUCH_PORT_MAX_NUM 1
|
||||
#define TOUCHSCREEN_WIDTH 1280
|
||||
#define TOUCHSCREEN_HEIGHT 720
|
||||
|
||||
/**
|
||||
* SDL Events manager for the SWITCH.
|
||||
*/
|
||||
class SwitchEventSource : public SdlEventSource {
|
||||
public:
|
||||
SwitchEventSource();
|
||||
bool pollEvent(Common::Event &event) override;
|
||||
protected:
|
||||
void preprocessEvents(SDL_Event *event) override;
|
||||
private:
|
||||
|
||||
enum {
|
||||
MAX_NUM_FINGERS = 3, // number of fingers to track per panel
|
||||
MAX_TAP_TIME = 250, // taps longer than this will not result in mouse click events
|
||||
MAX_TAP_MOTION_DISTANCE = 10, // max distance finger motion in Vita screen pixels to be considered a tap
|
||||
SIMULATED_CLICK_DURATION = 50, // time in ms how long simulated mouse clicks should be
|
||||
}; // track three fingers per panel
|
||||
|
||||
typedef struct {
|
||||
int id; // -1: no touch
|
||||
uint32 timeLastDown;
|
||||
int lastX; // last known screen coordinates
|
||||
int lastY; // last known screen coordinates
|
||||
float lastDownX; // SDL touch coordinates when last pressed down
|
||||
float lastDownY; // SDL touch coordinates when last pressed down
|
||||
} Touch;
|
||||
|
||||
Touch _finger[SCE_TOUCH_PORT_MAX_NUM][MAX_NUM_FINGERS]; // keep track of finger status
|
||||
|
||||
typedef enum DraggingType {
|
||||
DRAG_NONE = 0,
|
||||
DRAG_TWO_FINGER,
|
||||
DRAG_THREE_FINGER,
|
||||
} DraggingType;
|
||||
|
||||
DraggingType _multiFingerDragging[SCE_TOUCH_PORT_MAX_NUM]; // keep track whether we are currently drag-and-dropping
|
||||
|
||||
unsigned int _simulatedClickStartTime[SCE_TOUCH_PORT_MAX_NUM][2]; // initiation time of last simulated left or right click (zero if no click)
|
||||
|
||||
void preprocessFingerDown(SDL_Event *event);
|
||||
void preprocessFingerUp(SDL_Event *event);
|
||||
void preprocessFingerMotion(SDL_Event *event);
|
||||
void convertTouchXYToGameXY(float touchX, float touchY, int *gameX, int *gameY);
|
||||
void finishSimulatedMouseClicks(void);
|
||||
};
|
||||
|
||||
#endif /* BACKEND_EVENTS_SWITCH_H */
|
@ -32,7 +32,7 @@
|
||||
|
||||
#if defined(GP2X)
|
||||
#define SAMPLES_PER_SEC 11025
|
||||
#elif defined(PLAYSTATION3) || defined(PSP2)
|
||||
#elif defined(PLAYSTATION3) || defined(PSP2) || defined(NINTENDO_SWITCH)
|
||||
#define SAMPLES_PER_SEC 48000
|
||||
#else
|
||||
#define SAMPLES_PER_SEC 44100
|
||||
|
@ -341,6 +341,11 @@ MODULE_OBJS += \
|
||||
plugins/wii/wii-provider.o
|
||||
endif
|
||||
|
||||
ifeq ($(BACKEND),switch)
|
||||
MODULE_OBJS += \
|
||||
events/switchsdl/switchsdl-events.o
|
||||
endif
|
||||
|
||||
ifdef ENABLE_EVENTRECORDER
|
||||
MODULE_OBJS += \
|
||||
mixer/nullmixer/nullsdl-mixer.o \
|
||||
|
@ -52,6 +52,12 @@ MODULE_OBJS += \
|
||||
psp2/psp2.o
|
||||
endif
|
||||
|
||||
ifdef SWITCH
|
||||
MODULE_OBJS += \
|
||||
switch/switch-main.o \
|
||||
switch/switch.o
|
||||
endif
|
||||
|
||||
# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS.
|
||||
MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS))
|
||||
OBJS := $(MODULE_OBJS) $(OBJS)
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
#include "common/scummsys.h"
|
||||
|
||||
#if defined(POSIX) && !defined(MACOSX) && !defined(SAMSUNGTV) && !defined(MAEMO) && !defined(WEBOS) && !defined(LINUXMOTO) && !defined(GPH_DEVICE) && !defined(GP2X) && !defined(DINGUX) && !defined(OPENPANDORA) && !defined(PLAYSTATION3) && !defined(PSP2) && !defined(ANDROIDSDL)
|
||||
#if defined(POSIX) && !defined(MACOSX) && !defined(SAMSUNGTV) && !defined(MAEMO) && !defined(WEBOS) && !defined(LINUXMOTO) && !defined(GPH_DEVICE) && !defined(GP2X) && !defined(DINGUX) && !defined(OPENPANDORA) && !defined(PLAYSTATION3) && !defined(PSP2) && !defined(ANDROIDSDL) && !defined(NINTENDO_SWITCH)
|
||||
|
||||
#include "backends/platform/sdl/posix/posix.h"
|
||||
#include "backends/plugins/sdl/sdl-provider.h"
|
||||
|
62
backends/platform/sdl/switch/switch-main.cpp
Normal file
62
backends/platform/sdl/switch/switch-main.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_stdout
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_stderr
|
||||
|
||||
#include "common/scummsys.h"
|
||||
#include "backends/platform/sdl/switch/switch.h"
|
||||
#include "backends/plugins/sdl/sdl-provider.h"
|
||||
#include "base/main.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
#ifdef __SWITCH_DEBUG__
|
||||
socketInitializeDefault();
|
||||
nxlinkStdio();
|
||||
#endif
|
||||
|
||||
// Create our OSystem instance
|
||||
g_system = new OSystem_Switch();
|
||||
assert(g_system);
|
||||
|
||||
// Pre initialize the backend
|
||||
((OSystem_Switch *)g_system)->init();
|
||||
|
||||
#ifdef DYNAMIC_MODULES
|
||||
PluginManager::instance().addPluginProvider(new SDLPluginProvider());
|
||||
#endif
|
||||
|
||||
// Invoke the actual ScummVM main entry point:
|
||||
int res = scummvm_main(argc, argv);
|
||||
|
||||
// Free OSystem
|
||||
delete (OSystem_Switch *)g_system;
|
||||
|
||||
#ifdef __SWITCH_DEBUG__
|
||||
socketExit();
|
||||
#endif
|
||||
|
||||
return res;
|
||||
}
|
111
backends/platform/sdl/switch/switch.cpp
Normal file
111
backends/platform/sdl/switch/switch.cpp
Normal file
@ -0,0 +1,111 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_printf
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_unistd_h
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include "common/scummsys.h"
|
||||
#include "common/config-manager.h"
|
||||
#include "backends/platform/sdl/switch/switch.h"
|
||||
#include "backends/events/switchsdl/switchsdl-events.h"
|
||||
#include "backends/saves/posix/posix-saves.h"
|
||||
#include "backends/fs/posix/posix-fs-factory.h"
|
||||
#include "backends/fs/posix/posix-fs.h"
|
||||
|
||||
OSystem_Switch::OSystem_Switch(Common::String baseConfigName)
|
||||
: _baseConfigName(baseConfigName) {
|
||||
}
|
||||
|
||||
void OSystem_Switch::init() {
|
||||
|
||||
// Initialze File System Factory
|
||||
_fsFactory = new POSIXFilesystemFactory();
|
||||
|
||||
// Invoke parent implementation of this method
|
||||
OSystem_SDL::init();
|
||||
}
|
||||
|
||||
void OSystem_Switch::initBackend() {
|
||||
|
||||
ConfMan.registerDefault("joystick_num", 0);
|
||||
ConfMan.registerDefault("fullscreen", true);
|
||||
ConfMan.registerDefault("aspect_ratio", false);
|
||||
ConfMan.registerDefault("gfx_mode", "2x");
|
||||
ConfMan.registerDefault("output_rate", 48000);
|
||||
ConfMan.registerDefault("touchpad_mouse_mode", true);
|
||||
|
||||
if (!ConfMan.hasKey("joystick_num")) {
|
||||
ConfMan.setInt("joystick_num", 0);
|
||||
}
|
||||
if (!ConfMan.hasKey("fullscreen")) {
|
||||
ConfMan.setBool("fullscreen", true);
|
||||
}
|
||||
if (!ConfMan.hasKey("aspect_ratio")) {
|
||||
ConfMan.setBool("aspect_ratio", false);
|
||||
}
|
||||
if (!ConfMan.hasKey("gfx_mode")) {
|
||||
ConfMan.set("gfx_mode", "2x");
|
||||
}
|
||||
if (!ConfMan.hasKey("output_rate")) {
|
||||
ConfMan.setInt("output_rate", 48000);
|
||||
}
|
||||
if (!ConfMan.hasKey("touchpad_mouse_mode")) {
|
||||
ConfMan.setBool("touchpad_mouse_mode", true);
|
||||
}
|
||||
|
||||
// Create the savefile manager
|
||||
if (_savefileManager == 0) {
|
||||
_savefileManager = new POSIXSaveFileManager();
|
||||
}
|
||||
|
||||
// Event source
|
||||
if (_eventSource == 0) {
|
||||
_eventSource = new SwitchEventSource();
|
||||
}
|
||||
|
||||
// Invoke parent implementation of this method
|
||||
OSystem_SDL::initBackend();
|
||||
}
|
||||
|
||||
bool OSystem_Switch::hasFeature(Feature f) {
|
||||
|
||||
if (f == kFeatureDisplayLogFile)
|
||||
return false;
|
||||
if (f == kFeatureOpenUrl)
|
||||
return false;
|
||||
|
||||
return OSystem_SDL::hasFeature(f);
|
||||
}
|
||||
|
||||
void OSystem_Switch::logMessage(LogMessageType::Type type, const char *message) {
|
||||
printf("%s\n", message);
|
||||
}
|
||||
|
||||
Common::String OSystem_Switch::getDefaultConfigFileName() {
|
||||
return _baseConfigName;
|
||||
}
|
||||
|
||||
Common::WriteStream *OSystem_Switch::createLogFile() {
|
||||
Common::FSNode file("scummvm.log");
|
||||
return file.createWriteStream();
|
||||
}
|
50
backends/platform/sdl/switch/switch.h
Normal file
50
backends/platform/sdl/switch/switch.h
Normal file
@ -0,0 +1,50 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_SDL_SWITCH_H
|
||||
#define PLATFORM_SDL_SWITCH_H
|
||||
|
||||
#include "backends/platform/sdl/sdl.h"
|
||||
|
||||
class OSystem_Switch : public OSystem_SDL {
|
||||
public:
|
||||
// Let the subclasses be able to change _baseConfigName in the constructor
|
||||
OSystem_Switch(Common::String baseConfigName = "scummvm.ini");
|
||||
virtual ~OSystem_Switch() {}
|
||||
|
||||
virtual void init() override;
|
||||
virtual void initBackend() override;
|
||||
virtual bool hasFeature(Feature f) override;
|
||||
virtual void logMessage(LogMessageType::Type type, const char *message) override;
|
||||
|
||||
protected:
|
||||
// Base string for creating the default path and filename
|
||||
// for the configuration file
|
||||
Common::String _baseConfigName;
|
||||
|
||||
virtual Common::String getDefaultConfigFileName() override;
|
||||
|
||||
virtual Common::WriteStream *createLogFile() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
19
backends/platform/sdl/switch/switch.mk
Normal file
19
backends/platform/sdl/switch/switch.mk
Normal file
@ -0,0 +1,19 @@
|
||||
scummvm.nro: $(EXECUTABLE)
|
||||
mkdir -p $(srcdir)/switch_release/scummvm/data
|
||||
mkdir -p $(srcdir)/switch_release/scummvm/doc
|
||||
nacptool --create "ScummVM" "Cpasjuste" "2.12" $(srcdir)/switch_release/scummvm.nacp
|
||||
elf2nro $(EXECUTABLE) $(srcdir)/switch_release/scummvm/scummvm.nro --icon=$(srcdir)/dists/switch/icon.jpg --nacp=$(srcdir)/switch_release/scummvm.nacp
|
||||
|
||||
scummvm_switch.zip: scummvm.nro
|
||||
rm -f $(srcdir)/switch_release/scummvm.nacp
|
||||
cp $(srcdir)/backends/vkeybd/packs/vkeybd_default.zip $(srcdir)/switch_release/scummvm/data
|
||||
cp $(srcdir)/backends/vkeybd/packs/vkeybd_small.zip $(srcdir)/switch_release/scummvm/data
|
||||
cp $(DIST_FILES_THEMES) $(srcdir)/switch_release/scummvm/data
|
||||
ifdef DIST_FILES_ENGINEDATA
|
||||
cp $(DIST_FILES_ENGINEDATA) $(srcdir)/switch_release/scummvm/data
|
||||
endif
|
||||
cp $(DIST_FILES_DOCS) $(srcdir)/switch_release/scummvm/doc/
|
||||
cd $(srcdir)/switch_release && zip -r ../scummvm_switch.zip . && cd ..
|
||||
|
||||
.PHONY: scummvm.nro scummvm_switch.zip
|
||||
|
@ -47,6 +47,9 @@ POSIXSaveFileManager::POSIXSaveFileManager() {
|
||||
// Register default savepath.
|
||||
#if defined(SAMSUNGTV)
|
||||
ConfMan.registerDefault("savepath", "/mtd_wiselink/scummvm savegames");
|
||||
#elif defined(NINTENDO_SWITCH)
|
||||
Posix::assureDirectoryExists("./saves", nullptr);
|
||||
ConfMan.registerDefault("savepath", "./saves");
|
||||
#else
|
||||
Common::String savePath;
|
||||
|
||||
|
58
configure
vendored
58
configure
vendored
@ -526,7 +526,7 @@ get_system_exe_extension() {
|
||||
riscos)
|
||||
_exeext=",e1f"
|
||||
;;
|
||||
3ds | dreamcast | ds | gamecube | n64 | ps2 | psp | wii)
|
||||
3ds | dreamcast | ds | gamecube | n64 | ps2 | psp | switch | wii)
|
||||
_exeext=".elf"
|
||||
;;
|
||||
gph-linux)
|
||||
@ -919,7 +919,7 @@ Configuration:
|
||||
-h, --help display this help and exit
|
||||
--backend=BACKEND backend to build (3ds, android, dc, dingux, ds, gcw0,
|
||||
gph, iphone, ios7, linuxmoto, maemo, n64, null, openpandora,
|
||||
ps2, psp, psp2, samsungtv, sdl, tizen, webos, wii, wince) [sdl]
|
||||
ps2, psp, psp2, samsungtv, sdl, switch, tizen, webos, wii, wince) [sdl]
|
||||
|
||||
Installation directories:
|
||||
--prefix=PREFIX install architecture-independent files in PREFIX
|
||||
@ -972,6 +972,7 @@ Special configuration feature:
|
||||
psp2 for PlayStation Vita
|
||||
psp for PlayStation Portable
|
||||
samsungtv for Samsung TV
|
||||
switch for Nintendo Switch
|
||||
tizen for Samsung Tizen
|
||||
webos for HP Palm WebOS
|
||||
wii for Nintendo Wii
|
||||
@ -1685,6 +1686,16 @@ samsungtv)
|
||||
_host_cpu=arm
|
||||
_host_alias=arm-linux-gnueabi
|
||||
;;
|
||||
switch)
|
||||
_host_os=switch
|
||||
_host_cpu=arm
|
||||
_host_alias=aarch64-none-elf
|
||||
test "x$prefix" = xNONE && prefix=.
|
||||
datarootdir='${prefix}/data'
|
||||
datadir='${datarootdir}'
|
||||
docdir='${prefix}/doc'
|
||||
PKG_CONFIG_LIBDIR=$DEVKITPRO/portlibs/switch/lib/pkgconfig
|
||||
;;
|
||||
tizen)
|
||||
_host_os=tizen
|
||||
_host_cpu=arm
|
||||
@ -1784,7 +1795,7 @@ android)
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
3ds | ds | gamecube | wii)
|
||||
3ds | ds | gamecube | switch | wii)
|
||||
if test -z "$DEVKITPRO"; then
|
||||
echo "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to devkitPRO>"
|
||||
exit 1
|
||||
@ -2101,7 +2112,7 @@ if test "$have_gcc" = yes ; then
|
||||
case $_host_os in
|
||||
# newlib-based system include files suppress non-C89 function
|
||||
# declarations under __STRICT_ANSI__
|
||||
3ds | amigaos* | android | androidsdl | dreamcast | ds | gamecube | mingw* | mint* | n64 | psp | ps2 | ps3 | psp2 | tizen | wii | wince )
|
||||
3ds | amigaos* | android | androidsdl | dreamcast | ds | gamecube | mingw* | mint* | n64 | psp | ps2 | ps3 | psp2 | switch | tizen | wii | wince )
|
||||
;;
|
||||
*)
|
||||
append_var CXXFLAGS "-ansi"
|
||||
@ -2175,7 +2186,7 @@ fi
|
||||
# However, some platforms use GNU extensions in system header files, so
|
||||
# for these we must not use -pedantic.
|
||||
case $_host_os in
|
||||
3ds | android | androidsdl | gamecube | ps2 | psp | tizen | wii | webos)
|
||||
3ds | android | androidsdl | gamecube | ps2 | psp | switch | tizen | wii | webos)
|
||||
;;
|
||||
*)
|
||||
# ICC does not support pedantic, while GCC and clang do.
|
||||
@ -2883,6 +2894,22 @@ case $_host_os in
|
||||
# Needs -lbind -lsocket for the timidity MIDI driver
|
||||
append_var LIBS "-lnsl -lsocket"
|
||||
;;
|
||||
switch)
|
||||
_pkgconfig="$DEVKITPRO/portlibs/switch/bin/aarch64-none-elf-pkg-config"
|
||||
_sdlpath="$DEVKITPRO/portlibs/switch/bin"
|
||||
append_var DEFINES "-DSWITCH -D__SWITCH__ -DNINTENDO_SWITCH"
|
||||
append_var CXXFLAGS "-march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE -ftls-model=local-exec"
|
||||
append_var CXXFLAGS "-ffunction-sections -fdata-sections"
|
||||
append_var CXXFLAGS "-I$DEVKITPRO/libnx/include -I$DEVKITPRO/portlibs/switch/include"
|
||||
append_var LDFLAGS "-L$DEVKITPRO/libnx/lib -L$DEVKITPRO/portlibs/switch/lib"
|
||||
append_var LDFLAGS "-specs=$DEVKITPRO/libnx/switch.specs"
|
||||
add_line_to_config_mk 'SWITCH = 1'
|
||||
if test "$_debug_build" = yes; then
|
||||
append_var DEFINES "-D__SWITCH_DEBUG__"
|
||||
else
|
||||
_optimization_level=-O3
|
||||
fi
|
||||
;;
|
||||
tizen)
|
||||
add_line_to_config_mk "TIZEN_ROOTSTRAP = $TIZEN_ROOTSTRAP"
|
||||
append_var LDFLAGS "--sysroot=${TIZEN_ROOTSTRAP}"
|
||||
@ -3374,6 +3401,19 @@ if test -n "$_host"; then
|
||||
_mt32emu=no
|
||||
_vkeybd=yes
|
||||
;;
|
||||
switch)
|
||||
_backend="switch"
|
||||
_opengl_mode=gles2
|
||||
_build_scalers=yes
|
||||
_build_hq_scalers=yes
|
||||
_dynamic_modules=no
|
||||
_vkeybd=yes
|
||||
_mt32emu=yes
|
||||
_vorbis=yes
|
||||
_tremor=no
|
||||
_eventrec=no
|
||||
_port_mk="backends/platform/sdl/switch/switch.mk"
|
||||
;;
|
||||
tizen)
|
||||
_unix=yes
|
||||
_backend="tizen"
|
||||
@ -3566,6 +3606,10 @@ case $_backend in
|
||||
sdl)
|
||||
_sdl=auto
|
||||
;;
|
||||
switch)
|
||||
_sdl=auto
|
||||
append_var MODULES "backends/platform/sdl"
|
||||
;;
|
||||
tizen)
|
||||
# dirent.h not available. NONSTANDARD_PORT==ensure portdefs.h is included
|
||||
append_var DEFINES "-DTIZEN -DDISABLE_STDIO_FILESTREAM -DNONSTANDARD_PORT"
|
||||
@ -3686,7 +3730,7 @@ fi
|
||||
# Enable 16bit support only for backends which support it
|
||||
#
|
||||
case $_backend in
|
||||
3ds | android | androidsdl | dingux | dc | gph | iphone | ios7 | maemo | openpandora | psp | psp2 | samsungtv | sdl | tizen | webos | wii)
|
||||
3ds | android | androidsdl | dingux | dc | gph | iphone | ios7 | maemo | openpandora | psp | psp2 | samsungtv | sdl | switch | tizen | webos | wii)
|
||||
if test "$_16bit" = auto ; then
|
||||
_16bit=yes
|
||||
else
|
||||
@ -3765,7 +3809,7 @@ case $_host_os in
|
||||
amigaos* | cygwin* | dreamcast | ds | gamecube | mingw* | n64 | ps2 | ps3 | psp2 | psp | riscos | wii | wince)
|
||||
_posix=no
|
||||
;;
|
||||
3ds | android | androidsdl | beos* | bsd* | darwin* | freebsd* | gnu* | gph-linux | haiku* | hpux* | iphone | ios7 | irix*| k*bsd*-gnu* | linux* | maemo | mint* | netbsd* | openbsd* | solaris* | sunos* | uclinux* | webos)
|
||||
3ds | android | androidsdl | beos* | bsd* | darwin* | freebsd* | gnu* | gph-linux | haiku* | hpux* | iphone | ios7 | irix*| k*bsd*-gnu* | linux* | maemo | mint* | netbsd* | openbsd* | solaris* | sunos* | switch | uclinux* | webos)
|
||||
_posix=yes
|
||||
;;
|
||||
os2-emx*)
|
||||
|
BIN
dists/switch/icon.jpg
Normal file
BIN
dists/switch/icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
Loading…
Reference in New Issue
Block a user