diff options
Diffstat (limited to 'backends')
307 files changed, 25903 insertions, 1603 deletions
diff --git a/backends/audiocd/audiocd-stream.cpp b/backends/audiocd/audiocd-stream.cpp new file mode 100644 index 0000000000..3c0d0957da --- /dev/null +++ b/backends/audiocd/audiocd-stream.cpp @@ -0,0 +1,199 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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 "backends/audiocd/audiocd-stream.h" +#include "common/textconsole.h" + +AudioCDStream::AudioCDStream() : _buffer(), _frame(0), _bufferPos(0), _bufferFrame(0), _forceStop(false) { +} + +AudioCDStream::~AudioCDStream() { + // Stop the timer; the subclass needs to do this, + // so this is just a last resort. + stopTimer(); + + // Clear any buffered frames + emptyQueue(); +} + +int AudioCDStream::readBuffer(int16 *buffer, const int numSamples) { + int samples = 0; + + // See if any data is left first + while (_bufferPos < kSamplesPerFrame && samples < numSamples) + buffer[samples++] = _buffer[_bufferPos++]; + + // Bail out if done + if (endOfData()) + return samples; + + while (samples < numSamples && !endOfData()) { + if (!readNextFrame()) + return samples; + + // Copy the samples over + for (_bufferPos = 0; _bufferPos < kSamplesPerFrame && samples < numSamples;) + buffer[samples++] = _buffer[_bufferPos++]; + } + + return samples; +} + +bool AudioCDStream::readNextFrame() { + // Fetch a frame from the queue + int16 *buffer; + + { + Common::StackLock lock(_mutex); + + // Nothing we can do if it's empty + if (_bufferQueue.empty()) + return false; + + buffer = _bufferQueue.pop(); + } + + memcpy(_buffer, buffer, kSamplesPerFrame * 2); + delete[] buffer; + _frame++; + return true; +} + +bool AudioCDStream::endOfData() const { + return !shouldForceStop() && getStartFrame() + _frame >= getEndFrame() && _bufferPos == kSamplesPerFrame; +} + +bool AudioCDStream::seek(const Audio::Timestamp &where) { + // Stop the timer + stopTimer(); + + // Clear anything out of the queue + emptyQueue(); + + // Convert to the frame number + // Really not much else needed + _bufferPos = kSamplesPerFrame; + _frame = where.convertToFramerate(kFramesPerSecond).totalNumberOfFrames(); + _bufferFrame = _frame; + + // Start the timer again + startTimer(); + return true; +} + +Audio::Timestamp AudioCDStream::getLength() const { + return Audio::Timestamp(0, getEndFrame() - getStartFrame(), kFramesPerSecond); +} + +void AudioCDStream::timerProc(void *refCon) { + static_cast<AudioCDStream *>(refCon)->onTimer(); +} + +void AudioCDStream::onTimer() { + // The goal here is to do as much work in this timer instead + // of doing it in the readBuffer() call, which is the mixer. + + // If we're done, bail. + if (shouldForceStop() || getStartFrame() + _bufferFrame >= getEndFrame()) + return; + + // Get a quick count of the number of items in the queue + // We don't care that much; we only need a quick estimate + _mutex.lock(); + uint32 queueCount = _bufferQueue.size(); + _mutex.unlock(); + + // If we have enough audio buffered, bail out + if (queueCount >= kBufferThreshold) + return; + + while (!shouldForceStop() && queueCount < kBufferThreshold && getStartFrame() + _bufferFrame < getEndFrame()) { + int16 *buffer = new int16[kSamplesPerFrame]; + + // Figure out the MSF of the frame we're looking for + int frame = _bufferFrame + getStartFrame(); + + // Request to read that frame + if (!readFrame(frame, buffer)) { + warning("Failed to read CD audio"); + forceStop(); + return; + } + + _bufferFrame++; + + // Now push the buffer onto the queue + Common::StackLock lock(_mutex); + _bufferQueue.push(buffer); + queueCount = _bufferQueue.size(); + } +} + +void AudioCDStream::startTimer(bool fillBuffer) { + _forceStop = false; + if (fillBuffer) { + onTimer(); + } + g_system->getTimerManager()->installTimerProc(timerProc, 10 * 1000, this, "AudioCDStream"); +} + +void AudioCDStream::stopTimer() { + forceStop(); + g_system->getTimerManager()->removeTimerProc(timerProc); +} + +void AudioCDStream::emptyQueue() { + while (!_bufferQueue.empty()) + delete[] _bufferQueue.pop(); +} + +bool AudioCDStream::shouldForceStop() const { + Common::StackLock lock(_forceStopMutex); + return _forceStop; +} + +void AudioCDStream::forceStop() { + Common::StackLock lock(_forceStopMutex); + _forceStop = true; +} diff --git a/backends/audiocd/audiocd-stream.h b/backends/audiocd/audiocd-stream.h new file mode 100644 index 0000000000..dbc6a6321b --- /dev/null +++ b/backends/audiocd/audiocd-stream.h @@ -0,0 +1,108 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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 BACKENDS_AUDIOCD_AUDIOCD_STREAM_H +#define BACKENDS_AUDIOCD_AUDIOCD_STREAM_H + +#include "audio/audiostream.h" +#include "common/mutex.h" +#include "common/queue.h" +#include "common/timer.h" + +class AudioCDStream : public Audio::SeekableAudioStream { +public: + AudioCDStream(); + ~AudioCDStream(); + + int readBuffer(int16 *buffer, const int numSamples); + bool isStereo() const { return true; } + int getRate() const { return 44100; } + bool endOfData() const; + bool seek(const Audio::Timestamp &where); + Audio::Timestamp getLength() const; + +protected: + virtual uint getStartFrame() const = 0; + virtual uint getEndFrame() const = 0; + virtual bool readFrame(int frame, int16 *buffer) = 0; + + void startTimer(bool fillBuffer = false); + void stopTimer(); + + enum { + kBytesPerFrame = 2352, + kSamplesPerFrame = kBytesPerFrame / 2 + }; + + enum { + kSecondsPerMinute = 60, + kFramesPerSecond = 75 + }; + + enum { + // Keep about a second's worth of audio in the buffer + kBufferThreshold = kFramesPerSecond + }; + +private: + int16 _buffer[kSamplesPerFrame]; + int _frame; + uint _bufferPos; + + Common::Queue<int16 *> _bufferQueue; + int _bufferFrame; + Common::Mutex _mutex; + + bool _forceStop; + bool shouldForceStop() const; + void forceStop(); + Common::Mutex _forceStopMutex; + + bool readNextFrame(); + static void timerProc(void *refCon); + void onTimer(); + void emptyQueue(); +}; + +#endif diff --git a/backends/audiocd/audiocd.h b/backends/audiocd/audiocd.h index 6eae8e096b..b3674f2570 100644 --- a/backends/audiocd/audiocd.h +++ b/backends/audiocd/audiocd.h @@ -48,26 +48,31 @@ public: }; /** - * @name Emulated playback functions - * Engines should call these functions. Not all platforms - * support cd playback, and these functions should try to - * emulate it. + * Initialize the specified CD drive for audio playback. + * @return true if the CD drive was inited successfully + */ + virtual bool open() = 0; + + /** + * Close the currently open CD drive */ - //@{ + virtual void close() = 0; /** * Start audio CD playback - * @param track the track to play. - * @param numLoops how often playback should be repeated (-1 = infinitely often). - * @param startFrame the frame at which playback should start (75 frames = 1 second). - * @param duration the number of frames to play. - * @param only_emulate determines if the track should be emulated only + * @param track the track to play. + * @param numLoops how often playback should be repeated (<=0 means infinitely often). + * @param startFrame the frame at which playback should start (75 frames = 1 second). + * @param duration the number of frames to play. + * @param onlyEmulate determines if the track should be emulated only + * @note The @c onlyEmulate parameter is deprecated. + * @return @c true if the track started playing, @c false otherwise */ - virtual void play(int track, int numLoops, int startFrame, int duration, bool only_emulate = false) = 0; + virtual bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false) = 0; /** * Get if audio is being played. - * @return true if CD or emulated audio is playing + * @return true if CD audio is playing */ virtual bool isPlaying() const = 0; @@ -82,12 +87,12 @@ public: virtual void setBalance(int8 balance) = 0; /** - * Stop CD or emulated audio playback. + * Stop audio playback. */ virtual void stop() = 0; /** - * Update CD or emulated audio status. + * Update audio status. */ virtual void update() = 0; @@ -96,50 +101,6 @@ public: * @return a Status struct with playback data. */ virtual Status getStatus() const = 0; - - //@} - - - /** - * @name Real CD audio methods - * These functions should be called from the emulated - * ones if they can't emulate the audio playback. - */ - //@{ - - /** - * Initialize the specified CD drive for audio playback. - * @param drive the drive id - * @return true if the CD drive was inited successfully - */ - virtual bool openCD(int drive) = 0; - - /** - * Poll CD status. - * @return true if CD audio is playing - */ - virtual bool pollCD() const = 0; - - /** - * Start CD audio playback. - * @param track the track to play. - * @param num_loops how often playback should be repeated (-1 = infinitely often). - * @param start_frame the frame at which playback should start (75 frames = 1 second). - * @param duration the number of frames to play. - */ - virtual void playCD(int track, int num_loops, int start_frame, int duration) = 0; - - /** - * Stop CD audio playback. - */ - virtual void stopCD() = 0; - - /** - * Update CD audio status. - */ - virtual void updateCD() = 0; - - //@} }; #endif diff --git a/backends/audiocd/default/default-audiocd.cpp b/backends/audiocd/default/default-audiocd.cpp index abf80ac4cd..c2ce7cedcc 100644 --- a/backends/audiocd/default/default-audiocd.cpp +++ b/backends/audiocd/default/default-audiocd.cpp @@ -22,6 +22,7 @@ #include "backends/audiocd/default/default-audiocd.h" #include "audio/audiostream.h" +#include "common/config-manager.h" #include "common/system.h" DefaultAudioCDManager::DefaultAudioCDManager() { @@ -37,7 +38,25 @@ DefaultAudioCDManager::DefaultAudioCDManager() { assert(_mixer); } -void DefaultAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool only_emulate) { +DefaultAudioCDManager::~DefaultAudioCDManager() { + // Subclasses should call close as well + close(); +} + +bool DefaultAudioCDManager::open() { + // For emulation, opening is always valid + close(); + return true; +} + +void DefaultAudioCDManager::close() { + // Only need to stop for emulation + stop(); +} + +bool DefaultAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + stop(); + if (numLoops != 0 || startFrame != 0) { _cd.track = track; _cd.numLoops = numLoops; @@ -55,9 +74,6 @@ void DefaultAudioCDManager::play(int track, int numLoops, int startFrame, int du for (int i = 0; !stream && i < 2; ++i) stream = Audio::SeekableAudioStream::openStreamFile(trackName[i]); - // Stop any currently playing emulated track - _mixer->stopHandle(_handle); - if (stream != 0) { Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75); Audio::Timestamp end = duration ? Audio::Timestamp(0, startFrame + duration, 75) : stream->getLength(); @@ -70,12 +86,11 @@ void DefaultAudioCDManager::play(int track, int numLoops, int startFrame, int du _emulating = true; _mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, Audio::makeLoopingAudioStream(stream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops), -1, _cd.volume, _cd.balance); - } else { - _emulating = false; - if (!only_emulate) - playCD(track, numLoops, startFrame, duration); + return true; } } + + return false; } void DefaultAudioCDManager::stop() { @@ -83,52 +98,32 @@ void DefaultAudioCDManager::stop() { // Audio CD emulation _mixer->stopHandle(_handle); _emulating = false; - } else { - // Real Audio CD - stopCD(); } } bool DefaultAudioCDManager::isPlaying() const { - if (_emulating) { - // Audio CD emulation + // Audio CD emulation + if (_emulating) return _mixer->isSoundHandleActive(_handle); - } else { - // Real Audio CD - return pollCD(); - } + + // The default class only handles emulation + return false; } void DefaultAudioCDManager::setVolume(byte volume) { _cd.volume = volume; - if (_emulating) { - // Audio CD emulation - if (_mixer->isSoundHandleActive(_handle)) - _mixer->setChannelVolume(_handle, _cd.volume); - } else { - // Real Audio CD - // Unfortunately I can't implement this atm - // since SDL doesn't seem to offer an interface method for this. - - // g_system->setVolumeCD(_cd.volume); - } + // Audio CD emulation + if (_emulating && isPlaying()) + _mixer->setChannelVolume(_handle, _cd.volume); } void DefaultAudioCDManager::setBalance(int8 balance) { _cd.balance = balance; - if (_emulating) { - // Audio CD emulation - if (isPlaying()) - _mixer->setChannelBalance(_handle, _cd.balance); - } else { - // Real Audio CD - // Unfortunately I can't implement this atm - // since SDL doesn't seem to offer an interface method for this. - - // g_system->setBalanceCD(_cd.balance); - } + // Audio CD emulation + if (_emulating && isPlaying()) + _mixer->setChannelBalance(_handle, _cd.balance); } void DefaultAudioCDManager::update() { @@ -142,8 +137,6 @@ void DefaultAudioCDManager::update() { // or not. _emulating = false; } - } else { - updateCD(); } } @@ -152,3 +145,21 @@ DefaultAudioCDManager::Status DefaultAudioCDManager::getStatus() const { info.playing = isPlaying(); return info; } + +bool DefaultAudioCDManager::openRealCD() { + Common::String cdrom = ConfMan.get("cdrom"); + + // Try to parse it as an int + char *endPos; + int drive = strtol(cdrom.c_str(), &endPos, 0); + + // If not an integer, treat as a drive path + if (endPos == cdrom.c_str()) + return openCD(cdrom); + + if (drive < 0) + return false; + + return openCD(drive); +} + diff --git a/backends/audiocd/default/default-audiocd.h b/backends/audiocd/default/default-audiocd.h index 9e4ba6b33e..e3fbb4b5a1 100644 --- a/backends/audiocd/default/default-audiocd.h +++ b/backends/audiocd/default/default-audiocd.h @@ -26,29 +26,48 @@ #include "backends/audiocd/audiocd.h" #include "audio/mixer.h" +namespace Common { +class String; +} // End of namespace Common + /** * The default audio cd manager. Implements emulation of audio cd playback. */ class DefaultAudioCDManager : public AudioCDManager { public: DefaultAudioCDManager(); - virtual ~DefaultAudioCDManager() {} - - void play(int track, int numLoops, int startFrame, int duration, bool only_emulate = false); - void stop(); - bool isPlaying() const; - void setVolume(byte volume); - void setBalance(int8 balance); - void update(); + virtual ~DefaultAudioCDManager(); + + virtual bool open(); + virtual void close(); + virtual bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); + virtual void stop(); + virtual bool isPlaying() const; + virtual void setVolume(byte volume); + virtual void setBalance(int8 balance); + virtual void update(); virtual Status getStatus() const; // Subclasses should override for better status results +protected: + /** + * Open a CD using the cdrom config variable + */ + bool openRealCD(); + + /** + * Open a CD using the specified drive index + * @param drive The index of the drive + * @note The index is implementation-defined, but 0 is always the best choice + */ virtual bool openCD(int drive) { return false; } - virtual void updateCD() {} - virtual bool pollCD() const { return false; } - virtual void playCD(int track, int num_loops, int start_frame, int duration) {} - virtual void stopCD() {} -protected: + /** + * Open a CD from a specific drive + * @param drive The name of the drive/path + * @note The drive parameter is platform-specific + */ + virtual bool openCD(const Common::String &drive) { return false; } + Audio::SoundHandle _handle; bool _emulating; diff --git a/backends/audiocd/linux/linux-audiocd.cpp b/backends/audiocd/linux/linux-audiocd.cpp new file mode 100644 index 0000000000..caa0265637 --- /dev/null +++ b/backends/audiocd/linux/linux-audiocd.cpp @@ -0,0 +1,471 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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. + * + */ + +// Enable all forbidden symbols to allow us to include and use necessary APIs. +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/audiocd/linux/linux-audiocd.h" + +#ifdef USE_LINUXCD + +#include "backends/audiocd/audiocd-stream.h" +#include "backends/audiocd/default/default-audiocd.h" +#include "common/array.h" +#include "common/config-manager.h" +#include "common/str.h" +#include "common/debug.h" + +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <linux/cdrom.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> + +enum { + kLeadoutTrack = 0xAA +}; + +enum { + kBytesPerFrame = 2352, + kSamplesPerFrame = kBytesPerFrame / 2 +}; + +enum { + kSecondsPerMinute = 60, + kFramesPerSecond = 75 +}; + +enum { + // Keep about a second's worth of audio in the buffer + kBufferThreshold = kFramesPerSecond +}; + +static int getFrameCount(const cdrom_msf0 &msf) { + int time = msf.minute; + time *= kSecondsPerMinute; + time += msf.second; + time *= kFramesPerSecond; + time += msf.frame; + return time; +} + +// Helper function to convert an error code into a human-readable message +static Common::String getErrorMessage(int errorCode) { + char buf[256]; + buf[0] = 0; + +#ifdef _GNU_SOURCE + // glibc sucks + return Common::String(strerror_r(errorCode, buf, sizeof(buf))); +#else + strerror_r(errorCode, buf, sizeof(buf)); + return Common::String(buf); +#endif +} + +class LinuxAudioCDStream : public AudioCDStream { +public: + LinuxAudioCDStream(int fd, const cdrom_tocentry &startEntry, const cdrom_tocentry &endEntry); + ~LinuxAudioCDStream(); + +protected: + uint getStartFrame() const; + uint getEndFrame() const; + bool readFrame(int frame, int16 *buffer); + +private: + int _fd; + const cdrom_tocentry &_startEntry, &_endEntry; +}; + +LinuxAudioCDStream::LinuxAudioCDStream(int fd, const cdrom_tocentry &startEntry, const cdrom_tocentry &endEntry) : + _fd(fd), _startEntry(startEntry), _endEntry(endEntry) { + // We fill the buffer here already to prevent any out of sync issues due + // to the CD not yet having spun up. + startTimer(true); +} + +LinuxAudioCDStream::~LinuxAudioCDStream() { + stopTimer(); +} + +bool LinuxAudioCDStream::readFrame(int frame, int16 *buffer) { + // Create the argument + union { + cdrom_msf msf; + char buffer[kBytesPerFrame]; + } arg; + + int seconds = frame / kFramesPerSecond; + frame %= kFramesPerSecond; + int minutes = seconds / kSecondsPerMinute; + seconds %= kSecondsPerMinute; + + // Request to read that frame + // We don't use CDROMREADAUDIO, as it seems to cause kernel + // panics on ejecting discs. Probably bad to eject the disc + // while playing, but at least let's try to prevent that case. + arg.msf.cdmsf_min0 = minutes; + arg.msf.cdmsf_sec0 = seconds; + arg.msf.cdmsf_frame0 = frame; + // The "end" part is irrelevant (why isn't cdrom_msf0 the type + // instead?) + + if (ioctl(_fd, CDROMREADRAW, &arg) < 0) { + warning("Failed to CD read audio: %s", getErrorMessage(errno).c_str()); + return false; + } + + memcpy(buffer, arg.buffer, kBytesPerFrame); + return true; +} + +uint LinuxAudioCDStream::getStartFrame() const { + return getFrameCount(_startEntry.cdte_addr.msf); +} + +uint LinuxAudioCDStream::getEndFrame() const { + return getFrameCount(_endEntry.cdte_addr.msf); +} + + +class LinuxAudioCDManager : public DefaultAudioCDManager { +public: + LinuxAudioCDManager(); + ~LinuxAudioCDManager(); + + bool open(); + void close(); + bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); + +protected: + bool openCD(int drive); + bool openCD(const Common::String &drive); + +private: + struct Device { + Device(const Common::String &n, dev_t d) : name(n), device(d) {} + Common::String name; + dev_t device; + }; + + typedef Common::Array<Device> DeviceList; + DeviceList scanDevices(); + bool tryAddDrive(DeviceList &devices, const Common::String &drive); + bool tryAddDrive(DeviceList &devices, const Common::String &drive, dev_t device); + bool tryAddDrive(DeviceList &devices, dev_t device); + bool tryAddPath(DeviceList &devices, const Common::String &path); + bool tryAddGamePath(DeviceList &devices); + bool loadTOC(); + static bool hasDevice(const DeviceList &devices, dev_t device); + + int _fd; + cdrom_tochdr _tocHeader; + Common::Array<cdrom_tocentry> _tocEntries; +}; + +static bool isTrayEmpty(int errorNumber) { + switch (errorNumber) { + case EIO: + case ENOENT: + case EINVAL: +#ifdef ENOMEDIUM + case ENOMEDIUM: +#endif + return true; + } + + return false; +} + +LinuxAudioCDManager::LinuxAudioCDManager() { + _fd = -1; + memset(&_tocHeader, 0, sizeof(_tocHeader)); +} + +LinuxAudioCDManager::~LinuxAudioCDManager() { + close(); +} + +bool LinuxAudioCDManager::open() { + close(); + + if (openRealCD()) + return true; + + return DefaultAudioCDManager::open(); +} + +void LinuxAudioCDManager::close() { + DefaultAudioCDManager::close(); + + if (_fd < 0) + return; + + ::close(_fd); + memset(&_tocHeader, 0, sizeof(_tocHeader)); + _tocEntries.clear(); +} + +bool LinuxAudioCDManager::openCD(int drive) { + DeviceList devices = scanDevices(); + if (drive >= (int)devices.size()) + return false; + + _fd = ::open(devices[drive].name.c_str(), O_RDONLY | O_NONBLOCK, 0); + if (_fd < 0) + return false; + + if (!loadTOC()) { + close(); + return false; + } + + return true; +} + +bool LinuxAudioCDManager::openCD(const Common::String &drive) { + DeviceList devices; + if (!tryAddDrive(devices, drive) && !tryAddPath(devices, drive)) + return false; + + _fd = ::open(devices[0].name.c_str(), O_RDONLY | O_NONBLOCK, 0); + if (_fd < 0) + return false; + + if (!loadTOC()) { + close(); + return false; + } + + return true; +} + +bool LinuxAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + // Prefer emulation + if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate)) + return true; + + // If we're set to only emulate, or have no CD drive, return here + if (onlyEmulate || _fd < 0) + return false; + + // HACK: For now, just assume that track number is right + // That only works because ScummVM uses the wrong track number anyway + + if (track >= (int)_tocEntries.size() - 1) { + warning("No such track %d", track); + return false; + } + + // Bail if the track isn't an audio track + if ((_tocEntries[track].cdte_ctrl & 0x04) != 0) { + warning("Track %d is not audio", track); + return false; + } + + // Create the AudioStream and play it + debug(1, "Playing CD track %d", track); + + Audio::SeekableAudioStream *audioStream = new LinuxAudioCDStream(_fd, _tocEntries[track], _tocEntries[track + 1]); + + Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75); + Audio::Timestamp end = (duration == 0) ? audioStream->getLength() : Audio::Timestamp(0, startFrame + duration, 75); + + // Fake emulation since we're really playing an AudioStream + _emulating = true; + + _mixer->playStream( + Audio::Mixer::kMusicSoundType, + &_handle, + Audio::makeLoopingAudioStream(audioStream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops), + -1, + _cd.volume, + _cd.balance, + DisposeAfterUse::YES, + true); + + return true; +} + +LinuxAudioCDManager::DeviceList LinuxAudioCDManager::scanDevices() { + DeviceList devices; + + // Try to use the game's path first as the device + tryAddGamePath(devices); + + // Try adding the default CD-ROM + tryAddDrive(devices, "/dev/cdrom"); + + // TODO: Try others? + + return devices; +} + +bool LinuxAudioCDManager::tryAddDrive(DeviceList &devices, const Common::String &drive) { + struct stat stbuf; + if (stat(drive.c_str(), &stbuf) < 0) + return false; + + // Must be a character or block device + if (!S_ISCHR(stbuf.st_mode) && !S_ISBLK(stbuf.st_mode)) + return false; + + return tryAddDrive(devices, drive, stbuf.st_rdev); +} + +bool LinuxAudioCDManager::tryAddDrive(DeviceList &devices, const Common::String &drive, dev_t device) { + if (hasDevice(devices, device)) + return true; + + // Try opening the device and seeing if it is a CD-ROM drve + int fd = ::open(drive.c_str(), O_RDONLY | O_NONBLOCK, 0); + if (fd >= 0) { + cdrom_subchnl info; + info.cdsc_format = CDROM_MSF; + + bool isCD = ioctl(fd, CDROMSUBCHNL, &info) == 0 || isTrayEmpty(errno); + ::close(fd); + if (isCD) { + devices.push_back(Device(drive, device)); + return true; + } + } + + return false; +} + +bool LinuxAudioCDManager::tryAddDrive(DeviceList &devices, dev_t device) { + // Construct the block name + // TODO: libblkid's blkid_devno_to_devname is exactly what we look for. + // This requires an external dependency though. + Common::String name = Common::String::format("/dev/block/%d:%d", major(device), minor(device)); + + return tryAddDrive(devices, name, device); +} + +bool LinuxAudioCDManager::tryAddPath(DeviceList &devices, const Common::String &path) { + struct stat stbuf; + if (stat(path.c_str(), &stbuf) < 0) + return false; + + return tryAddDrive(devices, stbuf.st_dev); +} + +bool LinuxAudioCDManager::tryAddGamePath(DeviceList &devices) { + if (!ConfMan.hasKey("path")) + return false; + + return tryAddPath(devices, ConfMan.get("path")); +} + +bool LinuxAudioCDManager::loadTOC() { + if (_fd < 0) + return false; + + if (ioctl(_fd, CDROMREADTOCHDR, &_tocHeader) < 0) + return false; + + debug(4, "CD: Start Track: %d, End Track %d", _tocHeader.cdth_trk0, _tocHeader.cdth_trk1); + + for (int i = _tocHeader.cdth_trk0; i <= _tocHeader.cdth_trk1; i++) { + cdrom_tocentry entry; + memset(&entry, 0, sizeof(entry)); + entry.cdte_track = i; + entry.cdte_format = CDROM_MSF; + + if (ioctl(_fd, CDROMREADTOCENTRY, &entry) < 0) + return false; + +#if 0 + debug("Entry:"); + debug("\tTrack: %d", entry.cdte_track); + debug("\tAdr: %d", entry.cdte_adr); + debug("\tCtrl: %d", entry.cdte_ctrl); + debug("\tFormat: %d", entry.cdte_format); + debug("\tMSF: %d:%d:%d", entry.cdte_addr.msf.minute, entry.cdte_addr.msf.second, entry.cdte_addr.msf.frame); + debug("\tMode: %d\n", entry.cdte_datamode); +#endif + + _tocEntries.push_back(entry); + } + + // Fetch the leadout so we can get the length of the last frame + cdrom_tocentry entry; + memset(&entry, 0, sizeof(entry)); + entry.cdte_track = kLeadoutTrack; + entry.cdte_format = CDROM_MSF; + + if (ioctl(_fd, CDROMREADTOCENTRY, &entry) < 0) + return false; + +#if 0 + debug("Lead out:"); + debug("\tTrack: %d", entry.cdte_track); + debug("\tAdr: %d", entry.cdte_adr); + debug("\tCtrl: %d", entry.cdte_ctrl); + debug("\tFormat: %d", entry.cdte_format); + debug("\tMSF: %d:%d:%d", entry.cdte_addr.msf.minute, entry.cdte_addr.msf.second, entry.cdte_addr.msf.frame); + debug("\tMode: %d\n", entry.cdte_datamode); +#endif + + _tocEntries.push_back(entry); + return true; +} + +bool LinuxAudioCDManager::hasDevice(const DeviceList &devices, dev_t device) { + for (DeviceList::const_iterator it = devices.begin(); it != devices.end(); it++) + if (it->device == device) + return true; + + return false; +} + +AudioCDManager *createLinuxAudioCDManager() { + return new LinuxAudioCDManager(); +} + +#endif // USE_LINUXCD diff --git a/backends/audiocd/linux/linux-audiocd.h b/backends/audiocd/linux/linux-audiocd.h new file mode 100644 index 0000000000..09d6353991 --- /dev/null +++ b/backends/audiocd/linux/linux-audiocd.h @@ -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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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 BACKENDS_AUDIOCD_LINUX_H +#define BACKENDS_AUDIOCD_LINUX_H + +#include "common/scummsys.h" + +#ifdef USE_LINUXCD + +class AudioCDManager; + +/** + * Create an audio CD manager using the Linux CDROM API + */ +AudioCDManager *createLinuxAudioCDManager(); + +#endif + +#endif + diff --git a/backends/audiocd/macosx/macosx-audiocd.cpp b/backends/audiocd/macosx/macosx-audiocd.cpp new file mode 100644 index 0000000000..e8d41c3e10 --- /dev/null +++ b/backends/audiocd/macosx/macosx-audiocd.cpp @@ -0,0 +1,306 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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. + * + */ + +#ifdef MACOSX + +#include <sys/stat.h> +#include <sys/mount.h> +#include <limits.h> + +#include "common/scummsys.h" + +#include "audio/audiostream.h" +#include "audio/decoders/aiff.h" +#include "audio/timestamp.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/fs.h" +#include "common/hashmap.h" +#include "common/textconsole.h" +#include "backends/audiocd/default/default-audiocd.h" +#include "backends/audiocd/macosx/macosx-audiocd.h" +#include "backends/fs/stdiostream.h" + +// Partially based on SDL's code + +/** + * The Mac OS X audio cd manager. Implements real audio cd playback. + */ +class MacOSXAudioCDManager : public DefaultAudioCDManager { +public: + MacOSXAudioCDManager() {} + ~MacOSXAudioCDManager(); + + bool open(); + void close(); + bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); + +protected: + bool openCD(int drive); + bool openCD(const Common::String &drive); + +private: + struct Drive { + Drive(const Common::String &m, const Common::String &d, const Common::String &f) : + mountPoint(m), deviceName(d), fsType(f) {} + + Common::String mountPoint; + Common::String deviceName; + Common::String fsType; + }; + + typedef Common::Array<Drive> DriveList; + DriveList detectAllDrives(); + DriveList detectCDDADrives(); + + bool findTrackNames(const Common::String &drivePath); + + Common::HashMap<uint, Common::String> _trackMap; +}; + +MacOSXAudioCDManager::~MacOSXAudioCDManager() { + close(); +} + +bool MacOSXAudioCDManager::open() { + close(); + + if (openRealCD()) + return true; + + return DefaultAudioCDManager::open(); +} + +/** + * Find the base disk number of device name. + * Returns -1 if mount point is not /dev/disk* + */ +static int findBaseDiskNumber(const Common::String &diskName) { + if (!diskName.hasPrefix("/dev/disk")) + return -1; + + const char *startPtr = diskName.c_str() + 9; + char *endPtr; + int baseDiskNumber = strtol(startPtr, &endPtr, 10); + if (startPtr == endPtr) + return -1; + + return baseDiskNumber; +} + +bool MacOSXAudioCDManager::openCD(int drive) { + DriveList allDrives = detectAllDrives(); + if (allDrives.empty()) + return false; + + DriveList cddaDrives; + + // Try to get the volume related to the game's path + if (ConfMan.hasKey("path")) { + Common::String gamePath = ConfMan.get("path"); + struct statfs gamePathStat; + if (statfs(gamePath.c_str(), &gamePathStat) == 0) { + int baseDiskNumber = findBaseDiskNumber(gamePathStat.f_mntfromname); + if (baseDiskNumber >= 0) { + // Look for a CDDA drive with the same base disk number + for (uint32 i = 0; i < allDrives.size(); i++) { + if (allDrives[i].fsType == "cddafs" && findBaseDiskNumber(allDrives[i].deviceName) == baseDiskNumber) { + debug(1, "Preferring drive '%s'", allDrives[i].mountPoint.c_str()); + cddaDrives.push_back(allDrives[i]); + allDrives.remove_at(i); + break; + } + } + } + } + } + + // Add the remaining CDDA drives to the CDDA list + for (uint32 i = 0; i < allDrives.size(); i++) + if (allDrives[i].fsType == "cddafs") + cddaDrives.push_back(allDrives[i]); + + if (drive >= (int)cddaDrives.size()) + return false; + + debug(1, "Using '%s' as the CD drive", cddaDrives[drive].mountPoint.c_str()); + + return findTrackNames(cddaDrives[drive].mountPoint); +} + +bool MacOSXAudioCDManager::openCD(const Common::String &drive) { + DriveList drives = detectAllDrives(); + + for (uint32 i = 0; i < drives.size(); i++) { + if (drives[i].fsType != "cddafs") + continue; + + if (drives[i].mountPoint == drive || drives[i].deviceName == drive) { + debug(1, "Using '%s' as the CD drive", drives[i].mountPoint.c_str()); + return findTrackNames(drives[i].mountPoint); + } + } + + return false; +} + +void MacOSXAudioCDManager::close() { + DefaultAudioCDManager::close(); + _trackMap.clear(); +} + +enum { + // Some crazy high number that we'll never actually hit + kMaxDriveCount = 256 +}; + +MacOSXAudioCDManager::DriveList MacOSXAudioCDManager::detectAllDrives() { + // Fetch the lists of drives + struct statfs driveStats[kMaxDriveCount]; + int foundDrives = getfsstat(driveStats, sizeof(driveStats), MNT_WAIT); + if (foundDrives <= 0) + return DriveList(); + + DriveList drives; + for (int i = 0; i < foundDrives; i++) + drives.push_back(Drive(driveStats[i].f_mntonname, driveStats[i].f_mntfromname, driveStats[i].f_fstypename)); + + return drives; +} + +bool MacOSXAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + // Prefer emulation + if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate)) + return true; + + // If we're set to only emulate, or have no CD drive, return here + if (onlyEmulate || !_trackMap.contains(track)) + return false; + + if (!numLoops && !startFrame) + return false; + + // Now load the AIFF track from the name + Common::String fileName = _trackMap[track]; + Common::SeekableReadStream *stream = StdioStream::makeFromPath(fileName.c_str(), false); + + if (!stream) { + warning("Failed to open track '%s'", fileName.c_str()); + return false; + } + + Audio::AudioStream *audioStream = Audio::makeAIFFStream(stream, DisposeAfterUse::YES); + if (!audioStream) { + warning("Track '%s' is not an AIFF track", fileName.c_str()); + return false; + } + + Audio::SeekableAudioStream *seekStream = dynamic_cast<Audio::SeekableAudioStream *>(audioStream); + if (!seekStream) { + warning("Track '%s' is not seekable", fileName.c_str()); + return false; + } + + Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75); + Audio::Timestamp end = duration ? Audio::Timestamp(0, startFrame + duration, 75) : seekStream->getLength(); + + // Fake emulation since we're really playing an AIFF file + _emulating = true; + + _mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, + Audio::makeLoopingAudioStream(seekStream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops), -1, _cd.volume, _cd.balance); + return true; +} + +bool MacOSXAudioCDManager::findTrackNames(const Common::String &drivePath) { + Common::FSNode directory(drivePath); + + if (!directory.exists()) { + warning("Directory '%s' does not exist", drivePath.c_str()); + return false; + } + + if (!directory.isDirectory()) { + warning("'%s' is not a directory", drivePath.c_str()); + return false; + } + + Common::FSList children; + if (!directory.getChildren(children, Common::FSNode::kListFilesOnly)) { + warning("Failed to find children for '%s'", drivePath.c_str()); + return false; + } + + for (uint32 i = 0; i < children.size(); i++) { + if (!children[i].isDirectory()) { + Common::String fileName = children[i].getName(); + + if (fileName.hasSuffix(".aiff") || fileName.hasSuffix(".cdda")) { + uint j = 0; + + // Search for the track ID in the file name. + for (; j < fileName.size() && !Common::isDigit(fileName[j]); j++) + ; + + const char *trackIDString = fileName.c_str() + j; + char *endPtr = nullptr; + long trackID = strtol(trackIDString, &endPtr, 10); + + if (trackIDString != endPtr && trackID > 0 && trackID < UINT_MAX) { + _trackMap[trackID - 1] = drivePath + '/' + fileName; + } else { + warning("Invalid track file name: '%s'", fileName.c_str()); + } + } + } + } + + return true; +} + +AudioCDManager *createMacOSXAudioCDManager() { + return new MacOSXAudioCDManager(); +} + +#endif // MACOSX diff --git a/backends/audiocd/macosx/macosx-audiocd.h b/backends/audiocd/macosx/macosx-audiocd.h new file mode 100644 index 0000000000..42edd5de4f --- /dev/null +++ b/backends/audiocd/macosx/macosx-audiocd.h @@ -0,0 +1,61 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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 BACKENDS_AUDIOCD_MACOSX_H +#define BACKENDS_AUDIOCD_MACOSX_H + +#include "common/scummsys.h" + +#ifdef MACOSX + +class AudioCDManager; + +/** + * Create an audio CD manager for Mac OS X + */ +AudioCDManager *createMacOSXAudioCDManager(); + +#endif + +#endif // diff --git a/backends/audiocd/sdl/sdl-audiocd.cpp b/backends/audiocd/sdl/sdl-audiocd.cpp index ff50c56af3..3558fb5671 100644 --- a/backends/audiocd/sdl/sdl-audiocd.cpp +++ b/backends/audiocd/sdl/sdl-audiocd.cpp @@ -43,10 +43,16 @@ SdlAudioCDManager::SdlAudioCDManager() } SdlAudioCDManager::~SdlAudioCDManager() { - if (_cdrom) { - SDL_CDStop(_cdrom); - SDL_CDClose(_cdrom); - } + close(); +} + +bool SdlAudioCDManager::open() { + close(); + + if (openRealCD()) + return true; + + return DefaultAudioCDManager::open(); } bool SdlAudioCDManager::openCD(int drive) { @@ -67,44 +73,69 @@ bool SdlAudioCDManager::openCD(int drive) { return (_cdrom != NULL); } -void SdlAudioCDManager::stopCD() { +void SdlAudioCDManager::close() { + DefaultAudioCDManager::close(); + + if (_cdrom) { + SDL_CDStop(_cdrom); + SDL_CDClose(_cdrom); + _cdrom = 0; + } +} + +void SdlAudioCDManager::stop() { + DefaultAudioCDManager::stop(); + // Stop CD Audio in 1/10th of a second _cdStopTime = SDL_GetTicks() + 100; _cdNumLoops = 0; } -void SdlAudioCDManager::playCD(int track, int num_loops, int start_frame, int duration) { - if (!num_loops && !start_frame) - return; +bool SdlAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + // Prefer emulation + if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate)) + return true; - if (!_cdrom) - return; + // If we're set to only emulate, or have no CD, return here + if (onlyEmulate || !_cdrom) + return false; + if (!numLoops && !startFrame) + return false; + + // FIXME: Explain this. if (duration > 0) duration += 5; _cdTrack = track; - _cdNumLoops = num_loops; - _cdStartFrame = start_frame; + _cdNumLoops = numLoops; + _cdStartFrame = startFrame; SDL_CDStatus(_cdrom); - if (start_frame == 0 && duration == 0) + if (startFrame == 0 && duration == 0) SDL_CDPlayTracks(_cdrom, track, 0, 1, 0); else - SDL_CDPlayTracks(_cdrom, track, start_frame, 0, duration); + SDL_CDPlayTracks(_cdrom, track, startFrame, 0, duration); _cdDuration = duration; _cdStopTime = 0; _cdEndTime = SDL_GetTicks() + _cdrom->track[track].length * 1000 / CD_FPS; + + return true; } -bool SdlAudioCDManager::pollCD() const { +bool SdlAudioCDManager::isPlaying() const { + if (DefaultAudioCDManager::isPlaying()) + return true; + if (!_cdrom) return false; return (_cdNumLoops != 0 && (SDL_GetTicks() < _cdEndTime || SDL_CDStatus(_cdrom) == CD_PLAYING)); } -void SdlAudioCDManager::updateCD() { +void SdlAudioCDManager::update() { + DefaultAudioCDManager::update(); + if (!_cdrom) return; diff --git a/backends/audiocd/sdl/sdl-audiocd.h b/backends/audiocd/sdl/sdl-audiocd.h index bfad7b6805..91895dac99 100644 --- a/backends/audiocd/sdl/sdl-audiocd.h +++ b/backends/audiocd/sdl/sdl-audiocd.h @@ -37,12 +37,15 @@ public: SdlAudioCDManager(); virtual ~SdlAudioCDManager(); + virtual bool open(); + virtual void close(); + virtual bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); + virtual void stop(); + virtual bool isPlaying() const; + virtual void update(); + protected: virtual bool openCD(int drive); - virtual void updateCD(); - virtual bool pollCD() const; - virtual void playCD(int track, int num_loops, int start_frame, int duration); - virtual void stopCD(); SDL_CD *_cdrom; int _cdTrack, _cdNumLoops, _cdStartFrame, _cdDuration; diff --git a/backends/audiocd/win32/msvc/ntddcdrm.h b/backends/audiocd/win32/msvc/ntddcdrm.h new file mode 100644 index 0000000000..18527e2675 --- /dev/null +++ b/backends/audiocd/win32/msvc/ntddcdrm.h @@ -0,0 +1,362 @@ +/** + * @file ntddcdrm.h + * Copyright 2012, 2013 MinGW.org project + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +/* Created by Casper S. Hornstrup <chorns@users.sourceforge.net> */ +#ifndef __NTDDCDRM_H +#define __NTDDCDRM_H +#if 0 // Added to make MSVC happy. +#pragma GCC system_header +#include <_mingw.h> +#endif + +/* + * CDROM IOCTL interface. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if 0 // Added to make MSVC happy. +#include "ntddk.h" +#include "ntddstor.h" +#endif + +#define IOCTL_CDROM_BASE FILE_DEVICE_CD_ROM + +#define IOCTL_CDROM_CHECK_VERIFY \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0200, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_FIND_NEW_DEVICES \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0206, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_GET_CONTROL \ + CTL_CODE(IOCTL_CDROM_BASE, 0x000D, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_GET_DRIVE_GEOMETRY \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0013, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_GET_LAST_SESSION \ + CTL_CODE(IOCTL_CDROM_BASE, 0x000E, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_GET_VOLUME \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0005, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_PAUSE_AUDIO \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0003, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_PLAY_AUDIO_MSF \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0006, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_RAW_READ \ + CTL_CODE(IOCTL_CDROM_BASE, 0x000F, METHOD_OUT_DIRECT, FILE_READ_ACCESS) + +#define IOCTL_CDROM_READ_Q_CHANNEL \ + CTL_CODE(IOCTL_CDROM_BASE, 0x000B, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_READ_TOC \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0000, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_READ_TOC_EX \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0015, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_RESUME_AUDIO \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0004, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_SEEK_AUDIO_MSF \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0001, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_SET_VOLUME \ + CTL_CODE(IOCTL_CDROM_BASE, 0x000A, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_SIMBAD \ + CTL_CODE(IOCTL_CDROM_BASE, 0x1003, METHOD_BUFFERED, FILE_READ_ACCESS) + +#define IOCTL_CDROM_STOP_AUDIO \ + CTL_CODE(IOCTL_CDROM_BASE, 0x0002, METHOD_BUFFERED, FILE_READ_ACCESS) + + +#define MAXIMUM_NUMBER_TRACKS 100 +#define MAXIMUM_CDROM_SIZE 804 +#define MINIMUM_CDROM_READ_TOC_EX_SIZE 2 + +typedef struct _TRACK_DATA { + UCHAR Reserved; + UCHAR Control : 4; + UCHAR Adr : 4; + UCHAR TrackNumber; + UCHAR Reserved1; + UCHAR Address[4]; +} TRACK_DATA, *PTRACK_DATA; + +/* CDROM_DISK_DATA.DiskData flags */ +#define CDROM_DISK_AUDIO_TRACK 0x00000001 +#define CDROM_DISK_DATA_TRACK 0x00000002 + +typedef struct _CDROM_DISK_DATA { + ULONG DiskData; +} CDROM_DISK_DATA, *PCDROM_DISK_DATA; + +typedef struct _CDROM_PLAY_AUDIO_MSF { + UCHAR StartingM; + UCHAR StartingS; + UCHAR StartingF; + UCHAR EndingM; + UCHAR EndingS; + UCHAR EndingF; +} CDROM_PLAY_AUDIO_MSF, *PCDROM_PLAY_AUDIO_MSF; + +/* CDROM_READ_TOC_EX.Format constants */ +#define CDROM_READ_TOC_EX_FORMAT_TOC 0x00 +#define CDROM_READ_TOC_EX_FORMAT_SESSION 0x01 +#define CDROM_READ_TOC_EX_FORMAT_FULL_TOC 0x02 +#define CDROM_READ_TOC_EX_FORMAT_PMA 0x03 +#define CDROM_READ_TOC_EX_FORMAT_ATIP 0x04 +#define CDROM_READ_TOC_EX_FORMAT_CDTEXT 0x05 + +typedef struct _CDROM_READ_TOC_EX { + UCHAR Format : 4; + UCHAR Reserved1 : 3; + UCHAR Msf : 1; + UCHAR SessionTrack; + UCHAR Reserved2; + UCHAR Reserved3; +} CDROM_READ_TOC_EX, *PCDROM_READ_TOC_EX; + +typedef struct _CDROM_SEEK_AUDIO_MSF { + UCHAR M; + UCHAR S; + UCHAR F; +} CDROM_SEEK_AUDIO_MSF, *PCDROM_SEEK_AUDIO_MSF; + +/* CDROM_SUB_Q_DATA_FORMAT.Format constants */ +#define IOCTL_CDROM_SUB_Q_CHANNEL 0x00 +#define IOCTL_CDROM_CURRENT_POSITION 0x01 +#define IOCTL_CDROM_MEDIA_CATALOG 0x02 +#define IOCTL_CDROM_TRACK_ISRC 0x03 + +typedef struct _CDROM_SUB_Q_DATA_FORMAT { + UCHAR Format; + UCHAR Track; +} CDROM_SUB_Q_DATA_FORMAT, *PCDROM_SUB_Q_DATA_FORMAT; + +typedef struct _CDROM_TOC { + UCHAR Length[2]; + UCHAR FirstTrack; + UCHAR LastTrack; + TRACK_DATA TrackData[MAXIMUM_NUMBER_TRACKS]; +} CDROM_TOC, *PCDROM_TOC; + +#define CDROM_TOC_SIZE sizeof(CDROM_TOC) + +typedef struct _CDROM_TOC_ATIP_DATA_BLOCK { + UCHAR CdrwReferenceSpeed : 3; + UCHAR Reserved3 : 1; + UCHAR WritePower : 3; + UCHAR True1 : 1; + UCHAR Reserved4 : 6; + UCHAR UnrestrictedUse : 1; + UCHAR Reserved5 : 1; + UCHAR A3Valid : 1; + UCHAR A2Valid : 1; + UCHAR A1Valid : 1; + UCHAR Reserved6 : 3; + UCHAR IsCdrw : 1; + UCHAR True2 : 1; + UCHAR Reserved7; + UCHAR LeadInMsf[3]; + UCHAR Reserved8; + UCHAR LeadOutMsf[3]; + UCHAR Reserved9; + UCHAR A1Values[3]; + UCHAR Reserved10; + UCHAR A2Values[3]; + UCHAR Reserved11; + UCHAR A3Values[3]; + UCHAR Reserved12; +} CDROM_TOC_ATIP_DATA_BLOCK, *PCDROM_TOC_ATIP_DATA_BLOCK; + +#if 0 // Added to make MSVC happy. +typedef struct _CDROM_TOC_ATIP_DATA { + UCHAR Length[2]; + UCHAR Reserved1; + UCHAR Reserved2; + CDROM_TOC_ATIP_DATA_BLOCK Descriptors[0]; + CDROM_TOC_ATIP_DATA_BLOCK Descriptors[1]; +} CDROM_TOC_ATIP_DATA, *PCDROM_TOC_ATIP_DATA; +#endif + +/* CDROM_TOC_CD_TEXT_DATA_BLOCK.PackType constants */ +#define CDROM_CD_TEXT_PACK_ALBUM_NAME 0x80 +#define CDROM_CD_TEXT_PACK_PERFORMER 0x81 +#define CDROM_CD_TEXT_PACK_SONGWRITER 0x82 +#define CDROM_CD_TEXT_PACK_COMPOSER 0x83 +#define CDROM_CD_TEXT_PACK_ARRANGER 0x84 +#define CDROM_CD_TEXT_PACK_MESSAGES 0x85 +#define CDROM_CD_TEXT_PACK_DISC_ID 0x86 +#define CDROM_CD_TEXT_PACK_GENRE 0x87 +#define CDROM_CD_TEXT_PACK_TOC_INFO 0x88 +#define CDROM_CD_TEXT_PACK_TOC_INFO2 0x89 +#define CDROM_CD_TEXT_PACK_UPC_EAN 0x8e +#define CDROM_CD_TEXT_PACK_SIZE_INFO 0x8f + +#if 0 // Added to make MSVC happy. +typedef struct _CDROM_TOC_CD_TEXT_DATA_BLOCK { + UCHAR PackType; + UCHAR TrackNumber : 7; + UCHAR ExtensionFlag : 1; + UCHAR SequenceNumber; + UCHAR CharacterPosition : 4; + UCHAR BlockNumber : 3; + UCHAR Unicode : 1; + _ANONYMOUS_UNION union { + UCHAR Text[12]; + WCHAR WText[6]; + } DUMMYUNIONNAME; + UCHAR CRC[2]; +} CDROM_TOC_CD_TEXT_DATA_BLOCK, *PCDROM_TOC_CD_TEXT_DATA_BLOCK; + +typedef struct _CDROM_TOC_CD_TEXT_DATA { + UCHAR Length[2]; + UCHAR Reserved1; + UCHAR Reserved2; + CDROM_TOC_CD_TEXT_DATA_BLOCK Descriptors[0]; +} CDROM_TOC_CD_TEXT_DATA, *PCDROM_TOC_CD_TEXT_DATA; +#endif + +/* CDROM_TOC_FULL_TOC_DATA_BLOCK.Adr constants */ +#define ADR_NO_MODE_INFORMATION 0x0 +#define ADR_ENCODES_CURRENT_POSITION 0x1 +#define ADR_ENCODES_MEDIA_CATALOG 0x2 +#define ADR_ENCODES_ISRC 0x3 + +typedef struct _CDROM_TOC_FULL_TOC_DATA_BLOCK { + UCHAR SessionNumber; + UCHAR Control : 4; + UCHAR Adr : 4; + UCHAR Reserved1; + UCHAR Point; + UCHAR MsfExtra[3]; + UCHAR Zero; + UCHAR Msf[3]; +} CDROM_TOC_FULL_TOC_DATA_BLOCK, *PCDROM_TOC_FULL_TOC_DATA_BLOCK; + +#if 0 // Added to make MSVC happy. +typedef struct _CDROM_TOC_FULL_TOC_DATA { + UCHAR Length[2]; + UCHAR FirstCompleteSession; + UCHAR LastCompleteSession; + CDROM_TOC_FULL_TOC_DATA_BLOCK Descriptors[0]; +} CDROM_TOC_FULL_TOC_DATA, *PCDROM_TOC_FULL_TOC_DATA; + +typedef struct _CDROM_TOC_PMA_DATA { + UCHAR Length[2]; + UCHAR Reserved1; + UCHAR Reserved2; + CDROM_TOC_FULL_TOC_DATA_BLOCK Descriptors[0]; +} CDROM_TOC_PMA_DATA, *PCDROM_TOC_PMA_DATA; +#endif + +/* SUB_Q_HEADER.AudioStatus constants */ +#define AUDIO_STATUS_NOT_SUPPORTED 0x00 +#define AUDIO_STATUS_IN_PROGRESS 0x11 +#define AUDIO_STATUS_PAUSED 0x12 +#define AUDIO_STATUS_PLAY_COMPLETE 0x13 +#define AUDIO_STATUS_PLAY_ERROR 0x14 +#define AUDIO_STATUS_NO_STATUS 0x15 + +typedef struct _SUB_Q_HEADER { + UCHAR Reserved; + UCHAR AudioStatus; + UCHAR DataLength[2]; +} SUB_Q_HEADER, *PSUB_Q_HEADER; + +typedef struct _SUB_Q_MEDIA_CATALOG_NUMBER { + SUB_Q_HEADER Header; + UCHAR FormatCode; + UCHAR Reserved[3]; + UCHAR Reserved1 : 7; + UCHAR Mcval :1; + UCHAR MediaCatalog[15]; +} SUB_Q_MEDIA_CATALOG_NUMBER, *PSUB_Q_MEDIA_CATALOG_NUMBER; + +typedef struct _SUB_Q_TRACK_ISRC { + SUB_Q_HEADER Header; + UCHAR FormatCode; + UCHAR Reserved0; + UCHAR Track; + UCHAR Reserved1; + UCHAR Reserved2 : 7; + UCHAR Tcval : 1; + UCHAR TrackIsrc[15]; +} SUB_Q_TRACK_ISRC, *PSUB_Q_TRACK_ISRC; + +typedef struct _SUB_Q_CURRENT_POSITION { + SUB_Q_HEADER Header; + UCHAR FormatCode; + UCHAR Control : 4; + UCHAR ADR : 4; + UCHAR TrackNumber; + UCHAR IndexNumber; + UCHAR AbsoluteAddress[4]; + UCHAR TrackRelativeAddress[4]; +} SUB_Q_CURRENT_POSITION, *PSUB_Q_CURRENT_POSITION; + +typedef union _SUB_Q_CHANNEL_DATA { + SUB_Q_CURRENT_POSITION CurrentPosition; + SUB_Q_MEDIA_CATALOG_NUMBER MediaCatalog; + SUB_Q_TRACK_ISRC TrackIsrc; +} SUB_Q_CHANNEL_DATA, *PSUB_Q_CHANNEL_DATA; + +/* CDROM_AUDIO_CONTROL.LbaFormat constants */ +#define AUDIO_WITH_PREEMPHASIS 0x1 +#define DIGITAL_COPY_PERMITTED 0x2 +#define AUDIO_DATA_TRACK 0x4 +#define TWO_FOUR_CHANNEL_AUDIO 0x8 + +typedef struct _CDROM_AUDIO_CONTROL { + UCHAR LbaFormat; + USHORT LogicalBlocksPerSecond; +} CDROM_AUDIO_CONTROL, *PCDROM_AUDIO_CONTROL; + +typedef struct _VOLUME_CONTROL { + UCHAR PortVolume[4]; +} VOLUME_CONTROL, *PVOLUME_CONTROL; + +typedef enum _TRACK_MODE_TYPE { + YellowMode2, + XAForm2, + CDDA +} TRACK_MODE_TYPE, *PTRACK_MODE_TYPE; + +typedef struct __RAW_READ_INFO { + LARGE_INTEGER DiskOffset; + ULONG SectorCount; + TRACK_MODE_TYPE TrackMode; +} RAW_READ_INFO, *PRAW_READ_INFO; + +#ifdef __cplusplus +} +#endif + +#endif /* __NTDDCDRM_H */ diff --git a/backends/audiocd/win32/win32-audiocd.cpp b/backends/audiocd/win32/win32-audiocd.cpp new file mode 100644 index 0000000000..b3cde308b9 --- /dev/null +++ b/backends/audiocd/win32/win32-audiocd.cpp @@ -0,0 +1,388 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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. + * + */ + +#ifdef WIN32 + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#undef ARRAYSIZE // winnt.h defines ARRAYSIZE, but we want our own one... + +#include "backends/audiocd/win32/win32-audiocd.h" + +#include "audio/audiostream.h" +#include "backends/audiocd/audiocd-stream.h" +#include "backends/audiocd/default/default-audiocd.h" +#include "common/array.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/mutex.h" +#include "common/queue.h" +#include "common/str.h" +#include "common/timer.h" + +#include <winioctl.h> +#if _MSC_VER < 1900 +// WORKAROUND: Older versions of MSVC might not supply DDK headers by default. +// Visual Studio 2015 contains the required headers. We use a compatability +// header from MinGW's w32api for all older versions. +// TODO: Limit this to the Visual Studio versions which actually require this. +#include "msvc/ntddcdrm.h" +#elif defined(__MINGW32__) && !defined(__MINGW64__) +// Classic MinGW uses non standard paths for DDK headers. +#include <ddk/ntddcdrm.h> +#else +#include <ntddcdrm.h> +#endif + +class Win32AudioCDStream : public AudioCDStream { +public: + Win32AudioCDStream(HANDLE handle, const TRACK_DATA &startEntry, const TRACK_DATA &endEntry); + ~Win32AudioCDStream(); + +protected: + uint getStartFrame() const; + uint getEndFrame() const; + bool readFrame(int frame, int16 *buffer); + +private: + HANDLE _driveHandle; + const TRACK_DATA &_startEntry, &_endEntry; + + enum { + // The CD-ROM pre-gap is 2s + kPreGapFrames = kFramesPerSecond * 2 + }; + + static int getFrameCount(const TRACK_DATA &data) { + int time = data.Address[1]; + time *= kSecondsPerMinute; + time += data.Address[2]; + time *= kFramesPerSecond; + time += data.Address[3]; + return time; + } +}; + +Win32AudioCDStream::Win32AudioCDStream(HANDLE handle, const TRACK_DATA &startEntry, const TRACK_DATA &endEntry) : + _driveHandle(handle), _startEntry(startEntry), _endEntry(endEntry) { + // We fill the buffer here already to prevent any out of sync issues due + // to the CD not yet having spun up. + startTimer(true); +} + +Win32AudioCDStream::~Win32AudioCDStream() { + stopTimer(); +} + +uint Win32AudioCDStream::getStartFrame() const { + return getFrameCount(_startEntry); +} + +uint Win32AudioCDStream::getEndFrame() const { + return getFrameCount(_endEntry); +} + +bool Win32AudioCDStream::readFrame(int frame, int16 *buffer) { + // Request to read that frame + RAW_READ_INFO readAudio; + memset(&readAudio, 0, sizeof(readAudio)); + readAudio.DiskOffset.QuadPart = (frame - kPreGapFrames) * 2048; + readAudio.SectorCount = 1; + readAudio.TrackMode = CDDA; + + DWORD bytesReturned; + return DeviceIoControl( + _driveHandle, + IOCTL_CDROM_RAW_READ, + &readAudio, + sizeof(readAudio), + buffer, + kBytesPerFrame, + &bytesReturned, + NULL); +} + + +class Win32AudioCDManager : public DefaultAudioCDManager { +public: + Win32AudioCDManager(); + ~Win32AudioCDManager(); + + bool open(); + void close(); + bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); + +protected: + bool openCD(int drive); + bool openCD(const Common::String &drive); + +private: + bool loadTOC(); + + typedef Common::Array<char> DriveList; + DriveList detectDrives(); + bool tryAddDrive(char drive, DriveList &drives); + + HANDLE _driveHandle; + int _firstTrack, _lastTrack; + Common::Array<TRACK_DATA> _tocEntries; +}; + +Win32AudioCDManager::Win32AudioCDManager() { + _driveHandle = INVALID_HANDLE_VALUE; + _firstTrack = _lastTrack = 0; +} + +Win32AudioCDManager::~Win32AudioCDManager() { + close(); +} + +bool Win32AudioCDManager::open() { + close(); + + if (openRealCD()) + return true; + + return DefaultAudioCDManager::open(); +} + +bool Win32AudioCDManager::openCD(int drive) { + // Fetch the drive list + DriveList drives = detectDrives(); + if (drive >= (int)drives.size()) + return false; + + debug(1, "Opening CD drive %c:\\", drives[drive]); + + // Construct the drive path and try to open it + Common::String drivePath = Common::String::format("\\\\.\\%c:", drives[drive]); + _driveHandle = CreateFileA(drivePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + if (_driveHandle == INVALID_HANDLE_VALUE) { + warning("Failed to open drive %c:\\, error %d", drives[drive], (int)GetLastError()); + return false; + } + + if (!loadTOC()) { + close(); + return false; + } + + return true; +} + +bool Win32AudioCDManager::openCD(const Common::String &drive) { + // Just some bounds checking + if (drive.empty() || drive.size() > 3) + return false; + + if (!Common::isAlpha(drive[0]) || drive[1] != ':') + return false; + + if (drive[2] != 0 && drive[2] != '\\') + return false; + + DriveList drives; + if (!tryAddDrive(toupper(drive[0]), drives)) + return false; + + // Construct the drive path and try to open it + Common::String drivePath = Common::String::format("\\\\.\\%c:", drives[0]); + _driveHandle = CreateFileA(drivePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + if (_driveHandle == INVALID_HANDLE_VALUE) { + warning("Failed to open drive %c:\\, error %d", drives[0], (int)GetLastError()); + return false; + } + + if (!loadTOC()) { + close(); + return false; + } + + return true; +} + +void Win32AudioCDManager::close() { + DefaultAudioCDManager::close(); + + if (_driveHandle != INVALID_HANDLE_VALUE) { + CloseHandle(_driveHandle); + _driveHandle = INVALID_HANDLE_VALUE; + } + + _firstTrack = _lastTrack = 0; + _tocEntries.clear(); +} + +bool Win32AudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + // Prefer emulation + if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate)) + return true; + + // If we're set to only emulate, or have no CD drive, return here + if (onlyEmulate || _driveHandle == INVALID_HANDLE_VALUE) + return false; + + // HACK: For now, just assume that track number is right + // That only works because ScummVM uses the wrong track number anyway + + if (track >= (int)_tocEntries.size() - 1) { + warning("No such track %d", track); + return false; + } + + // Bail if the track isn't an audio track + if ((_tocEntries[track].Control & 0x04) != 0) { + warning("Track %d is not audio", track); + return false; + } + + // Create the AudioStream and play it + debug(1, "Playing CD track %d", track); + + Audio::SeekableAudioStream *audioStream = new Win32AudioCDStream(_driveHandle, _tocEntries[track], _tocEntries[track + 1]); + + Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75); + Audio::Timestamp end = (duration == 0) ? audioStream->getLength() : Audio::Timestamp(0, startFrame + duration, 75); + + // Fake emulation since we're really playing an AudioStream + _emulating = true; + + _mixer->playStream( + Audio::Mixer::kMusicSoundType, + &_handle, + Audio::makeLoopingAudioStream(audioStream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops), + -1, + _cd.volume, + _cd.balance, + DisposeAfterUse::YES, + true); + return true; +} + +bool Win32AudioCDManager::loadTOC() { + CDROM_READ_TOC_EX tocRequest; + memset(&tocRequest, 0, sizeof(tocRequest)); + tocRequest.Format = CDROM_READ_TOC_EX_FORMAT_TOC; + tocRequest.Msf = 1; + tocRequest.SessionTrack = 0; + + DWORD bytesReturned; + CDROM_TOC tocData; + bool result = DeviceIoControl( + _driveHandle, + IOCTL_CDROM_READ_TOC_EX, + &tocRequest, + sizeof(tocRequest), + &tocData, + sizeof(tocData), + &bytesReturned, + NULL); + if (!result) { + debug("Failed to query the CD TOC: %d", (int)GetLastError()); + return false; + } + + _firstTrack = tocData.FirstTrack; + _lastTrack = tocData.LastTrack; +#if 0 + debug("First Track: %d", tocData.FirstTrack); + debug("Last Track: %d", tocData.LastTrack); +#endif + + for (uint32 i = 0; i < (bytesReturned - 4) / sizeof(TRACK_DATA); i++) + _tocEntries.push_back(tocData.TrackData[i]); + +#if 0 + for (uint32 i = 0; i < _tocEntries.size(); i++) { + const TRACK_DATA &entry = _tocEntries[i]; + debug("Entry:"); + debug("\tTrack: %d", entry.TrackNumber); + debug("\tAdr: %d", entry.Adr); + debug("\tCtrl: %d", entry.Control); + debug("\tMSF: %d:%d:%d\n", entry.Address[1], entry.Address[2], entry.Address[3]); + } +#endif + + return true; +} + +Win32AudioCDManager::DriveList Win32AudioCDManager::detectDrives() { + DriveList drives; + + // Try to get the game path's drive + char gameDrive = 0; + if (ConfMan.hasKey("path")) { + Common::String gamePath = ConfMan.get("path"); + char fullPath[MAX_PATH]; + DWORD result = GetFullPathNameA(gamePath.c_str(), sizeof(fullPath), fullPath, 0); + + if (result > 0 && result < sizeof(fullPath) && Common::isAlpha(fullPath[0]) && fullPath[1] == ':' && tryAddDrive(toupper(fullPath[0]), drives)) + gameDrive = drives[0]; + } + + // Try adding the rest of the drives + for (char drive = 'A'; drive <= 'Z'; drive++) + if (drive != gameDrive) + tryAddDrive(drive, drives); + + return drives; +} + +bool Win32AudioCDManager::tryAddDrive(char drive, DriveList &drives) { + Common::String drivePath = Common::String::format("%c:\\", drive); + + // Ensure it's an actual CD drive + if (GetDriveTypeA(drivePath.c_str()) != DRIVE_CDROM) + return false; + + debug(2, "Detected drive %c:\\ as a CD drive", drive); + drives.push_back(drive); + return true; +} + +AudioCDManager *createWin32AudioCDManager() { + return new Win32AudioCDManager(); +} + +#endif // WIN32 diff --git a/backends/audiocd/win32/win32-audiocd.h b/backends/audiocd/win32/win32-audiocd.h new file mode 100644 index 0000000000..0c103641ef --- /dev/null +++ b/backends/audiocd/win32/win32-audiocd.h @@ -0,0 +1,60 @@ +/* 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. + * + * Original license header: + * + * Cabal - Legacy Game Implementations + * + * Cabal 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 BACKENDS_AUDIOCD_WIN32_H +#define BACKENDS_AUDIOCD_WIN32_H + +#ifdef WIN32 + +class AudioCDManager; + +/** + * Create an AudioCDManager using the Win32 API + */ +AudioCDManager *createWin32AudioCDManager(); + +#endif + +#endif + diff --git a/backends/base-backend.h b/backends/base-backend.h index 598f682b32..6d074a4c3e 100644 --- a/backends/base-backend.h +++ b/backends/base-backend.h @@ -33,6 +33,7 @@ public: virtual void initBackend(); virtual void displayMessageOnOSD(const char *msg); + virtual void displayActivityIconOnOSD(const Graphics::Surface *icon) {} virtual void fillScreen(uint32 col); }; diff --git a/backends/cloud/box/boxlistdirectorybyidrequest.cpp b/backends/cloud/box/boxlistdirectorybyidrequest.cpp new file mode 100644 index 0000000000..0258e3e1e9 --- /dev/null +++ b/backends/cloud/box/boxlistdirectorybyidrequest.cpp @@ -0,0 +1,195 @@ +/* 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 "backends/cloud/box/boxlistdirectorybyidrequest.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Box { + +#define BOX_LIST_DIRECTORY_LIMIT 1000 +#define BOX_FOLDERS_API_LINK "https://api.box.com/2.0/folders/%s/items?offset=%u&limit=%u&fields=%s" + +BoxListDirectoryByIdRequest::BoxListDirectoryByIdRequest(BoxStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedId(id), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +BoxListDirectoryByIdRequest::~BoxListDirectoryByIdRequest() { + _ignoreCallback = true; + if (_workingRequest) _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void BoxListDirectoryByIdRequest::start() { + _ignoreCallback = true; + if (_workingRequest) _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + makeRequest(0); +} + +void BoxListDirectoryByIdRequest::makeRequest(uint32 offset) { + Common::String url = Common::String::format( + BOX_FOLDERS_API_LINK, + _requestedId.c_str(), + offset, + BOX_LIST_DIRECTORY_LIMIT, + "id,type,name,size,modified_at" + ); + + Networking::JsonCallback callback = new Common::Callback<BoxListDirectoryByIdRequest, Networking::JsonResponse>(this, &BoxListDirectoryByIdRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<BoxListDirectoryByIdRequest, Networking::ErrorResponse>(this, &BoxListDirectoryByIdRequest::errorCallback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void BoxListDirectoryByIdRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) { + delete response.value; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject responseObject = json->asObject(); + //debug(9, "%s", json->stringify(true).c_str()); + + //TODO: handle error messages passed as JSON + /* + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + warning("Box returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + */ + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (responseObject.contains("entries")) { + if (!responseObject.getVal("entries")->isArray()) { + error.response = Common::String::format( + "\"entries\" found, but that's not an array!\n%s", + responseObject.getVal("entries")->stringify(true).c_str() + ); + finishError(error); + delete json; + return; + } + + Common::JSONArray items = responseObject.getVal("entries")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "BoxListDirectoryByIdRequest")) continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsString(item, "id", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "name", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "type", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "modified_at", "BoxListDirectoryByIdRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsStringOrIntegerNumber(item, "size", "BoxListDirectoryByIdRequest")) continue; + + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("type")->asString() == "folder"); + uint32 size; + if (item.getVal("size")->isString()) { + size = item.getVal("size")->asString().asUint64(); + } else { + size = item.getVal("size")->asIntegerNumber(); + } + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("modified_at")->asString()); + + //as we list directory by id, we can't determine full path for the file, so we leave it empty + _files.push_back(StorageFile(id, "", name, size, timestamp, isDirectory)); + } + } + + uint32 received = 0; + uint32 totalCount = 0; + if (responseObject.contains("total_count") && responseObject.getVal("total_count")->isIntegerNumber()) + totalCount = responseObject.getVal("total_count")->asIntegerNumber(); + if (responseObject.contains("offset") && responseObject.getVal("offset")->isIntegerNumber()) + received = responseObject.getVal("offset")->asIntegerNumber(); + if (responseObject.contains("limit") && responseObject.getVal("limit")->isIntegerNumber()) + received += responseObject.getVal("limit")->asIntegerNumber(); + bool hasMore = (received < totalCount); + + if (hasMore) makeRequest(received); + else finishListing(_files); + + delete json; +} + +void BoxListDirectoryByIdRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + if (error.request) _date = error.request->date(); + finishError(error); +} + +void BoxListDirectoryByIdRequest::handle() {} + +void BoxListDirectoryByIdRequest::restart() { start(); } + +Common::String BoxListDirectoryByIdRequest::date() const { return _date; } + +void BoxListDirectoryByIdRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxlistdirectorybyidrequest.h b/backends/cloud/box/boxlistdirectorybyidrequest.h new file mode 100644 index 0000000000..96609211f1 --- /dev/null +++ b/backends/cloud/box/boxlistdirectorybyidrequest.h @@ -0,0 +1,63 @@ +/* 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 BACKENDS_CLOUD_BOX_BOXLISTDIRECTORYBYIDREQUEST_H +#define BACKENDS_CLOUD_BOX_BOXLISTDIRECTORYBYIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Box { + +class BoxStorage; + +class BoxListDirectoryByIdRequest: public Networking::Request { + Common::String _requestedId; + BoxStorage *_storage; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void makeRequest(uint32 offset); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + BoxListDirectoryByIdRequest(BoxStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb); + virtual ~BoxListDirectoryByIdRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxstorage.cpp b/backends/cloud/box/boxstorage.cpp new file mode 100644 index 0000000000..598ca55c15 --- /dev/null +++ b/backends/cloud/box/boxstorage.cpp @@ -0,0 +1,346 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxlistdirectorybyidrequest.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/box/boxuploadrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace Box { + +#define BOX_OAUTH2_TOKEN "https://api.box.com/oauth2/token" +#define BOX_API_FOLDERS "https://api.box.com/2.0/folders" +#define BOX_API_FILES_CONTENT "https://api.box.com/2.0/files/%s/content" +#define BOX_API_USERS_ME "https://api.box.com/2.0/users/me" + +char *BoxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *BoxStorage::SECRET = nullptr; + +void BoxStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_BOX_KEY; + SECRET = RELEASE_BOX_SECRET; +#else + Common::String k = ConfMan.get("BOX_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("BOX_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +BoxStorage::BoxStorage(Common::String accessToken, Common::String refreshToken): + _token(accessToken), _refreshToken(refreshToken) {} + +BoxStorage::BoxStorage(Common::String code) { + getAccessToken( + new Common::Callback<BoxStorage, BoolResponse>(this, &BoxStorage::codeFlowComplete), + new Common::Callback<BoxStorage, Networking::ErrorResponse>(this, &BoxStorage::codeFlowFailed), + code + ); +} + +BoxStorage::~BoxStorage() {} + +void BoxStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("BoxStorage: no refresh token available to get new access token."); + if (callback) (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, BOX_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("grant_type=authorization_code"); + request->addPostField("code=" + code); + } else { + request->addPostField("grant_type=refresh_token"); + request->addPostField("refresh_token=" + _refreshToken); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + /* + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + */ + addRequest(request); +} + +void BoxStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BoxStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "BoxStorage")) { + warning("BoxStorage: bad response, no token passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void BoxStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("BoxStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + CloudMan.replaceStorage(this, kStorageBoxId); + ConfMan.flushToDisk(); +} + +void BoxStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "BoxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void BoxStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String BoxStorage::name() const { + return "Box"; +} + +void BoxStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 0; + + // can check that "type": "user" + // there is also "max_upload_size", "phone" and "avatar_url" + + if (Networking::CurlJsonRequest::jsonContainsString(info, "id", "BoxStorage::infoInnerCallback")) + uid = info.getVal("id")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsString(info, "name", "BoxStorage::infoInnerCallback")) + name = info.getVal("name")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsString(info, "login", "BoxStorage::infoInnerCallback")) + email = info.getVal("login")->asString(); + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "space_amount", "BoxStorage::infoInnerCallback")) + quotaAllocated = info.getVal("space_amount")->asIntegerNumber(); + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "space_used", "BoxStorage::infoInnerCallback")) + quotaUsed = info.getVal("space_used")->asIntegerNumber(); + + Common::String username = email; + if (username == "") username = name; + if (username == "") username = uid; + CloudMan.setStorageUsername(kStorageBoxId, username); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +Networking::Request *BoxStorage::listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = getPrintFilesCallback(); + return addRequest(new BoxListDirectoryByIdRequest(this, id, callback, errorCallback)); +} + +void BoxStorage::createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("BoxStorage::createDirectoryInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (outerCallback) { + if (Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage::createDirectoryInnerCallback")) { + Common::JSONObject info = json->asObject(); + (*outerCallback)(BoolResponse(nullptr, info.contains("id"))); + } else { + (*outerCallback)(BoolResponse(nullptr, false)); + } + delete outerCallback; + } + + delete json; +} + +Networking::Request *BoxStorage::createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + + Common::String url = BOX_API_FOLDERS; + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::createDirectoryInnerCallback, callback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject parentObject; + parentObject.setVal("id", new Common::JSONValue(parentId)); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("parent", new Common::JSONValue(parentObject)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + return addRequest(request); +} + +Networking::Request *BoxStorage::upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new BoxUploadRequest(this, remotePath, localPath, callback, errorCallback)); +} + +Networking::Request *BoxStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + warning("BoxStorage::upload(ReadStream) not implemented"); + if (errorCallback) + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "BoxStorage::upload(ReadStream) not implemented", -1)); + delete callback; + delete errorCallback; + return nullptr; +} + +bool BoxStorage::uploadStreamSupported() { + return false; +} + +Networking::Request *BoxStorage::streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + if (callback) { + Common::String url = Common::String::format(BOX_API_FILES_CONTENT, id.c_str()); + Common::String header = "Authorization: Bearer " + _token; + curl_slist *headersList = curl_slist_append(nullptr, header.c_str()); + Networking::NetworkReadStream *stream = new Networking::NetworkReadStream(url.c_str(), headersList, ""); + (*callback)(Networking::NetworkReadStreamResponse(nullptr, stream)); + } + delete callback; + delete errorCallback; + return nullptr; +} + +Networking::Request *BoxStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, StorageInfoResponse, Networking::JsonResponse>(this, &BoxStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(this, innerCallback, errorCallback, BOX_API_USERS_ME); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Common::String BoxStorage::savesDirectoryPath() { return "scummvm/saves/"; } + +BoxStorage *BoxStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("BoxStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("BoxStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new BoxStorage(accessToken, refreshToken); +} + +Common::String BoxStorage::getRootDirectoryId() { + return "0"; +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxstorage.h b/backends/cloud/box/boxstorage.h new file mode 100644 index 0000000000..27bf60e39b --- /dev/null +++ b/backends/cloud/box/boxstorage.h @@ -0,0 +1,116 @@ +/* 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 BACKENDS_CLOUD_BOX_BOXSTORAGE_H +#define BACKENDS_CLOUD_BOX_BOXSTORAGE_H + +#include "backends/cloud/id/idstorage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Box { + +class BoxStorage: public Id::IdStorage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + BoxStorage(Common::String token, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + BoxStorage(Common::String code); + virtual ~BoxStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns whether Storage supports upload(ReadStream). */ + virtual bool uploadStreamSupported(); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return BoxStorage for those. + * @return pointer to the newly created BoxStorage or 0 if some problem occured. + */ + static BoxStorage *loadFromConfig(Common::String keyPrefix); + + virtual Common::String getRootDirectoryId(); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxtokenrefresher.cpp b/backends/cloud/box/boxtokenrefresher.cpp new file mode 100644 index 0000000000..b5008ef890 --- /dev/null +++ b/backends/cloud/box/boxtokenrefresher.cpp @@ -0,0 +1,138 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace Box { + +BoxTokenRefresher::BoxTokenRefresher(BoxStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +BoxTokenRefresher::~BoxTokenRefresher() {} + +void BoxTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("BoxTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: Bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void BoxTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "BoxTokenRefresher")) { + Common::JSONObject result = json->asObject(); + if (result.contains("type") && result.getVal("type")->isString() && result.getVal("type")->asString() == "error") { + //new token needed => request token & then retry original request + long httpCode = -1; + if (_stream) { + httpCode = _stream->httpResponseCode(); + debug(9, "BoxTokenRefresher: code %ld", httpCode); + } + + bool irrecoverable = true; + + Common::String code, message; + if (jsonContainsString(result, "code", "BoxTokenRefresher")) { + code = result.getVal("code")->asString(); + debug(9, "BoxTokenRefresher: code = %s", code.c_str()); + } + + if (jsonContainsString(result, "message", "BoxTokenRefresher")) { + message = result.getVal("message")->asString(); + debug(9, "BoxTokenRefresher: message = %s", message.c_str()); + } + + //TODO: decide when token refreshment will help + //for now refreshment is used only when HTTP 401 is passed in finishError() + //if (code == "unauthenticated") irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void BoxTokenRefresher::finishError(Networking::ErrorResponse error) { + if (error.httpResponseCode == 401) { // invalid_token + pause(); + _parentStorage->getAccessToken(new Common::Callback<BoxTokenRefresher, Storage::BoolResponse>(this, &BoxTokenRefresher::tokenRefreshed)); + return; + } + + // there are also 400 == invalid_request and 403 == insufficient_scope + // but TokenRefresher is there to refresh token when it's invalid only + + Request::finishError(error); +} + +void BoxTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void BoxTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxtokenrefresher.h b/backends/cloud/box/boxtokenrefresher.h new file mode 100644 index 0000000000..2111904a5c --- /dev/null +++ b/backends/cloud/box/boxtokenrefresher.h @@ -0,0 +1,53 @@ +/* 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 BACKENDS_CLOUD_BOX_BOXTOKENREFRESHER_H +#define BACKENDS_CLOUD_BOX_BOXTOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Box { + +class BoxStorage; + +class BoxTokenRefresher: public Networking::CurlJsonRequest { + BoxStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); + virtual void finishError(Networking::ErrorResponse error); +public: + BoxTokenRefresher(BoxStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~BoxTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/box/boxuploadrequest.cpp b/backends/cloud/box/boxuploadrequest.cpp new file mode 100644 index 0000000000..3294eea50d --- /dev/null +++ b/backends/cloud/box/boxuploadrequest.cpp @@ -0,0 +1,232 @@ +/* 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 "backends/cloud/box/boxuploadrequest.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/box/boxtokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Box { + +#define BOX_API_FILES "https://upload.box.com/api/2.0/files" + +BoxUploadRequest::BoxUploadRequest(BoxStorage *storage, Common::String path, Common::String localPath, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _localPath(localPath), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +BoxUploadRequest::~BoxUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _uploadCallback; +} + +void BoxUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _resolvedId = ""; //used to update file contents + _parentId = ""; //used to create file within parent directory + _ignoreCallback = false; + + resolveId(); +} + +void BoxUploadRequest::resolveId() { + //check whether such file already exists + Storage::UploadCallback innerCallback = new Common::Callback<BoxUploadRequest, Storage::UploadResponse>(this, &BoxUploadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<BoxUploadRequest, Networking::ErrorResponse>(this, &BoxUploadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_savePath, innerCallback, innerErrorCallback); +} + +void BoxUploadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + _resolvedId = response.value.id(); + upload(); +} + +void BoxUploadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + + //not resolved => error or no such file + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + _parentId = parentId; + upload(); + return; + } + + finishError(error); +} + +void BoxUploadRequest::upload() { + Common::String name = _savePath; + for (uint32 i = name.size(); i > 0; --i) { + if (name[i - 1] == '/' || name[i - 1] == '\\') { + name.erase(0, i); + break; + } + } + + Common::String url = BOX_API_FILES; + if (_resolvedId != "") + url += "/" + _resolvedId; + url += "/content"; + Networking::JsonCallback callback = new Common::Callback<BoxUploadRequest, Networking::JsonResponse>(this, &BoxUploadRequest::uploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<BoxUploadRequest, Networking::ErrorResponse>(this, &BoxUploadRequest::notUploadedCallback); + Networking::CurlJsonRequest *request = new BoxTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + + Common::JSONObject jsonRequestParameters; + if (_resolvedId == "") { + Common::JSONObject parentObject; + parentObject.setVal("id", new Common::JSONValue(_parentId)); + jsonRequestParameters.setVal("parent", new Common::JSONValue(parentObject)); + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + } + + Common::JSONValue value(jsonRequestParameters); + request->addFormField("attributes", Common::JSON::stringify(&value)); + request->addFormFile("file", _localPath); + + _workingRequest = ConnMan.addRequest(request); +} + +void BoxUploadRequest::uploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + error.httpResponseCode = code; + } + } + + if (error.httpResponseCode != 200 && error.httpResponseCode != 201) + warning("BoxUploadRequest: looks like an error (bad HTTP code)"); + + //check JSON and show warnings if it's malformed + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject object = json->asObject(); + if (Networking::CurlJsonRequest::jsonContainsArray(object, "entries", "BoxUploadRequest")) { + Common::JSONArray entries = object.getVal("entries")->asArray(); + if (entries.size() == 0) { + warning("BoxUploadRequest: 'entries' found, but it's empty"); + } else if (!Networking::CurlJsonRequest::jsonIsObject(entries[0], "BoxUploadRequest")) { + warning("BoxUploadRequest: 'entries' first item is not an object"); + } else { + Common::JSONObject item = entries[0]->asObject(); + + if (Networking::CurlJsonRequest::jsonContainsString(item, "id", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "name", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "type", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(item, "modified_at", "BoxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsStringOrIntegerNumber(item, "size", "BoxUploadRequest")) { + + //finished + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("type")->asString() == "folder"); + uint32 size; + if (item.getVal("size")->isString()) { + size = item.getVal("size")->asString().asUint64(); + } else { + size = item.getVal("size")->asIntegerNumber(); + } + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("modified_at")->asString()); + + finishUpload(StorageFile(id, _savePath, name, size, timestamp, isDirectory)); + delete json; + return; + } + } + } + + //TODO: check errors + /* + if (object.contains("error")) { + warning("Box returned error: %s", json->stringify(true).c_str()); + delete json; + error.response = json->stringify(true); + finishError(error); + return; + } + */ + + warning("BoxUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + + delete json; +} + +void BoxUploadRequest::notUploadedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + finishError(error); +} + +void BoxUploadRequest::handle() {} + +void BoxUploadRequest::restart() { start(); } + +void BoxUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Box +} // End of namespace Cloud diff --git a/backends/cloud/box/boxuploadrequest.h b/backends/cloud/box/boxuploadrequest.h new file mode 100644 index 0000000000..96f37fb7b5 --- /dev/null +++ b/backends/cloud/box/boxuploadrequest.h @@ -0,0 +1,63 @@ +/* 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 BACKENDS_CLOUD_BOX_BOXUPLOADREQUEST_H +#define BACKENDS_CLOUD_BOX_BOXUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Box { +class BoxStorage; + +class BoxUploadRequest: public Networking::Request { + BoxStorage *_storage; + Common::String _savePath, _localPath; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _resolvedId, _parentId; + + void start(); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void upload(); + void uploadedCallback(Networking::JsonResponse response); + void notUploadedCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + BoxUploadRequest(BoxStorage *storage, Common::String path, Common::String localPath, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~BoxUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Box +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/cloudicon.cpp b/backends/cloud/cloudicon.cpp new file mode 100644 index 0000000000..2adc7460d2 --- /dev/null +++ b/backends/cloud/cloudicon.cpp @@ -0,0 +1,185 @@ +/* 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 "backends/cloud/cloudicon.h" +#include "common/memstream.h" +#include "common/system.h" +#include "image/png.h" + +namespace Cloud { + +const float CloudIcon::ALPHA_SPEED = 0.0005; +const float CloudIcon::ALPHA_MAX = 1; +const float CloudIcon::ALPHA_MIN = 0.6; + +CloudIcon::CloudIcon() { + initIcons(); + hide(); + _lastUpdateTime = g_system->getMillis(); +} + +CloudIcon::~CloudIcon() { + _icon.free(); + _disabledIcon.free(); + _alphaIcon.free(); +} + +void CloudIcon::show(CloudIcon::Type icon, int duration) { + if (_type == icon) { + return; // Nothing to do + } + + if (icon != kNone) { + _state = kShown; + _type = icon; + + if (duration) { + _hideTime = g_system->getMillis() + duration; + } else { + _hideTime = 0; + } + } else { + _state = kGoingToHide; + } +} + +void CloudIcon::hide() { + _state = kHidden; + _type = kNone; + _hideTime = 0; + _currentAlpha = 0; + _alphaRising = true; +} + +CloudIcon::Type CloudIcon::getShownType() const { + return _type; +} + +bool CloudIcon::needsUpdate() const { + uint32 delaySinceLastUpdate = g_system->getMillis() - _lastUpdateTime; + return delaySinceLastUpdate >= UPDATE_DELAY_MIN_MILLIS; +} + +void CloudIcon::update() { + uint32 currentTime = g_system->getMillis(); + uint32 delaySinceLastUpdate = currentTime - _lastUpdateTime; + _lastUpdateTime = currentTime; + + switch (_state) { + case kHidden: + return; // Nothing to do + case kShown: + if (_alphaRising) { + if (_currentAlpha < ALPHA_MIN) + _currentAlpha += 5 * ALPHA_SPEED * delaySinceLastUpdate; + else + _currentAlpha += ALPHA_SPEED * delaySinceLastUpdate; + if (_currentAlpha > ALPHA_MAX) { + _currentAlpha = ALPHA_MAX; + _alphaRising = false; + } + } else { + _currentAlpha -= ALPHA_SPEED * delaySinceLastUpdate; + if (_currentAlpha < ALPHA_MIN) { + _currentAlpha = ALPHA_MIN; + _alphaRising = true; + } + } + + if (_hideTime != 0 && _hideTime <= currentTime) { + _hideTime = 0; + _state = kGoingToHide; + } + break; + case kGoingToHide: + _currentAlpha -= 5 * ALPHA_SPEED * delaySinceLastUpdate; + if (_currentAlpha <= 0) { + hide(); + } + break; + } + + if (!_icon.getPixels() || !_disabledIcon.getPixels()) { + // Loading the icons failed. Don't try to draw them. + return; + } + + if (_state != kHidden) { + makeAlphaIcon((_type == kDisabled ? _disabledIcon : _icon), _currentAlpha); + g_system->displayActivityIconOnOSD(&_alphaIcon); + } else { + g_system->displayActivityIconOnOSD(nullptr); + } +} + +#include "backends/cloud/cloudicon_data.h" +#include "backends/cloud/cloudicon_disabled_data.h" + +void CloudIcon::initIcons() { + loadIcon(_icon, cloudicon_data, ARRAYSIZE(cloudicon_data)); + loadIcon(_disabledIcon, cloudicon_disabled_data, ARRAYSIZE(cloudicon_disabled_data)); +} + +void CloudIcon::loadIcon(Graphics::Surface &icon, byte *data, uint32 size) { + Image::PNGDecoder decoder; + Common::MemoryReadStream stream(data, size); + if (!decoder.loadStream(stream)) { + warning("CloudIcon::loadIcon: error decoding PNG"); + return; + } + + const Graphics::Surface *s = decoder.getSurface(); + icon.copyFrom(*s); +} + +void CloudIcon::makeAlphaIcon(const Graphics::Surface &icon, float alpha) { + _alphaIcon.copyFrom(icon); + + byte *pixels = (byte *)_alphaIcon.getPixels(); + for (int y = 0; y < _alphaIcon.h; y++) { + byte *row = pixels + y * _alphaIcon.pitch; + for (int x = 0; x < _alphaIcon.w; x++) { + uint32 srcColor; + if (_alphaIcon.format.bytesPerPixel == 2) + srcColor = READ_UINT16(row); + else if (_alphaIcon.format.bytesPerPixel == 3) + srcColor = READ_UINT24(row); + else + srcColor = READ_UINT32(row); + + // Update color's alpha + byte r, g, b, a; + _alphaIcon.format.colorToARGB(srcColor, a, r, g, b); + a = (byte)(a * alpha); + uint32 color = _alphaIcon.format.ARGBToColor(a, r, g, b); + + if (_alphaIcon.format.bytesPerPixel == 2) + *((uint16 *)row) = color; + else + *((uint32 *)row) = color; + + row += _alphaIcon.format.bytesPerPixel; + } + } +} + +} // End of namespace Cloud diff --git a/backends/cloud/cloudicon.h b/backends/cloud/cloudicon.h new file mode 100644 index 0000000000..2b2f9cfd57 --- /dev/null +++ b/backends/cloud/cloudicon.h @@ -0,0 +1,90 @@ +/* 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 BACKENDS_NETWORKING_CURL_CLOUDICON_H +#define BACKENDS_NETWORKING_CURL_CLOUDICON_H + +#include "graphics/surface.h" + +namespace Cloud { + +class CloudIcon { +public: + CloudIcon(); + ~CloudIcon(); + + /** + * The type of cloud icon to show + */ + enum Type { + kNone, /** Hide the currently shown icon if any */ + kSyncing, /** Cloud syncing icon */ + kDisabled /** Cloud syncing not available icon */ + }; + + /** + * Select the icon to show on the OSD + * + * @param icon Icon type to show. Use kNone to hide the current icon if any. + * @param duration Duration in milliseconds the icon stays visible on screen. 0 means the icon stays indefinitely. + */ + void show(Type icon, int duration = 0); + + /** The currently visible icon. kNone means no icon is shown. */ + Type getShownType() const; + + /** Returns true if the icon state needs to be checked for changes */ + bool needsUpdate() const; + + /** Update the icon visible on the OSD */ + void update(); + +private: + static const float ALPHA_SPEED, ALPHA_MAX, ALPHA_MIN; + static const int UPDATE_DELAY_MIN_MILLIS = 10; + + enum State { + kHidden, + kShown, + kGoingToHide + }; + + State _state; + Type _type; + + Graphics::Surface _icon, _disabledIcon, _alphaIcon; + float _currentAlpha; + bool _alphaRising; + + uint32 _hideTime; + uint32 _lastUpdateTime; + + void initIcons(); + void loadIcon(Graphics::Surface &icon, byte *data, uint32 size); + void makeAlphaIcon(const Graphics::Surface &icon, float alpha); + + void hide(); +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/cloudicon_data.h b/backends/cloud/cloudicon_data.h new file mode 100644 index 0000000000..21d88182a3 --- /dev/null +++ b/backends/cloud/cloudicon_data.h @@ -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. + * + */ + +// This is a PNG file dumped into array. +// $ recode data..d1 <dists/cloudicon.png >cloudicon_data.h +// The tool is from https://github.com/pinard/Recode + +byte cloudicon_data[] = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, + 82, 0, 0, 0, 32, 0, 0, 0, 32, 8, 6, 0, 0, 0, 115, + 122, 122, 244, 0, 0, 0, 4, 115, 66, 73, 84, 8, 8, 8, 8, + 124, 8, 100, 136, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, + 18, 0, 0, 11, 18, 1, 210, 221, 126, 252, 0, 0, 0, 22, 116, + 69, 88, 116, 67, 114, 101, 97, 116, 105, 111, 110, 32, 84, 105, 109, + 101, 0, 48, 54, 47, 48, 51, 47, 49, 54, 159, 192, 233, 192, 0, + 0, 0, 28, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, + 0, 65, 100, 111, 98, 101, 32, 70, 105, 114, 101, 119, 111, 114, 107, + 115, 32, 67, 83, 54, 232, 188, 178, 140, 0, 0, 4, 50, 73, 68, + 65, 84, 88, 133, 197, 151, 109, 104, 150, 101, 20, 199, 127, 247, 227, + 179, 105, 51, 23, 65, 181, 150, 224, 154, 214, 132, 194, 249, 33, 165, + 22, 189, 231, 194, 210, 250, 16, 171, 180, 55, 42, 152, 68, 65, 100, + 52, 233, 5, 146, 144, 144, 26, 249, 169, 62, 164, 80, 89, 152, 25, + 18, 226, 42, 49, 87, 88, 180, 94, 96, 96, 246, 234, 180, 70, 50, + 66, 214, 55, 247, 22, 133, 247, 255, 244, 225, 58, 247, 158, 107, 143, + 219, 243, 60, 186, 192, 3, 135, 251, 220, 215, 117, 223, 231, 127, 238, + 235, 252, 239, 235, 58, 39, 105, 250, 206, 56, 147, 146, 55, 85, 252, + 108, 13, 112, 59, 176, 12, 88, 2, 204, 7, 230, 248, 220, 48, 208, + 15, 244, 2, 221, 64, 23, 48, 86, 137, 211, 228, 146, 158, 178, 43, + 80, 7, 172, 3, 218, 129, 179, 43, 241, 9, 140, 0, 155, 129, 87, + 128, 193, 146, 15, 47, 248, 178, 100, 0, 237, 64, 39, 112, 14, 96, + 174, 20, 217, 153, 228, 162, 0, 50, 25, 2, 58, 128, 45, 83, 1, + 228, 53, 121, 10, 170, 129, 183, 128, 213, 126, 47, 215, 49, 224, 39, + 224, 19, 160, 7, 56, 2, 204, 0, 154, 128, 27, 128, 229, 110, 215, + 120, 64, 181, 132, 149, 184, 30, 120, 4, 248, 183, 24, 40, 185, 248, + 243, 147, 86, 160, 154, 144, 195, 91, 252, 43, 5, 140, 2, 31, 2, + 27, 129, 195, 83, 125, 141, 75, 19, 240, 2, 129, 47, 179, 61, 144, + 4, 216, 7, 172, 44, 14, 34, 105, 232, 62, 41, 128, 109, 192, 189, + 14, 126, 2, 24, 0, 30, 3, 246, 150, 1, 46, 150, 54, 15, 184, + 1, 200, 251, 216, 123, 192, 253, 19, 2, 152, 183, 119, 66, 0, 237, + 192, 27, 110, 159, 0, 250, 128, 187, 128, 67, 167, 8, 158, 201, 98, + 224, 3, 160, 209, 131, 72, 128, 53, 68, 156, 200, 153, 192, 181, 206, + 68, 167, 219, 50, 49, 96, 226, 78, 19, 135, 162, 103, 138, 117, 169, + 137, 46, 19, 163, 38, 82, 19, 63, 152, 120, 220, 196, 12, 159, 63, + 104, 98, 149, 137, 99, 238, 211, 28, 163, 46, 243, 145, 147, 192, 117, + 157, 68, 173, 219, 195, 18, 143, 74, 28, 137, 230, 139, 181, 77, 162, + 71, 98, 165, 68, 141, 68, 78, 98, 145, 196, 107, 18, 59, 252, 30, + 137, 3, 18, 207, 72, 140, 249, 125, 173, 99, 33, 65, 114, 209, 110, + 131, 192, 218, 65, 2, 105, 4, 108, 245, 165, 74, 125, 165, 206, 5, + 214, 2, 151, 3, 7, 128, 119, 128, 95, 253, 189, 169, 228, 105, 224, + 85, 183, 171, 128, 29, 192, 29, 4, 82, 142, 18, 246, 151, 177, 164, + 126, 151, 1, 220, 3, 188, 79, 32, 222, 40, 97, 167, 235, 243, 151, + 207, 2, 190, 5, 154, 35, 231, 223, 0, 45, 101, 242, 127, 12, 152, + 75, 97, 191, 88, 12, 124, 237, 254, 18, 96, 21, 176, 35, 227, 192, + 50, 207, 15, 38, 126, 49, 113, 56, 202, 243, 221, 38, 154, 139, 114, + 223, 82, 130, 23, 153, 214, 155, 88, 20, 221, 255, 104, 226, 104, 116, + 223, 106, 42, 144, 112, 169, 137, 196, 131, 248, 56, 10, 166, 222, 196, + 141, 21, 128, 77, 165, 223, 155, 120, 42, 34, 246, 158, 200, 247, 18, + 19, 228, 21, 178, 60, 223, 151, 41, 1, 190, 112, 251, 60, 224, 171, + 104, 238, 116, 36, 33, 240, 224, 32, 240, 25, 176, 31, 120, 194, 199, + 27, 161, 112, 26, 102, 167, 154, 1, 127, 186, 253, 98, 9, 240, 81, + 2, 97, 43, 149, 231, 61, 128, 129, 104, 108, 78, 28, 64, 44, 25, + 105, 218, 74, 56, 156, 13, 252, 76, 248, 43, 42, 145, 140, 176, 249, + 226, 137, 188, 133, 20, 12, 123, 68, 9, 225, 171, 127, 7, 46, 40, + 227, 180, 82, 112, 128, 153, 126, 189, 148, 194, 105, 57, 12, 5, 18, + 246, 71, 68, 185, 214, 237, 191, 166, 65, 190, 98, 237, 243, 235, 205, + 209, 88, 127, 188, 19, 246, 250, 53, 39, 177, 194, 237, 157, 37, 118, + 193, 83, 213, 183, 37, 102, 73, 92, 39, 145, 196, 152, 57, 75, 193, + 82, 246, 249, 21, 75, 89, 104, 41, 205, 150, 178, 222, 82, 250, 163, + 241, 211, 213, 253, 150, 178, 201, 82, 174, 180, 148, 185, 150, 146, 248, + 120, 183, 165, 133, 20, 116, 153, 24, 113, 123, 150, 137, 103, 77, 28, + 55, 113, 141, 137, 173, 38, 134, 60, 61, 167, 178, 236, 3, 38, 54, + 152, 184, 213, 255, 253, 39, 221, 55, 142, 213, 101, 42, 252, 5, 99, + 132, 202, 101, 45, 97, 175, 94, 14, 172, 0, 118, 1, 15, 185, 78, + 71, 86, 19, 138, 217, 196, 117, 179, 99, 146, 204, 126, 125, 188, 30, + 168, 35, 236, 255, 181, 132, 3, 233, 15, 224, 54, 202, 87, 64, 229, + 164, 5, 216, 9, 92, 232, 224, 67, 192, 66, 188, 88, 205, 69, 185, + 26, 180, 148, 14, 207, 81, 206, 82, 230, 89, 202, 110, 75, 185, 108, + 26, 249, 191, 202, 82, 222, 181, 148, 243, 163, 220, 119, 56, 22, 150, + 78, 172, 7, 144, 216, 34, 177, 205, 237, 188, 196, 2, 137, 61, 18, + 15, 158, 6, 243, 31, 144, 216, 37, 209, 224, 190, 18, 137, 237, 142, + 49, 254, 92, 50, 115, 211, 164, 69, 233, 71, 64, 43, 140, 23, 165, + 127, 19, 26, 142, 231, 8, 117, 192, 84, 82, 69, 56, 118, 215, 3, + 55, 17, 54, 160, 172, 40, 253, 148, 80, 168, 78, 44, 74, 171, 59, + 39, 237, 11, 170, 129, 55, 129, 251, 40, 108, 205, 2, 254, 1, 126, + 243, 96, 122, 129, 163, 62, 223, 0, 92, 237, 160, 141, 17, 112, 38, + 219, 129, 135, 139, 193, 1, 146, 170, 151, 43, 106, 76, 106, 139, 198, + 229, 192, 73, 20, 96, 12, 152, 177, 253, 56, 101, 26, 147, 36, 191, + 177, 226, 214, 108, 13, 133, 214, 172, 220, 75, 35, 14, 90, 190, 53, + 203, 189, 84, 113, 119, 156, 53, 167, 173, 192, 21, 252, 95, 205, 105, + 178, 225, 204, 182, 231, 255, 1, 200, 91, 112, 221, 160, 249, 68, 42, + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 +}; diff --git a/backends/cloud/cloudicon_disabled_data.h b/backends/cloud/cloudicon_disabled_data.h new file mode 100644 index 0000000000..4340a8a37c --- /dev/null +++ b/backends/cloud/cloudicon_disabled_data.h @@ -0,0 +1,117 @@ +/* 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. + * + */ + +// This is a PNG file dumped into array. +// $ recode data..d1 <dists/cloudicon_disabled.png >cloudicon_disabled_data.h +// The tool is from https://github.com/pinard/Recode + +byte cloudicon_disabled_data[] = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, + 82, 0, 0, 0, 32, 0, 0, 0, 32, 8, 6, 0, 0, 0, 115, + 122, 122, 244, 0, 0, 0, 4, 115, 66, 73, 84, 8, 8, 8, 8, + 124, 8, 100, 136, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, + 18, 0, 0, 11, 18, 1, 210, 221, 126, 252, 0, 0, 0, 22, 116, + 69, 88, 116, 67, 114, 101, 97, 116, 105, 111, 110, 32, 84, 105, 109, + 101, 0, 48, 54, 47, 48, 51, 47, 49, 54, 159, 192, 233, 192, 0, + 0, 0, 28, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, + 0, 65, 100, 111, 98, 101, 32, 70, 105, 114, 101, 119, 111, 114, 107, + 115, 32, 67, 83, 54, 232, 188, 178, 140, 0, 0, 4, 139, 73, 68, + 65, 84, 88, 133, 197, 215, 91, 168, 86, 69, 20, 7, 240, 223, 254, + 244, 120, 236, 120, 57, 26, 94, 10, 31, 34, 35, 11, 36, 203, 74, + 212, 160, 204, 212, 110, 166, 248, 208, 205, 172, 151, 144, 136, 158, 82, + 80, 168, 32, 233, 165, 160, 32, 130, 236, 165, 34, 204, 74, 18, 52, + 36, 169, 232, 34, 24, 221, 243, 218, 133, 74, 77, 195, 44, 200, 91, + 122, 188, 156, 172, 102, 159, 221, 195, 204, 246, 219, 126, 231, 226, 17, + 138, 22, 12, 123, 246, 236, 153, 245, 95, 107, 205, 127, 205, 172, 157, + 21, 254, 95, 233, 27, 122, 63, 183, 5, 179, 48, 13, 87, 98, 52, + 6, 167, 111, 71, 176, 11, 27, 177, 14, 107, 209, 222, 27, 165, 217, + 137, 211, 207, 25, 137, 197, 152, 143, 65, 189, 209, 137, 163, 120, 1, + 79, 98, 111, 143, 147, 143, 247, 172, 108, 62, 158, 194, 16, 20, 169, + 105, 232, 151, 82, 171, 24, 80, 74, 27, 22, 225, 197, 110, 13, 104, + 235, 122, 188, 31, 94, 194, 93, 21, 192, 14, 49, 172, 223, 226, 109, + 124, 130, 29, 232, 131, 49, 184, 22, 55, 166, 126, 75, 131, 65, 175, + 225, 94, 252, 213, 201, 128, 131, 93, 131, 191, 137, 27, 42, 192, 199, + 241, 6, 158, 192, 246, 238, 188, 73, 50, 6, 143, 98, 78, 50, 164, + 140, 200, 123, 34, 135, 78, 49, 34, 219, 215, 89, 193, 171, 152, 151, + 192, 3, 246, 224, 1, 188, 123, 26, 224, 170, 92, 134, 143, 49, 160, + 50, 86, 96, 5, 238, 174, 78, 108, 204, 130, 249, 98, 216, 75, 240, + 109, 184, 13, 63, 156, 1, 248, 68, 44, 21, 189, 47, 165, 67, 140, + 196, 60, 172, 87, 225, 68, 182, 167, 62, 105, 100, 2, 106, 77, 11, + 118, 139, 123, 186, 163, 59, 164, 53, 76, 16, 195, 125, 29, 250, 15, + 227, 167, 73, 12, 237, 27, 73, 91, 75, 142, 28, 21, 185, 51, 60, + 141, 181, 225, 98, 41, 59, 106, 33, 185, 26, 88, 28, 104, 77, 253, + 163, 129, 251, 3, 59, 42, 223, 79, 105, 171, 184, 53, 240, 73, 224, + 150, 64, 203, 32, 106, 99, 185, 160, 224, 236, 16, 245, 118, 4, 54, + 5, 166, 4, 22, 6, 218, 211, 218, 214, 132, 37, 32, 75, 238, 181, + 224, 55, 12, 76, 222, 191, 140, 251, 144, 39, 79, 135, 98, 1, 198, + 98, 11, 150, 227, 251, 50, 204, 67, 113, 121, 90, 156, 57, 185, 127, + 135, 154, 184, 9, 95, 160, 9, 43, 49, 59, 69, 225, 24, 206, 65, + 123, 150, 54, 247, 14, 188, 158, 214, 30, 23, 79, 186, 109, 9, 252, + 44, 124, 142, 113, 149, 232, 127, 134, 201, 196, 88, 79, 66, 115, 5, + 252, 24, 182, 114, 224, 32, 35, 230, 212, 207, 139, 75, 241, 169, 168, + 47, 195, 157, 88, 89, 110, 193, 180, 64, 145, 250, 223, 5, 182, 87, + 194, 125, 123, 96, 92, 195, 22, 76, 14, 201, 227, 177, 226, 65, 144, + 227, 111, 28, 198, 151, 113, 131, 135, 5, 46, 169, 172, 249, 38, 176, + 187, 242, 62, 35, 160, 150, 199, 197, 19, 114, 178, 156, 34, 231, 173, + 244, 180, 154, 115, 115, 166, 166, 57, 167, 180, 193, 34, 3, 7, 165, + 61, 11, 137, 105, 27, 112, 160, 62, 111, 235, 106, 22, 166, 126, 71, + 206, 59, 165, 238, 156, 43, 114, 100, 155, 98, 120, 218, 146, 206, 2, + 83, 241, 225, 26, 134, 165, 253, 27, 173, 65, 134, 138, 137, 222, 154, + 222, 139, 228, 249, 87, 233, 217, 133, 76, 159, 19, 47, 169, 89, 226, + 129, 214, 71, 188, 192, 134, 148, 231, 64, 121, 171, 21, 248, 85, 244, + 232, 177, 30, 192, 59, 90, 82, 6, 193, 9, 108, 234, 30, 28, 30, + 9, 209, 128, 74, 214, 71, 204, 190, 121, 231, 201, 5, 228, 220, 218, + 248, 97, 136, 200, 246, 102, 106, 29, 234, 73, 190, 5, 135, 186, 7, + 135, 201, 9, 167, 111, 227, 135, 50, 2, 71, 146, 69, 153, 232, 245, + 206, 192, 136, 234, 196, 86, 49, 13, 154, 162, 113, 39, 211, 101, 51, + 126, 239, 25, 28, 154, 19, 206, 133, 234, 119, 195, 17, 234, 36, 220, + 85, 33, 202, 213, 169, 191, 175, 36, 92, 139, 152, 106, 3, 212, 9, + 119, 76, 100, 251, 126, 157, 9, 218, 69, 219, 150, 158, 211, 42, 99, + 187, 114, 245, 147, 112, 83, 122, 214, 2, 51, 83, 127, 85, 16, 89, + 62, 94, 60, 61, 202, 20, 58, 36, 38, 244, 126, 93, 159, 146, 93, + 180, 101, 129, 254, 129, 107, 2, 89, 26, 219, 24, 42, 6, 188, 95, + 153, 124, 81, 202, 251, 37, 3, 249, 101, 188, 120, 114, 148, 223, 219, + 197, 212, 56, 208, 123, 240, 245, 129, 167, 3, 19, 3, 163, 42, 6, + 172, 11, 234, 36, 92, 43, 242, 105, 32, 250, 227, 161, 89, 44, 45, + 98, 196, 139, 14, 178, 146, 112, 27, 210, 179, 23, 178, 7, 203, 240, + 248, 156, 152, 251, 15, 38, 221, 146, 138, 181, 212, 73, 216, 46, 214, + 112, 11, 82, 180, 103, 138, 71, 237, 40, 145, 52, 29, 216, 221, 194, + 220, 41, 49, 0, 103, 36, 129, 185, 152, 158, 116, 101, 9, 171, 29, + 178, 85, 245, 121, 213, 235, 184, 90, 215, 229, 248, 89, 172, 112, 190, + 62, 83, 112, 209, 145, 85, 226, 229, 147, 105, 188, 142, 43, 172, 220, + 155, 179, 40, 29, 201, 85, 6, 239, 204, 153, 155, 243, 117, 47, 216, + 222, 216, 38, 229, 188, 146, 51, 188, 162, 119, 81, 194, 82, 205, 130, + 178, 189, 24, 120, 173, 97, 108, 80, 34, 102, 111, 73, 87, 182, 123, + 2, 107, 2, 231, 133, 184, 213, 89, 96, 69, 194, 56, 57, 47, 91, + 222, 57, 100, 253, 68, 130, 92, 175, 94, 148, 254, 129, 15, 240, 176, + 88, 7, 116, 39, 77, 226, 181, 187, 68, 172, 146, 154, 69, 78, 101, + 98, 77, 57, 91, 99, 81, 250, 82, 215, 138, 202, 178, 188, 44, 78, + 37, 67, 254, 196, 143, 201, 152, 141, 98, 217, 86, 224, 60, 92, 149, + 64, 207, 175, 0, 151, 178, 66, 119, 101, 249, 243, 61, 184, 163, 254, + 99, 210, 218, 48, 94, 94, 5, 101, 13, 162, 1, 176, 100, 251, 97, + 167, 249, 49, 233, 115, 179, 250, 111, 78, 23, 109, 115, 193, 178, 130, + 62, 5, 151, 20, 52, 23, 241, 76, 200, 10, 106, 149, 103, 173, 50, + 158, 21, 28, 45, 120, 174, 96, 110, 193, 71, 61, 232, 151, 61, 219, + 115, 4, 170, 82, 254, 156, 206, 16, 47, 197, 127, 231, 231, 244, 153, + 222, 27, 240, 159, 200, 63, 153, 185, 24, 191, 162, 246, 71, 153, 0, + 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 +}; diff --git a/backends/cloud/cloudmanager.cpp b/backends/cloud/cloudmanager.cpp new file mode 100644 index 0000000000..5f7d6942ca --- /dev/null +++ b/backends/cloud/cloudmanager.cpp @@ -0,0 +1,480 @@ +/* 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 "backends/cloud/cloudmanager.h" +#include "backends/cloud/box/boxstorage.h" +#include "backends/cloud/dropbox/dropboxstorage.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "common/translation.h" +#include "common/config-manager.h" +#include "common/str.h" +#ifdef USE_SDL_NET +#include "backends/networking/sdl_net/localwebserver.h" +#endif + +namespace Common { + +DECLARE_SINGLETON(Cloud::CloudManager); + +} + +namespace Cloud { + +const char *const CloudManager::kStoragePrefix = "storage_"; + +CloudManager::CloudManager() : _currentStorageIndex(0), _activeStorage(nullptr) {} + +CloudManager::~CloudManager() { + g_system->getEventManager()->getEventDispatcher()->unregisterSource(this); + + delete _activeStorage; + freeStorages(); +} + +Common::String CloudManager::getStorageConfigName(uint32 index) const { + switch (index) { + case kStorageNoneId: return "<none>"; + case kStorageDropboxId: return "Dropbox"; + case kStorageOneDriveId: return "OneDrive"; + case kStorageGoogleDriveId: return "GoogleDrive"; + case kStorageBoxId: return "Box"; + } + assert(false); // Unhandled StorageID value + return ""; +} + +void CloudManager::loadStorage() { + switch (_currentStorageIndex) { + case kStorageDropboxId: + _activeStorage = Dropbox::DropboxStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageOneDriveId: + _activeStorage = OneDrive::OneDriveStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageGoogleDriveId: + _activeStorage = GoogleDrive::GoogleDriveStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + case kStorageBoxId: + _activeStorage = Box::BoxStorage::loadFromConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + break; + default: + _activeStorage = nullptr; + } + + if (!_activeStorage) { + _currentStorageIndex = kStorageNoneId; + } +} + +void CloudManager::init() { + //init configs structs + for (uint32 i = 0; i < kStorageTotal; ++i) { + Common::String name = getStorageConfigName(i); + StorageConfig config; + config.name = _(name); + config.username = ""; + config.lastSyncDate = ""; + config.usedBytes = 0; + if (ConfMan.hasKey(kStoragePrefix + name + "_username", ConfMan.kCloudDomain)) + config.username = ConfMan.get(kStoragePrefix + name + "_username", ConfMan.kCloudDomain); + if (ConfMan.hasKey(kStoragePrefix + name + "_lastSync", ConfMan.kCloudDomain)) + config.lastSyncDate = ConfMan.get(kStoragePrefix + name + "_lastSync", ConfMan.kCloudDomain); + if (ConfMan.hasKey(kStoragePrefix + name + "_usedBytes", ConfMan.kCloudDomain)) + config.usedBytes = ConfMan.get(kStoragePrefix + name + "_usedBytes", ConfMan.kCloudDomain).asUint64(); + _storages.push_back(config); + } + + //load an active storage if there is any + _currentStorageIndex = kStorageNoneId; + if (ConfMan.hasKey("current_storage", ConfMan.kCloudDomain)) + _currentStorageIndex = ConfMan.getInt("current_storage", ConfMan.kCloudDomain); + + loadStorage(); + + g_system->getEventManager()->getEventDispatcher()->registerSource(this, false); +} + +void CloudManager::save() { + for (uint32 i = 0; i < _storages.size(); ++i) { + if (i == kStorageNoneId) + continue; + Common::String name = getStorageConfigName(i); + ConfMan.set(kStoragePrefix + name + "_username", _storages[i].username, ConfMan.kCloudDomain); + ConfMan.set(kStoragePrefix + name + "_lastSync", _storages[i].lastSyncDate, ConfMan.kCloudDomain); + ConfMan.set(kStoragePrefix + name + "_usedBytes", Common::String::format("%lu", _storages[i].usedBytes), ConfMan.kCloudDomain); + } + + ConfMan.set("current_storage", Common::String::format("%u", _currentStorageIndex), ConfMan.kCloudDomain); + if (_activeStorage) + _activeStorage->saveConfig(kStoragePrefix + getStorageConfigName(_currentStorageIndex) + "_"); + ConfMan.flushToDisk(); +} + +void CloudManager::replaceStorage(Storage *storage, uint32 index) { + freeStorages(); + if (!storage) + error("CloudManager::replaceStorage: NULL storage passed"); + if (index >= kStorageTotal) + error("CloudManager::replaceStorage: invalid index passed"); + if (_activeStorage != nullptr && _activeStorage->isWorking()) { + warning("CloudManager::replaceStorage: replacing Storage while the other is working"); + if (_activeStorage->isDownloading()) + _activeStorage->cancelDownload(); + if (_activeStorage->isSyncing()) + _activeStorage->cancelSync(); + removeStorage(_activeStorage); + } else { + delete _activeStorage; + } + _activeStorage = storage; + _currentStorageIndex = index; + save(); + + //do what should be done on first Storage connect + if (_activeStorage) { + _activeStorage->info(nullptr, nullptr); //automatically calls setStorageUsername() + _activeStorage->syncSaves(nullptr, nullptr); + } +} + +void CloudManager::removeStorage(Storage *storage) { + // can't just delete it as it's mostly likely the one who calls the method + // it would be freed on freeStorages() call (on next Storage connect or replace) + _storagesToRemove.push_back(storage); +} + +void CloudManager::freeStorages() { + for (uint32 i = 0; i < _storagesToRemove.size(); ++i) + delete _storagesToRemove[i]; + _storagesToRemove.clear(); +} + +void CloudManager::passNoStorageConnected(Networking::ErrorCallback errorCallback) const { + if (errorCallback == nullptr) + return; + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "No Storage connected!", -1)); +} + +Storage *CloudManager::getCurrentStorage() const { + return _activeStorage; +} + +uint32 CloudManager::getStorageIndex() const { + return _currentStorageIndex; +} + +Common::StringArray CloudManager::listStorages() const { + Common::StringArray result; + for (uint32 i = 0; i < _storages.size(); ++i) { + result.push_back(_storages[i].name); + } + return result; +} + +bool CloudManager::switchStorage(uint32 index) { + if (index >= _storages.size()) { + warning("CloudManager::switchStorage: invalid index passed"); + return false; + } + + Storage *storage = getCurrentStorage(); + if (storage && storage->isWorking()) { + warning("CloudManager::switchStorage: another storage is working now"); + return false; + } + + _currentStorageIndex = index; + loadStorage(); + save(); + return true; +} + +Common::String CloudManager::getStorageUsername(uint32 index) { + if (index >= _storages.size()) + return ""; + return _storages[index].username; +} + +uint64 CloudManager::getStorageUsedSpace(uint32 index) { + if (index >= _storages.size()) + return 0; + return _storages[index].usedBytes; +} + +Common::String CloudManager::getStorageLastSync(uint32 index) { + if (index >= _storages.size()) + return ""; + if (index == _currentStorageIndex && isSyncing()) + return ""; + return _storages[index].lastSyncDate; +} + +void CloudManager::setStorageUsername(uint32 index, Common::String name) { + if (index >= _storages.size()) + return; + _storages[index].username = name; + save(); +} + +void CloudManager::setStorageUsedSpace(uint32 index, uint64 used) { + if (index >= _storages.size()) + return; + _storages[index].usedBytes = used; + save(); +} + +void CloudManager::setStorageLastSync(uint32 index, Common::String date) { + if (index >= _storages.size()) + return; + _storages[index].lastSyncDate = date; + save(); +} + +void CloudManager::connectStorage(uint32 index, Common::String code) { + freeStorages(); + + Storage *storage = nullptr; + switch (index) { + case kStorageDropboxId: + storage = new Dropbox::DropboxStorage(code); + break; + case kStorageOneDriveId: + storage = new OneDrive::OneDriveStorage(code); + break; + case kStorageGoogleDriveId: + storage = new GoogleDrive::GoogleDriveStorage(code); + break; + case kStorageBoxId: + storage = new Box::BoxStorage(code); + break; + } + // in these constructors Storages request token using the passed code + // when the token is received, they call replaceStorage() + // or removeStorage(), if some error occurred + // thus, no memory leak happens +} + +Networking::Request *CloudManager::listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->listDirectory(path, callback, errorCallback, recursive); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Networking::Request *CloudManager::downloadFolder(Common::String remotePath, Common::String localPath, Storage::FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->downloadFolder(remotePath, localPath, callback, errorCallback, recursive); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Networking::Request *CloudManager::info(Storage::StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Storage *storage = getCurrentStorage(); + if (storage) { + return storage->info(callback, errorCallback); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +Common::String CloudManager::savesDirectoryPath() { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->savesDirectoryPath(); + return ""; +} + +SavesSyncRequest *CloudManager::syncSaves(Storage::BoolCallback callback, Networking::ErrorCallback errorCallback) { + Storage *storage = getCurrentStorage(); + if (storage) { + setStorageLastSync(_currentStorageIndex, "???"); //TODO get the date + return storage->syncSaves(callback, errorCallback); + } else { + passNoStorageConnected(errorCallback); + delete callback; + delete errorCallback; + } + return nullptr; +} + +bool CloudManager::isWorking() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isWorking(); + return false; +} + +bool CloudManager::couldUseLocalServer() { +#ifdef USE_SDL_NET + return Networking::LocalWebserver::getPort() == Networking::LocalWebserver::DEFAULT_SERVER_PORT; +#else + return false; +#endif +} + +///// SavesSyncRequest-related ///// + +bool CloudManager::isSyncing() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isSyncing(); + return false; +} + +double CloudManager::getSyncDownloadingProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncDownloadingProgress(); + return 1; +} + +double CloudManager::getSyncProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncProgress(); + return 1; +} + +Common::Array<Common::String> CloudManager::getSyncingFiles() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getSyncingFiles(); + return Common::Array<Common::String>(); +} + +void CloudManager::cancelSync() const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->cancelSync(); +} + +void CloudManager::setSyncTarget(GUI::CommandReceiver *target) const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->setSyncTarget(target); +} + +void CloudManager::showCloudDisabledIcon() { + _icon.show(CloudIcon::kDisabled, 3000); +} + +///// DownloadFolderRequest-related ///// + +bool CloudManager::startDownload(Common::String remotePath, Common::String localPath) const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->startDownload(remotePath, localPath); + return false; +} + +void CloudManager::cancelDownload() const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->cancelDownload(); +} + +void CloudManager::setDownloadTarget(GUI::CommandReceiver *target) const { + Storage *storage = getCurrentStorage(); + if (storage) + storage->setDownloadTarget(target); +} + +bool CloudManager::isDownloading() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->isDownloading(); + return false; +} + +double CloudManager::getDownloadingProgress() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadingProgress(); + return 1; +} + +uint64 CloudManager::getDownloadBytesNumber() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadBytesNumber(); + return 0; +} + +uint64 CloudManager::getDownloadTotalBytesNumber() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadTotalBytesNumber(); + return 0; +} + +uint64 CloudManager::getDownloadSpeed() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadSpeed(); + return 0; +} + +Common::String CloudManager::getDownloadRemoteDirectory() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadRemoteDirectory(); + return ""; +} + +Common::String CloudManager::getDownloadLocalDirectory() const { + Storage *storage = getCurrentStorage(); + if (storage) + return storage->getDownloadLocalDirectory(); + return ""; +} + +bool CloudManager::pollEvent(Common::Event &event) { + if (_icon.needsUpdate()) { + if (_icon.getShownType() != CloudIcon::kDisabled) { + if (isWorking()) { + _icon.show(CloudIcon::kSyncing); + } else { + _icon.show(CloudIcon::kNone); + } + } + + _icon.update(); + } + + return false; +} + +} // End of namespace Cloud diff --git a/backends/cloud/cloudmanager.h b/backends/cloud/cloudmanager.h new file mode 100644 index 0000000000..f58ea8373a --- /dev/null +++ b/backends/cloud/cloudmanager.h @@ -0,0 +1,294 @@ +/* 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 CLOUD_CLOUDMANAGER_H +#define CLOUD_CLOUDMANAGER_H + +#include "backends/cloud/storage.h" +#include "backends/cloud/cloudicon.h" + +#include "common/array.h" +#include "common/singleton.h" +#include "common/str-array.h" +#include "common/events.h" + +namespace GUI { + +class CommandReceiver; + +} + +namespace Cloud { + +// The actual indexes in CloudManager's array +enum StorageID { + kStorageNoneId = 0, + kStorageDropboxId = 1, + kStorageOneDriveId = 2, + kStorageGoogleDriveId = 3, + kStorageBoxId = 4, + + kStorageTotal +}; + +class CloudManager : public Common::Singleton<CloudManager>, public Common::EventSource { + static const char *const kStoragePrefix; + + struct StorageConfig { + Common::String name, username; + uint64 usedBytes; + Common::String lastSyncDate; + }; + + Common::Array<StorageConfig> _storages; + uint _currentStorageIndex; + Storage *_activeStorage; + Common::Array<Storage *> _storagesToRemove; + + CloudIcon _icon; + + void loadStorage(); + + Common::String getStorageConfigName(uint32 index) const; + + /** Frees memory used by storages which failed to connect. */ + void freeStorages(); + + /** Calls the error callback with a special "no storage connected" message. */ + void passNoStorageConnected(Networking::ErrorCallback errorCallback) const; + + /** + * Common::EventSource interface + * + * The cloud manager registers itself as an event source even if does not + * actually produce events as a mean to be polled periodically by the GUI + * or engine code. + * + * The periodical polling is used to update the OSD icon indicating + * background sync activity. + */ + virtual bool pollEvent(Common::Event &event) override; + +public: + CloudManager(); + virtual ~CloudManager(); + + /** + * Loads all information from configs and creates current Storage instance. + * + * @note It's called once on startup in scummvm_main(). + */ + void init(); + + /** + * Saves all information into configuration file. + */ + void save(); + + /** + * Replace active Storage. + * @note this method automatically saves the changes with ConfMan. + * + * @param storage Cloud::Storage to replace active storage with. + * @param index one of Cloud::StorageID enum values to indicate what storage type is replaced. + */ + void replaceStorage(Storage *storage, uint32 index); + + /** Adds storage in the list of storages to remove later. */ + void removeStorage(Storage *storage); + + /** + * Returns active Storage, which could be used to interact + * with cloud storage. + * + * @return active Cloud::Storage or null, if there is no active Storage. + */ + Cloud::Storage *getCurrentStorage() const; + + /** + * Return active Storage's index. + * + * @return active Storage's index. + */ + uint32 getStorageIndex() const; + + /** + * Return Storages names as list. + * + * @return a list of Storages names. + */ + Common::StringArray listStorages() const; + + /** + * Changes the storage to the one with given index. + * + * @param new Storage's index. + */ + bool switchStorage(uint32 index); + + /** + * Return username used by Storage. + * + * @param Storage's index. + * @returns username or "" if index is invalid (no such Storage). + */ + Common::String getStorageUsername(uint32 index); + + /** + * Return space used by Storage. + * + * @param Storage's index. + * @returns used space in bytes or 0 if index is invalid (no such Storage). + */ + uint64 getStorageUsedSpace(uint32 index); + + /** + * Return Storage's last sync date. + * + * @param Storage's index. + * @returns last sync date or "" if index is invalid (no such Storage). + It also returns "" if there never was any sync + or if storage is syncing right now. + */ + Common::String getStorageLastSync(uint32 index); + + /** + * Set Storage's username. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param name username to set + */ + void setStorageUsername(uint32 index, Common::String name); + + /** + * Set Storage's used space field. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param used value to set + */ + void setStorageUsedSpace(uint32 index, uint64 used); + + /** + * Set Storage's last sync date. + * Automatically saves changes to the config. + * + * @param index Storage's index. + * @param date date to set + */ + void setStorageLastSync(uint32 index, Common::String date); + + /** + * Replace Storage which has given index with a + * storage created with given code. + * + * @param index Storage's index + * @param code OAuth2 code received from user + */ + void connectStorage(uint32 index, Common::String code); + + /** Returns ListDirectoryResponse with list of files. */ + Networking::Request *listDirectory(Common::String path, Storage::ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns Common::Array<StorageFile> with list of files, which were not downloaded. */ + Networking::Request *downloadFolder(Common::String remotePath, Common::String localPath, Storage::FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Return the StorageInfo struct. */ + Networking::Request *info(Storage::StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + Common::String savesDirectoryPath(); + + /** + * Starts saves syncing process in currently active storage if there is any. + */ + SavesSyncRequest *syncSaves(Cloud::Storage::BoolCallback callback = nullptr, Networking::ErrorCallback errorCallback = nullptr); + + /** Returns whether there are any requests running. */ + bool isWorking() const; + + /** Returns whether LocalWebserver is available to use for auth. */ + static bool couldUseLocalServer(); + + ///// SavesSyncRequest-related ///// + + /** Returns whether there is a SavesSyncRequest running. */ + bool isSyncing() const; + + /** Returns a number in [0, 1] range which represents current sync downloading progress (1 = complete). */ + double getSyncDownloadingProgress() const; + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + double getSyncProgress() const; + + /** Returns an array of saves names which are not yet synced (thus cannot be used). */ + Common::Array<Common::String> getSyncingFiles() const; + + /** Cancels running sync. */ + void cancelSync() const; + + /** Sets SavesSyncRequest's target to given CommandReceiver. */ + void setSyncTarget(GUI::CommandReceiver *target) const; + + /** Shows a "cloud disabled" icon for three seconds. */ + void showCloudDisabledIcon(); + + ///// DownloadFolderRequest-related ///// + + /** Starts a folder download. */ + bool startDownload(Common::String remotePath, Common::String localPath) const; + + /** Cancels running download. */ + void cancelDownload() const; + + /** Sets FolderDownloadRequest's target to given CommandReceiver. */ + void setDownloadTarget(GUI::CommandReceiver *target) const; + + /** Returns whether there is a FolderDownloadRequest running. */ + bool isDownloading() const; + + /** Returns a number in [0, 1] range which represents current download progress (1 = complete). */ + double getDownloadingProgress() const; + + /** Returns a number of bytes that is downloaded in current download progress. */ + uint64 getDownloadBytesNumber() const; + + /** Returns a total number of bytes to be downloaded in current download progress. */ + uint64 getDownloadTotalBytesNumber() const; + + /** Returns download speed of current download progress. */ + uint64 getDownloadSpeed() const; + + /** Returns remote directory path. */ + Common::String getDownloadRemoteDirectory() const; + + /** Returns local directory path. */ + Common::String getDownloadLocalDirectory() const; +}; + +/** Shortcut for accessing the connection manager. */ +#define CloudMan Cloud::CloudManager::instance() + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/downloadrequest.cpp b/backends/cloud/downloadrequest.cpp new file mode 100644 index 0000000000..c07a23b671 --- /dev/null +++ b/backends/cloud/downloadrequest.cpp @@ -0,0 +1,138 @@ +/* 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 "backends/cloud/downloadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "common/textconsole.h" + +namespace Cloud { + +DownloadRequest::DownloadRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb, Common::String remoteFileId, Common::DumpFile *dumpFile): + Request(nullptr, ecb), _boolCallback(callback), _localFile(dumpFile), _remoteFileId(remoteFileId), _storage(storage), + _remoteFileStream(nullptr), _workingRequest(nullptr), _ignoreCallback(false), _buffer(new byte[DOWNLOAD_REQUEST_BUFFER_SIZE]) { + start(); +} + +DownloadRequest::~DownloadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; + delete _localFile; + delete[] _buffer; +} + +void DownloadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _remoteFileStream = nullptr; + //TODO: add some way to reopen DumpFile, so DownloadRequest could be restarted + _ignoreCallback = false; + + _workingRequest = _storage->streamFileById( + _remoteFileId, + new Common::Callback<DownloadRequest, Networking::NetworkReadStreamResponse>(this, &DownloadRequest::streamCallback), + new Common::Callback<DownloadRequest, Networking::ErrorResponse>(this, &DownloadRequest::streamErrorCallback) + ); +} + +void DownloadRequest::streamCallback(Networking::NetworkReadStreamResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _remoteFileStream = (Networking::NetworkReadStream *)response.value; +} + +void DownloadRequest::streamErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void DownloadRequest::handle() { + if (!_localFile) { + warning("DownloadRequest: no file to write"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (!_localFile->isOpen()) { + warning("DownloadRequest: failed to open file to write"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (!_remoteFileStream) { + //waiting for callback + return; + } + + uint32 readBytes = _remoteFileStream->read(_buffer, DOWNLOAD_REQUEST_BUFFER_SIZE); + + if (readBytes != 0) + if (_localFile->write(_buffer, readBytes) != readBytes) { + warning("DownloadRequest: unable to write all received bytes into output file"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + if (_remoteFileStream->eos()) { + if (_remoteFileStream->httpResponseCode() != 200) { + warning("DownloadRequest: HTTP response code is not 200 OK (it's %ld)", _remoteFileStream->httpResponseCode()); + //TODO: do something about it actually + // the problem is file's already downloaded, stream is over + // so we can't return error message anymore + } + + finishDownload(_remoteFileStream->httpResponseCode() == 200); + + _localFile->close(); //yes, I know it's closed automatically in ~DumpFile() + } +} + +void DownloadRequest::restart() { + warning("DownloadRequest: can't restart as there are no means to reopen DumpFile"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + //start(); +} + +void DownloadRequest::finishDownload(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +void DownloadRequest::finishError(Networking::ErrorResponse error) { + if (_localFile) + _localFile->close(); + Request::finishError(error); +} + +double DownloadRequest::getProgress() const { + if (_remoteFileStream) + return _remoteFileStream->getProgress(); + return 0; +} + +} // End of namespace Cloud diff --git a/backends/cloud/downloadrequest.h b/backends/cloud/downloadrequest.h new file mode 100644 index 0000000000..a5698d89cb --- /dev/null +++ b/backends/cloud/downloadrequest.h @@ -0,0 +1,64 @@ +/* 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 BACKENDS_CLOUD_DOWNLOADREQUEST_H +#define BACKENDS_CLOUD_DOWNLOADREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/networkreadstream.h" +#include "backends/cloud/storage.h" +#include "common/file.h" + +namespace Cloud { + +#define DOWNLOAD_REQUEST_BUFFER_SIZE 1 * 1024 * 1024 + +class DownloadRequest: public Networking::Request { + Storage::BoolCallback _boolCallback; + Common::DumpFile *_localFile; + Common::String _remoteFileId; + Storage *_storage; + Networking::NetworkReadStream *_remoteFileStream; + Request *_workingRequest; + bool _ignoreCallback; + byte *_buffer; + + void start(); + void streamCallback(Networking::NetworkReadStreamResponse response); + void streamErrorCallback(Networking::ErrorResponse error); + void finishDownload(bool success); + virtual void finishError(Networking::ErrorResponse error); + +public: + DownloadRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb, Common::String remoteFileId, Common::DumpFile *dumpFile); + virtual ~DownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp new file mode 100644 index 0000000000..411e367a9d --- /dev/null +++ b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.cpp @@ -0,0 +1,137 @@ +/* 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 "backends/cloud/dropbox/dropboxcreatedirectoryrequest.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_CREATE_FOLDER "https://api.dropboxapi.com/2/files/create_folder" + +DropboxCreateDirectoryRequest::DropboxCreateDirectoryRequest(Common::String token, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _path(path), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxCreateDirectoryRequest::~DropboxCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void DropboxCreateDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxCreateDirectoryRequest, Networking::JsonResponse>(this, &DropboxCreateDirectoryRequest::responseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxCreateDirectoryRequest, Networking::ErrorResponse>(this, &DropboxCreateDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_CREATE_FOLDER); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(_path)); + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxCreateDirectoryRequest::responseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + if (response.request) _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject info = json->asObject(); + if (info.contains("id")) { + finishCreation(true); + } else { + if (Networking::CurlJsonRequest::jsonContainsString(info, "error_summary", "DropboxCreateDirectoryRequest")) { + Common::String summary = info.getVal("error_summary")->asString(); + if (summary.contains("path") && summary.contains("conflict") && summary.contains("folder")) { + // existing directory - not an error for CreateDirectoryRequest + finishCreation(false); + delete json; + return; + } + } + error.response = json->stringify(true); + finishError(error); + } + + delete json; +} + +void DropboxCreateDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void DropboxCreateDirectoryRequest::handle() {} + +void DropboxCreateDirectoryRequest::restart() { start(); } + +Common::String DropboxCreateDirectoryRequest::date() const { return _date; } + +void DropboxCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h new file mode 100644 index 0000000000..01faf4d8af --- /dev/null +++ b/backends/cloud/dropbox/dropboxcreatedirectoryrequest.h @@ -0,0 +1,57 @@ +/* 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 BACKENDS_CLOUD_DROPBOX_DROPBOXCREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXCREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxCreateDirectoryRequest: public Networking::Request { + Common::String _token; + Common::String _path; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + DropboxCreateDirectoryRequest(Common::String token, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~DropboxCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxinforequest.cpp b/backends/cloud/dropbox/dropboxinforequest.cpp new file mode 100644 index 0000000000..3d82c8d34b --- /dev/null +++ b/backends/cloud/dropbox/dropboxinforequest.cpp @@ -0,0 +1,192 @@ +/* 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 "backends/cloud/dropbox/dropboxinforequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_GET_CURRENT_ACCOUNT "https://api.dropboxapi.com/2/users/get_current_account" +#define DROPBOX_API_GET_SPACE_USAGE "https://api.dropboxapi.com/2/users/get_space_usage" + +DropboxInfoRequest::DropboxInfoRequest(Common::String token, Storage::StorageInfoCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _infoCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxInfoRequest::~DropboxInfoRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _infoCallback; +} + +void DropboxInfoRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxInfoRequest, Networking::JsonResponse>(this, &DropboxInfoRequest::userResponseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxInfoRequest, Networking::ErrorResponse>(this, &DropboxInfoRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_GET_CURRENT_ACCOUNT); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + request->addPostField("null"); //use POST + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxInfoRequest::userResponseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + //Dropbox documentation states there are no errors for this API method + Common::JSONObject info = json->asObject(); + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "name", "DropboxInfoRequest") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("name"), "DropboxInfoRequest")) { + Common::JSONObject nameInfo = info.getVal("name")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(nameInfo, "display_name", "DropboxInfoRequest")) { + _name = nameInfo.getVal("display_name")->asString(); + } + } + if (Networking::CurlJsonRequest::jsonContainsString(info, "account_id", "DropboxInfoRequest")) { + _uid = info.getVal("account_id")->asString(); + } + if (Networking::CurlJsonRequest::jsonContainsString(info, "email", "DropboxInfoRequest")) { + _email = info.getVal("email")->asString(); + } + CloudMan.setStorageUsername(kStorageDropboxId, _email); + delete json; + + Networking::JsonCallback innerCallback = new Common::Callback<DropboxInfoRequest, Networking::JsonResponse>(this, &DropboxInfoRequest::quotaResponseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxInfoRequest, Networking::ErrorResponse>(this, &DropboxInfoRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, DROPBOX_API_GET_SPACE_USAGE); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + request->addPostField("null"); //use POST + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxInfoRequest::quotaResponseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + //Dropbox documentation states there are no errors for this API method + Common::JSONObject info = json->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "used", "DropboxInfoRequest")) { + error.response = "Passed JSON misses 'used' attribute!"; + finishError(error); + delete json; + return; + } + + uint64 used = info.getVal("used")->asIntegerNumber(), allocated = 0; + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "allocation", "DropboxInfoRequest") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("allocation"), "DropboxInfoRequest")) { + Common::JSONObject allocation = info.getVal("allocation")->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(allocation, "allocated", "DropboxInfoRequest")) { + error.response = "Passed JSON misses 'allocation/allocated' attribute!"; + finishError(error); + delete json; + return; + } + + allocated = allocation.getVal("allocated")->asIntegerNumber(); + } + + finishInfo(StorageInfo(_uid, _name, _email, used, allocated)); + delete json; +} + +void DropboxInfoRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) return; + finishError(error); +} + +void DropboxInfoRequest::handle() {} + +void DropboxInfoRequest::restart() { start(); } + +void DropboxInfoRequest::finishInfo(StorageInfo info) { + Request::finishSuccess(); + if (_infoCallback) + (*_infoCallback)(Storage::StorageInfoResponse(this, info)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxinforequest.h b/backends/cloud/dropbox/dropboxinforequest.h new file mode 100644 index 0000000000..0ed92cd68a --- /dev/null +++ b/backends/cloud/dropbox/dropboxinforequest.h @@ -0,0 +1,56 @@ +/* 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 BACKENDS_CLOUD_DROPBOX_DROPBOXINFOREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXINFOREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxInfoRequest: public Networking::Request { + Common::String _token; + Common::String _uid, _name, _email; + Storage::StorageInfoCallback _infoCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void userResponseCallback(Networking::JsonResponse response); + void quotaResponseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishInfo(StorageInfo info); +public: + DropboxInfoRequest(Common::String token, Storage::StorageInfoCallback cb, Networking::ErrorCallback ecb); + virtual ~DropboxInfoRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp b/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp new file mode 100644 index 0000000000..0b90ec6d2c --- /dev/null +++ b/backends/cloud/dropbox/dropboxlistdirectoryrequest.cpp @@ -0,0 +1,223 @@ +/* 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 "backends/cloud/dropbox/dropboxlistdirectoryrequest.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "common/debug.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_LIST_FOLDER "https://api.dropboxapi.com/2/files/list_folder" +#define DROPBOX_API_LIST_FOLDER_CONTINUE "https://api.dropboxapi.com/2/files/list_folder/continue" + +DropboxListDirectoryRequest::DropboxListDirectoryRequest(Common::String token, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), _requestedPath(path), _requestedRecursive(recursive), _listDirectoryCallback(cb), + _token(token), _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxListDirectoryRequest::~DropboxListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void DropboxListDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + Networking::JsonCallback callback = new Common::Callback<DropboxListDirectoryRequest, Networking::JsonResponse>(this, &DropboxListDirectoryRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxListDirectoryRequest, Networking::ErrorResponse>(this, &DropboxListDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, DROPBOX_API_LIST_FOLDER); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(_requestedPath)); + jsonRequestParameters.setVal("recursive", new Common::JSONValue(_requestedRecursive)); + jsonRequestParameters.setVal("include_media_info", new Common::JSONValue(false)); + jsonRequestParameters.setVal("include_deleted", new Common::JSONValue(false)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxListDirectoryRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + + if (_ignoreCallback) { + delete response.value; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject responseObject = json->asObject(); + + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + if (responseObject.contains("error_summary") && responseObject.getVal("error_summary")->isString()) { + warning("Dropbox returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + } + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (responseObject.contains("entries")) { + if (!responseObject.getVal("entries")->isArray()) { + error.response = Common::String::format( + "\"entries\" found, but that's not an array!\n%s", + responseObject.getVal("entries")->stringify(true).c_str() + ); + finishError(error); + delete json; + return; + } + + Common::JSONArray items = responseObject.getVal("entries")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "DropboxListDirectoryRequest")) + continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsString(item, "path_lower", "DropboxListDirectoryRequest")) + continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, ".tag", "DropboxListDirectoryRequest")) + continue; + + Common::String path = item.getVal("path_lower")->asString(); + bool isDirectory = (item.getVal(".tag")->asString() == "folder"); + uint32 size = 0, timestamp = 0; + if (!isDirectory) { + if (!Networking::CurlJsonRequest::jsonContainsString(item, "server_modified", "DropboxListDirectoryRequest")) + continue; + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(item, "size", "DropboxListDirectoryRequest")) + continue; + + size = item.getVal("size")->asIntegerNumber(); + timestamp = ISO8601::convertToTimestamp(item.getVal("server_modified")->asString()); + } + _files.push_back(StorageFile(path, size, timestamp, isDirectory)); + } + } + + bool hasMore = false; + if (responseObject.contains("has_more")) { + if (!responseObject.getVal("has_more")->isBool()) { + warning("DropboxListDirectoryRequest: \"has_more\" is not a boolean"); + debug(9, "%s", responseObject.getVal("has_more")->stringify(true).c_str()); + error.response = "\"has_more\" is not a boolean!"; + finishError(error); + delete json; + return; + } + + hasMore = responseObject.getVal("has_more")->asBool(); + } + + if (hasMore) { + if (!Networking::CurlJsonRequest::jsonContainsString(responseObject, "cursor", "DropboxListDirectoryRequest")) { + error.response = "\"has_more\" found, but \"cursor\" is not (or it's not a string)!"; + finishError(error); + delete json; + return; + } + + Networking::JsonCallback callback = new Common::Callback<DropboxListDirectoryRequest, Networking::JsonResponse>(this, &DropboxListDirectoryRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxListDirectoryRequest, Networking::ErrorResponse>(this, &DropboxListDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, DROPBOX_API_LIST_FOLDER_CONTINUE); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("cursor", new Common::JSONValue(responseObject.getVal("cursor")->asString())); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); + } else { + finishListing(_files); + } + + delete json; +} + +void DropboxListDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void DropboxListDirectoryRequest::handle() {} + +void DropboxListDirectoryRequest::restart() { start(); } + +Common::String DropboxListDirectoryRequest::date() const { return _date; } + +void DropboxListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxlistdirectoryrequest.h b/backends/cloud/dropbox/dropboxlistdirectoryrequest.h new file mode 100644 index 0000000000..f031d6198b --- /dev/null +++ b/backends/cloud/dropbox/dropboxlistdirectoryrequest.h @@ -0,0 +1,61 @@ +/* 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 BACKENDS_CLOUD_DROPBOX_DROPBOXLISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXLISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::String _token; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + DropboxListDirectoryRequest(Common::String token, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~DropboxListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxstorage.cpp b/backends/cloud/dropbox/dropboxstorage.cpp new file mode 100644 index 0000000000..c2ca55c083 --- /dev/null +++ b/backends/cloud/dropbox/dropboxstorage.cpp @@ -0,0 +1,199 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/dropbox/dropboxstorage.h" +#include "backends/cloud/dropbox/dropboxcreatedirectoryrequest.h" +#include "backends/cloud/dropbox/dropboxinforequest.h" +#include "backends/cloud/dropbox/dropboxlistdirectoryrequest.h" +#include "backends/cloud/dropbox/dropboxuploadrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_OAUTH2_TOKEN "https://api.dropboxapi.com/oauth2/token" +#define DROPBOX_API_FILES_DOWNLOAD "https://content.dropboxapi.com/2/files/download" + +char *DropboxStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *DropboxStorage::SECRET = nullptr; + +void DropboxStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_DROPBOX_KEY; + SECRET = RELEASE_DROPBOX_SECRET; +#else + Common::String k = ConfMan.get("DROPBOX_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("DROPBOX_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +DropboxStorage::DropboxStorage(Common::String accessToken, Common::String userId): _token(accessToken), _uid(userId) {} + +DropboxStorage::DropboxStorage(Common::String code) { + getAccessToken(code); +} + +DropboxStorage::~DropboxStorage() {} + +void DropboxStorage::getAccessToken(Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + Networking::JsonCallback callback = new Common::Callback<DropboxStorage, Networking::JsonResponse>(this, &DropboxStorage::codeFlowComplete); + Networking::ErrorCallback errorCallback = new Common::Callback<DropboxStorage, Networking::ErrorResponse>(this, &DropboxStorage::codeFlowFailed); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, DROPBOX_OAUTH2_TOKEN); + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void DropboxStorage::codeFlowComplete(Networking::JsonResponse response) { + Common::JSONValue *json = (Common::JSONValue *)response.value; + if (json == nullptr) { + debug(9, "DropboxStorage::codeFlowComplete: got NULL instead of JSON!"); + CloudMan.removeStorage(this); + return; + } + + if (!json->isObject()) { + debug(9, "DropboxStorage::codeFlowComplete: Passed JSON is not an object!"); + CloudMan.removeStorage(this); + delete json; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "DropboxStorage::codeFlowComplete") || + !Networking::CurlJsonRequest::jsonContainsString(result, "uid", "DropboxStorage::codeFlowComplete")) { + warning("DropboxStorage: bad response, no token/uid passed"); + debug(9, "%s", json->stringify(true).c_str()); + CloudMan.removeStorage(this); + } else { + _token = result.getVal("access_token")->asString(); + _uid = result.getVal("uid")->asString(); + ConfMan.removeKey("dropbox_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageDropboxId); + ConfMan.flushToDisk(); + } + + delete json; +} + +void DropboxStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "DropboxStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void DropboxStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain); +} + +Common::String DropboxStorage::name() const { + return "Dropbox"; +} + +Networking::Request *DropboxStorage::listDirectory(Common::String path, ListDirectoryCallback outerCallback, Networking::ErrorCallback errorCallback, bool recursive) { + return addRequest(new DropboxListDirectoryRequest(_token, path, outerCallback, errorCallback, recursive)); +} + +Networking::Request *DropboxStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new DropboxUploadRequest(_token, path, contents, callback, errorCallback)); +} + +Networking::Request *DropboxStorage::streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("path", new Common::JSONValue(path)); + Common::JSONValue value(jsonRequestParameters); + + Networking::CurlRequest *request = new Networking::CurlRequest(nullptr, nullptr, DROPBOX_API_FILES_DOWNLOAD); //TODO: is it OK to pass no callbacks? + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Dropbox-API-Arg: " + Common::JSON::stringify(&value)); + request->addHeader("Content-Type: "); //required to be empty (as we do POST, it's usually app/form-url-encoded) + + Networking::NetworkReadStreamResponse response = request->execute(); + if (callback) + (*callback)(response); + return response.request; // no leak here, response.request == request +} + +Networking::Request *DropboxStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new DropboxCreateDirectoryRequest(_token, path, callback, errorCallback)); +} + +Networking::Request *DropboxStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new DropboxInfoRequest(_token, callback, errorCallback)); +} + +Common::String DropboxStorage::savesDirectoryPath() { return "/saves/"; } + +DropboxStorage *DropboxStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("DropboxStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) { + warning("DropboxStorage: no user_id found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain); + + return new DropboxStorage(accessToken, userId); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxstorage.h b/backends/cloud/dropbox/dropboxstorage.h new file mode 100644 index 0000000000..44fb8a3475 --- /dev/null +++ b/backends/cloud/dropbox/dropboxstorage.h @@ -0,0 +1,101 @@ +/* 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 BACKENDS_CLOUD_DROPBOX_STORAGE_H +#define BACKENDS_CLOUD_DROPBOX_STORAGE_H + +#include "backends/cloud/storage.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxStorage: public Cloud::Storage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _uid; + + /** This private constructor is called from loadFromConfig(). */ + DropboxStorage(Common::String token, Common::String uid); + + void getAccessToken(Common::String code); + void codeFlowComplete(Networking::JsonResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + +public: + /** This constructor uses OAuth code flow to get tokens. */ + DropboxStorage(Common::String code); + virtual ~DropboxStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return DropboxStorage for those. + * @return pointer to the newly created DropboxStorage or 0 if some problem occured. + */ + static DropboxStorage *loadFromConfig(Common::String keyPrefix); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/dropbox/dropboxuploadrequest.cpp b/backends/cloud/dropbox/dropboxuploadrequest.cpp new file mode 100644 index 0000000000..361d8acd97 --- /dev/null +++ b/backends/cloud/dropbox/dropboxuploadrequest.cpp @@ -0,0 +1,204 @@ +/* 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 "backends/cloud/dropbox/dropboxuploadrequest.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace Dropbox { + +#define DROPBOX_API_FILES_UPLOAD "https://content.dropboxapi.com/2/files/upload" +#define DROPBOX_API_FILES_UPLOAD_SESSION "https://content.dropboxapi.com/2/files/upload_session/" + +DropboxUploadRequest::DropboxUploadRequest(Common::String token, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _token(token), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +DropboxUploadRequest::~DropboxUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void DropboxUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (!_contentsStream) { + warning("DropboxUploadRequest: cannot start because stream is invalid"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + if (!_contentsStream->seek(0)) { + warning("DropboxUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _ignoreCallback = false; + + uploadNextPart(); +} + +void DropboxUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + + Common::String url = DROPBOX_API_FILES_UPLOAD_SESSION; + Common::JSONObject jsonRequestParameters; + + if (_contentsStream->pos() == 0 || _sessionId == "") { + if ((uint32)_contentsStream->size() <= UPLOAD_PER_ONE_REQUEST) { + url = DROPBOX_API_FILES_UPLOAD; + jsonRequestParameters.setVal("path", new Common::JSONValue(_savePath)); + jsonRequestParameters.setVal("mode", new Common::JSONValue("overwrite")); + jsonRequestParameters.setVal("autorename", new Common::JSONValue(false)); + jsonRequestParameters.setVal("mute", new Common::JSONValue(false)); + } else { + url += "start"; + jsonRequestParameters.setVal("close", new Common::JSONValue(false)); + } + } else { + if ((uint32)(_contentsStream->size() - _contentsStream->pos()) <= UPLOAD_PER_ONE_REQUEST) { + url += "finish"; + Common::JSONObject jsonCursor, jsonCommit; + jsonCursor.setVal("session_id", new Common::JSONValue(_sessionId)); + jsonCursor.setVal("offset", new Common::JSONValue((long long int)_contentsStream->pos())); + jsonCommit.setVal("path", new Common::JSONValue(_savePath)); + jsonCommit.setVal("mode", new Common::JSONValue("overwrite")); + jsonCommit.setVal("autorename", new Common::JSONValue(false)); + jsonCommit.setVal("mute", new Common::JSONValue(false)); + jsonRequestParameters.setVal("cursor", new Common::JSONValue(jsonCursor)); + jsonRequestParameters.setVal("commit", new Common::JSONValue(jsonCommit)); + } else { + url += "append_v2"; + Common::JSONObject jsonCursor; + jsonCursor.setVal("session_id", new Common::JSONValue(_sessionId)); + jsonCursor.setVal("offset", new Common::JSONValue((long long int)_contentsStream->pos())); + jsonRequestParameters.setVal("cursor", new Common::JSONValue(jsonCursor)); + jsonRequestParameters.setVal("close", new Common::JSONValue(false)); + } + } + + Common::JSONValue value(jsonRequestParameters); + Networking::JsonCallback callback = new Common::Callback<DropboxUploadRequest, Networking::JsonResponse>(this, &DropboxUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<DropboxUploadRequest, Networking::ErrorResponse>(this, &DropboxUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, failureCallback, url); + request->addHeader("Authorization: Bearer " + _token); + request->addHeader("Content-Type: application/octet-stream"); + request->addHeader("Dropbox-API-Arg: " + Common::JSON::stringify(&value)); + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + request->setBuffer(buffer, size); + + _workingRequest = ConnMan.addRequest(request); +} + +void DropboxUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + bool needsFinishRequest = false; + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + //debug(9, "%s", json->stringify(true).c_str()); + + if (object.contains("error") || object.contains("error_summary")) { + if (Networking::CurlJsonRequest::jsonContainsString(object, "error_summary", "DropboxUploadRequest")) { + warning("Dropbox returned error: %s", object.getVal("error_summary")->asString().c_str()); + } + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "path_lower", "DropboxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "server_modified", "DropboxUploadRequest") && + Networking::CurlJsonRequest::jsonContainsIntegerNumber(object, "size", "DropboxUploadRequest")) { + //finished + Common::String path = object.getVal("path_lower")->asString(); + uint32 size = object.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(object.getVal("server_modified")->asString()); + finishUpload(StorageFile(path, size, timestamp, false)); + return; + } + + if (_sessionId == "") { + if (Networking::CurlJsonRequest::jsonContainsString(object, "session_id", "DropboxUploadRequest")) + _sessionId = object.getVal("session_id")->asString(); + needsFinishRequest = true; + } + } + + if (!needsFinishRequest && (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1)) { + warning("DropboxUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void DropboxUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void DropboxUploadRequest::handle() {} + +void DropboxUploadRequest::restart() { start(); } + +void DropboxUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Dropbox +} // End of namespace Cloud diff --git a/backends/cloud/dropbox/dropboxuploadrequest.h b/backends/cloud/dropbox/dropboxuploadrequest.h new file mode 100644 index 0000000000..c1c0047298 --- /dev/null +++ b/backends/cloud/dropbox/dropboxuploadrequest.h @@ -0,0 +1,60 @@ +/* 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 BACKENDS_CLOUD_DROPBOX_DROPBOXUPLOADREQUEST_H +#define BACKENDS_CLOUD_DROPBOX_DROPBOXUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Dropbox { + +class DropboxUploadRequest: public Networking::Request { + Common::String _token; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _sessionId; + + void start(); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + DropboxUploadRequest(Common::String token, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~DropboxUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Dropbox +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/folderdownloadrequest.cpp b/backends/cloud/folderdownloadrequest.cpp new file mode 100644 index 0000000000..3d31beba4b --- /dev/null +++ b/backends/cloud/folderdownloadrequest.cpp @@ -0,0 +1,197 @@ +/* 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 "backends/cloud/folderdownloadrequest.h" +#include "backends/cloud/downloadrequest.h" +#include "backends/cloud/id/iddownloadrequest.h" +#include "common/debug.h" +#include "gui/downloaddialog.h" +#include <backends/networking/curl/connectionmanager.h> + +namespace Cloud { + +FolderDownloadRequest::FolderDownloadRequest(Storage *storage, Storage::FileArrayCallback callback, Networking::ErrorCallback ecb, Common::String remoteDirectoryPath, Common::String localDirectoryPath, bool recursive): + Request(nullptr, ecb), CommandSender(nullptr), _storage(storage), _fileArrayCallback(callback), + _remoteDirectoryPath(remoteDirectoryPath), _localDirectoryPath(localDirectoryPath), _recursive(recursive), + _workingRequest(nullptr), _ignoreCallback(false), _totalFiles(0) { + start(); +} + +FolderDownloadRequest::~FolderDownloadRequest() { + sendCommand(GUI::kDownloadEndedCmd, 0); + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _fileArrayCallback; +} + +void FolderDownloadRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _currentFile = StorageFile(); + _pendingFiles.clear(); + _failedFiles.clear(); + _ignoreCallback = false; + _totalFiles = 0; + _downloadedBytes = _totalBytes = _wasDownloadedBytes = _currentDownloadSpeed = 0; + + //list directory first + _workingRequest = _storage->listDirectory( + _remoteDirectoryPath, + new Common::Callback<FolderDownloadRequest, Storage::ListDirectoryResponse>(this, &FolderDownloadRequest::directoryListedCallback), + new Common::Callback<FolderDownloadRequest, Networking::ErrorResponse>(this, &FolderDownloadRequest::directoryListedErrorCallback), + _recursive + ); +} + +void FolderDownloadRequest::directoryListedCallback(Storage::ListDirectoryResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _pendingFiles = response.value; + + // remove all directories + // non-empty directories would be created by DumpFile, and empty ones are just ignored + for (Common::Array<StorageFile>::iterator i = _pendingFiles.begin(); i != _pendingFiles.end();) + if (i->isDirectory()) + _pendingFiles.erase(i); + else { + _totalBytes += i->size(); + ++i; + } + + _totalFiles = _pendingFiles.size(); + downloadNextFile(); +} + +void FolderDownloadRequest::directoryListedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void FolderDownloadRequest::fileDownloadedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (!response.value) _failedFiles.push_back(_currentFile); + _downloadedBytes += _currentFile.size(); + downloadNextFile(); +} + +void FolderDownloadRequest::fileDownloadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + fileDownloadedCallback(Storage::BoolResponse(error.request, false)); +} + +void FolderDownloadRequest::downloadNextFile() { + do { + if (_pendingFiles.empty()) { + sendCommand(GUI::kDownloadEndedCmd, 0); + finishDownload(_failedFiles); + return; + } + + _currentFile = _pendingFiles.back(); + _pendingFiles.pop_back(); + } while (_currentFile.isDirectory()); // directories are actually removed earlier, in the directoryListedCallback() + + sendCommand(GUI::kDownloadProgressCmd, (int)(getProgress() * 100)); + + Common::String remotePath = _currentFile.path(); + Common::String localPath = remotePath; + if (_remoteDirectoryPath == "" || remotePath.hasPrefix(_remoteDirectoryPath)) { + localPath.erase(0, _remoteDirectoryPath.size()); + if (_remoteDirectoryPath != "" && (_remoteDirectoryPath.lastChar() != '/' && _remoteDirectoryPath.lastChar() != '\\')) + localPath.erase(0, 1); + } else { + warning("FolderDownloadRequest: Can't process the following paths:"); + warning("remote directory: %s", _remoteDirectoryPath.c_str()); + warning("remote file under that directory: %s", remotePath.c_str()); + } + if (_localDirectoryPath != "") { + if (_localDirectoryPath.lastChar() == '/' || _localDirectoryPath.lastChar() == '\\') + localPath = _localDirectoryPath + localPath; + else + localPath = _localDirectoryPath + "/" + localPath; + } + debug(9, "FolderDownloadRequest: %s -> %s", remotePath.c_str(), localPath.c_str()); + _workingRequest = _storage->downloadById( + _currentFile.id(), localPath, + new Common::Callback<FolderDownloadRequest, Storage::BoolResponse>(this, &FolderDownloadRequest::fileDownloadedCallback), + new Common::Callback<FolderDownloadRequest, Networking::ErrorResponse>(this, &FolderDownloadRequest::fileDownloadedErrorCallback) + ); +} + +void FolderDownloadRequest::handle() { + uint32 microsecondsPassed = Networking::ConnectionManager::getCloudRequestsPeriodInMicroseconds(); + uint64 currentDownloadedBytes = getDownloadedBytes(); + uint64 downloadedThisPeriod = currentDownloadedBytes - _wasDownloadedBytes; + _currentDownloadSpeed = downloadedThisPeriod * (1000000L / microsecondsPassed); + _wasDownloadedBytes = currentDownloadedBytes; +} + +void FolderDownloadRequest::restart() { start(); } + +void FolderDownloadRequest::finishDownload(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_fileArrayCallback) + (*_fileArrayCallback)(Storage::FileArrayResponse(this, files)); +} + +double FolderDownloadRequest::getProgress() const { + if (_totalFiles == 0 || _totalBytes == 0) + return 0; + return (double)getDownloadedBytes() / (double)getTotalBytesToDownload(); +} + +uint64 FolderDownloadRequest::getDownloadedBytes() const { + if (_totalFiles == 0) + return 0; + + double currentFileProgress = 0; + DownloadRequest *downloadRequest = dynamic_cast<DownloadRequest *>(_workingRequest); + if (downloadRequest != nullptr) { + currentFileProgress = downloadRequest->getProgress(); + } else { + Id::IdDownloadRequest *idDownloadRequest = dynamic_cast<Id::IdDownloadRequest *>(_workingRequest); + if (idDownloadRequest != nullptr) + currentFileProgress = idDownloadRequest->getProgress(); + } + + return _downloadedBytes + (uint64)(currentFileProgress * _currentFile.size()); +} + +uint64 FolderDownloadRequest::getTotalBytesToDownload() const { + return _totalBytes; +} + +uint64 FolderDownloadRequest::getDownloadSpeed() const { + return _currentDownloadSpeed; +} + +} // End of namespace Cloud diff --git a/backends/cloud/folderdownloadrequest.h b/backends/cloud/folderdownloadrequest.h new file mode 100644 index 0000000000..3cb6677d9f --- /dev/null +++ b/backends/cloud/folderdownloadrequest.h @@ -0,0 +1,79 @@ +/* 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 BACKENDS_CLOUD_FOLDERDOWNLOADREQUEST_H +#define BACKENDS_CLOUD_FOLDERDOWNLOADREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/cloud/storage.h" +#include "gui/object.h" + +namespace Cloud { + +class FolderDownloadRequest: public Networking::Request, public GUI::CommandSender { + Storage *_storage; + Storage::FileArrayCallback _fileArrayCallback; + Common::String _remoteDirectoryPath, _localDirectoryPath; + bool _recursive; + Common::Array<StorageFile> _pendingFiles, _failedFiles; + StorageFile _currentFile; + Request *_workingRequest; + bool _ignoreCallback; + uint32 _totalFiles; + uint64 _downloadedBytes, _totalBytes, _wasDownloadedBytes, _currentDownloadSpeed; + + void start(); + void directoryListedCallback(Storage::ListDirectoryResponse response); + void directoryListedErrorCallback(Networking::ErrorResponse error); + void fileDownloadedCallback(Storage::BoolResponse response); + void fileDownloadedErrorCallback(Networking::ErrorResponse error); + void downloadNextFile(); + void finishDownload(Common::Array<StorageFile> &files); +public: + FolderDownloadRequest(Storage *storage, Storage::FileArrayCallback callback, Networking::ErrorCallback ecb, Common::String remoteDirectoryPath, Common::String localDirectoryPath, bool recursive); + virtual ~FolderDownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Returns a number of downloaded bytes. */ + uint64 getDownloadedBytes() const; + + /** Returns a total number of bytes to download. */ + uint64 getTotalBytesToDownload() const; + + /** Returns average download speed for the last second. */ + uint64 getDownloadSpeed() const; + + /** Returns remote directory path. */ + Common::String getRemotePath() const { return _remoteDirectoryPath; } + + /** Returns local directory path. */ + Common::String getLocalPath() const { return _localDirectoryPath; } +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp new file mode 100644 index 0000000000..24db75b077 --- /dev/null +++ b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.cpp @@ -0,0 +1,163 @@ +/* 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 "backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "googledrivetokenrefresher.h" + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files?spaces=drive&fields=files%28id,mimeType,modifiedTime,name,size%29,nextPageToken&orderBy=folder,name" +//files(id,mimeType,modifiedTime,name,size),nextPageToken + +GoogleDriveListDirectoryByIdRequest::GoogleDriveListDirectoryByIdRequest(GoogleDriveStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedId(id), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +GoogleDriveListDirectoryByIdRequest::~GoogleDriveListDirectoryByIdRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void GoogleDriveListDirectoryByIdRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _files.clear(); + _ignoreCallback = false; + + makeRequest(""); +} + +void GoogleDriveListDirectoryByIdRequest::makeRequest(Common::String pageToken) { + Common::String url = GOOGLEDRIVE_API_FILES; + if (pageToken != "") + url += "&pageToken=" + pageToken; + url += "&q=%27" + _requestedId + "%27+in+parents"; + + Networking::JsonCallback callback = new Common::Callback<GoogleDriveListDirectoryByIdRequest, Networking::JsonResponse>(this, &GoogleDriveListDirectoryByIdRequest::responseCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveListDirectoryByIdRequest, Networking::ErrorResponse>(this, &GoogleDriveListDirectoryByIdRequest::errorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void GoogleDriveListDirectoryByIdRequest::responseCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) { + delete response.value; + return; + } + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json) { + Common::JSONObject responseObject = json->asObject(); + + ///debug("%s", json->stringify(true).c_str()); + + if (responseObject.contains("error") || responseObject.contains("error_summary")) { + warning("GoogleDrive returned error: %s", responseObject.getVal("error_summary")->asString().c_str()); + error.failed = true; + error.response = json->stringify(); + finishError(error); + delete json; + return; + } + + //TODO: check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + + if (responseObject.contains("files") && responseObject.getVal("files")->isArray()) { + Common::JSONArray items = responseObject.getVal("files")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + Common::JSONObject item = items[i]->asObject(); + Common::String id = item.getVal("id")->asString(); + Common::String name = item.getVal("name")->asString(); + bool isDirectory = (item.getVal("mimeType")->asString() == "application/vnd.google-apps.folder"); + uint32 size = 0, timestamp = 0; + if (item.contains("size") && item.getVal("size")->isString()) + size = item.getVal("size")->asString().asUint64(); + if (item.contains("modifiedTime") && item.getVal("modifiedTime")->isString()) + timestamp = ISO8601::convertToTimestamp(item.getVal("modifiedTime")->asString()); + + //as we list directory by id, we can't determine full path for the file, so we leave it empty + _files.push_back(StorageFile(id, "", name, size, timestamp, isDirectory)); + } + } + + bool hasMore = (responseObject.contains("nextPageToken")); + + if (hasMore) { + Common::String token = responseObject.getVal("nextPageToken")->asString(); + makeRequest(token); + } else { + finishListing(_files); + } + } else { + warning("null, not json"); + error.failed = true; + finishError(error); + } + + delete json; +} + +void GoogleDriveListDirectoryByIdRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void GoogleDriveListDirectoryByIdRequest::handle() {} + +void GoogleDriveListDirectoryByIdRequest::restart() { start(); } + +Common::String GoogleDriveListDirectoryByIdRequest::date() const { return _date; } + +void GoogleDriveListDirectoryByIdRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h new file mode 100644 index 0000000000..fbdad16372 --- /dev/null +++ b/backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h @@ -0,0 +1,63 @@ +/* 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 BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVELISTDIRECTORYBYIDREQUEST_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVELISTDIRECTORYBYIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage; + +class GoogleDriveListDirectoryByIdRequest: public Networking::Request { + Common::String _requestedId; + GoogleDriveStorage *_storage; + + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void makeRequest(Common::String pageToken); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + GoogleDriveListDirectoryByIdRequest(GoogleDriveStorage *storage, Common::String id, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb); + virtual ~GoogleDriveListDirectoryByIdRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivestorage.cpp b/backends/cloud/googledrive/googledrivestorage.cpp new file mode 100644 index 0000000000..ec4c84400c --- /dev/null +++ b/backends/cloud/googledrive/googledrivestorage.cpp @@ -0,0 +1,350 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/googledrive/googledrivetokenrefresher.h" +#include "backends/cloud/googledrive/googledrivelistdirectorybyidrequest.h" +#include "backends/cloud/googledrive/googledriveuploadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include "common/debug.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_OAUTH2_TOKEN "https://accounts.google.com/o/oauth2/token" +#define GOOGLEDRIVE_API_FILES_ALT_MEDIA "https://www.googleapis.com/drive/v3/files/%s?alt=media" +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/drive/v3/files" +#define GOOGLEDRIVE_API_ABOUT "https://www.googleapis.com/drive/v3/about?fields=storageQuota,user" + +char *GoogleDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *GoogleDriveStorage::SECRET = nullptr; + +void GoogleDriveStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_GOOGLE_DRIVE_KEY; + SECRET = RELEASE_GOOGLE_DRIVE_SECRET; +#else + Common::String k = ConfMan.get("GOOGLE_DRIVE_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("GOOGLE_DRIVE_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +GoogleDriveStorage::GoogleDriveStorage(Common::String accessToken, Common::String refreshToken): + _token(accessToken), _refreshToken(refreshToken) {} + +GoogleDriveStorage::GoogleDriveStorage(Common::String code) { + getAccessToken( + new Common::Callback<GoogleDriveStorage, BoolResponse>(this, &GoogleDriveStorage::codeFlowComplete), + new Common::Callback<GoogleDriveStorage, Networking::ErrorResponse>(this, &GoogleDriveStorage::codeFlowFailed), + code + ); +} + +GoogleDriveStorage::~GoogleDriveStorage() {} + +void GoogleDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("GoogleDriveStorage: no refresh token available to get new access token."); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, GOOGLEDRIVE_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + } else { + request->addPostField("refresh_token=" + _refreshToken); + request->addPostField("grant_type=refresh_token"); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void GoogleDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "GoogleDriveStorage")) { + warning("GoogleDriveStorage: bad response, no token passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "GoogleDriveStorage")) + warning("GoogleDriveStorage: no refresh_token passed"); + else + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void GoogleDriveStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("GoogleDriveStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + ConfMan.removeKey("googledrive_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageGoogleDriveId); + ConfMan.flushToDisk(); +} + +void GoogleDriveStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "GoogleDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void GoogleDriveStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String GoogleDriveStorage::name() const { + return "Google Drive"; +} + +void GoogleDriveStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 0; + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "user", "GoogleDriveStorage::infoInnerCallback") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("user"), "GoogleDriveStorage::infoInnerCallback")) { + //"me":true, "kind":"drive#user","photoLink": "", + //"displayName":"Alexander Tkachev","emailAddress":"alexander@tkachov.ru","permissionId":"" + Common::JSONObject user = info.getVal("user")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "permissionId", "GoogleDriveStorage::infoInnerCallback")) + uid = user.getVal("permissionId")->asString(); //not sure it's user's id, but who cares anyway? + if (Networking::CurlJsonRequest::jsonContainsString(user, "displayName", "GoogleDriveStorage::infoInnerCallback")) + name = user.getVal("displayName")->asString(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "emailAddress", "GoogleDriveStorage::infoInnerCallback")) + email = user.getVal("emailAddress")->asString(); + } + + if (Networking::CurlJsonRequest::jsonContainsAttribute(info, "storageQuota", "GoogleDriveStorage::infoInnerCallback") && + Networking::CurlJsonRequest::jsonIsObject(info.getVal("storageQuota"), "GoogleDriveStorage::infoInnerCallback")) { + //"usageInDrive":"6332462","limit":"18253611008","usage":"6332462","usageInDriveTrash":"0" + Common::JSONObject storageQuota = info.getVal("storageQuota")->asObject(); + + if (Networking::CurlJsonRequest::jsonContainsString(storageQuota, "usage", "GoogleDriveStorage::infoInnerCallback")) { + Common::String usage = storageQuota.getVal("usage")->asString(); + quotaUsed = usage.asUint64(); + } + + if (Networking::CurlJsonRequest::jsonContainsString(storageQuota, "limit", "GoogleDriveStorage::infoInnerCallback")) { + Common::String limit = storageQuota.getVal("limit")->asString(); + quotaAllocated = limit.asUint64(); + } + } + + CloudMan.setStorageUsername(kStorageGoogleDriveId, email); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +void GoogleDriveStorage::createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("GoogleDriveStorage::createDirectoryInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (outerCallback) { + if (Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage::createDirectoryInnerCallback")) { + Common::JSONObject info = json->asObject(); + (*outerCallback)(BoolResponse(nullptr, info.contains("id"))); + } else { + (*outerCallback)(BoolResponse(nullptr, false)); + } + delete outerCallback; + } + + delete json; +} + +Networking::Request *GoogleDriveStorage::listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<GoogleDriveStorage, FileArrayResponse>(this, &GoogleDriveStorage::printFiles); + return addRequest(new GoogleDriveListDirectoryByIdRequest(this, id, callback, errorCallback)); +} + +Networking::Request *GoogleDriveStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new GoogleDriveUploadRequest(this, path, contents, callback, errorCallback)); +} + +Networking::Request *GoogleDriveStorage::streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + if (callback) { + Common::String url = Common::String::format(GOOGLEDRIVE_API_FILES_ALT_MEDIA, ConnMan.urlEncode(id).c_str()); + Common::String header = "Authorization: Bearer " + _token; + curl_slist *headersList = curl_slist_append(nullptr, header.c_str()); + Networking::NetworkReadStream *stream = new Networking::NetworkReadStream(url.c_str(), headersList, ""); + (*callback)(Networking::NetworkReadStreamResponse(nullptr, stream)); + } + delete callback; + delete errorCallback; + return nullptr; +} + +void GoogleDriveStorage::printInfo(StorageInfoResponse response) { + debug(9, "\nGoogleDriveStorage: user info:"); + debug(9, "\tname: %s", response.value.name().c_str()); + debug(9, "\temail: %s", response.value.email().c_str()); + debug(9, "\tdisk usage: %lu/%lu", response.value.used(), response.value.available()); +} + +Networking::Request *GoogleDriveStorage::createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + + Common::String url = GOOGLEDRIVE_API_FILES; + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::createDirectoryInnerCallback, callback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONArray parentsArray; + parentsArray.push_back(new Common::JSONValue(parentId)); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("mimeType", new Common::JSONValue("application/vnd.google-apps.folder")); + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("parents", new Common::JSONValue(parentsArray)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + return addRequest(request); +} + +Networking::Request *GoogleDriveStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + if (!callback) + callback = new Common::Callback<GoogleDriveStorage, StorageInfoResponse>(this, &GoogleDriveStorage::printInfo); + Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, StorageInfoResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(this, innerCallback, errorCallback, GOOGLEDRIVE_API_ABOUT); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Common::String GoogleDriveStorage::savesDirectoryPath() { return "scummvm/saves/"; } + +GoogleDriveStorage *GoogleDriveStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("GoogleDriveStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("GoogleDriveStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new GoogleDriveStorage(accessToken, refreshToken); +} + +Common::String GoogleDriveStorage::getRootDirectoryId() { + return "root"; +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivestorage.h b/backends/cloud/googledrive/googledrivestorage.h new file mode 100644 index 0000000000..457369dc90 --- /dev/null +++ b/backends/cloud/googledrive/googledrivestorage.h @@ -0,0 +1,118 @@ +/* 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 BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVESTORAGE_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVESTORAGE_H + +#include "backends/cloud/id/idstorage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage: public Id::IdStorage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + GoogleDriveStorage(Common::String token, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + /** Returns bool based on JSON response from cloud. */ + void createDirectoryInnerCallback(BoolCallback outerCallback, Networking::JsonResponse json); + + void printInfo(StorageInfoResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + GoogleDriveStorage(Common::String code); + virtual ~GoogleDriveStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns Array<StorageFile> - the list of files. */ + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return GoogleDriveStorage for those. + * @return pointer to the newly created GoogleDriveStorage or 0 if some problem occured. + */ + static GoogleDriveStorage *loadFromConfig(Common::String keyPrefix); + + virtual Common::String getRootDirectoryId(); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledrivetokenrefresher.cpp b/backends/cloud/googledrive/googledrivetokenrefresher.cpp new file mode 100644 index 0000000000..a8d9d55bad --- /dev/null +++ b/backends/cloud/googledrive/googledrivetokenrefresher.cpp @@ -0,0 +1,126 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/googledrive/googledrivetokenrefresher.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace GoogleDrive { + +GoogleDriveTokenRefresher::GoogleDriveTokenRefresher(GoogleDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +GoogleDriveTokenRefresher::~GoogleDriveTokenRefresher() {} + +void GoogleDriveTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("GoogleDriveTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: Bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void GoogleDriveTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "GoogleDriveTokenRefresher")) { + Common::JSONObject result = json->asObject(); + long httpResponseCode = -1; + if (result.contains("error") && jsonIsObject(result.getVal("error"), "GoogleDriveTokenRefresher")) { + //new token needed => request token & then retry original request + if (_stream) { + httpResponseCode = _stream->httpResponseCode(); + debug(9, "GoogleDriveTokenRefresher: code = %ld", httpResponseCode); + } + + Common::JSONObject error = result.getVal("error")->asObject(); + bool irrecoverable = true; + + uint32 code = -1; + Common::String message; + if (jsonContainsIntegerNumber(error, "code", "GoogleDriveTokenRefresher")) { + code = error.getVal("code")->asIntegerNumber(); + debug(9, "GoogleDriveTokenRefresher: code = %u", code); + } + + if (jsonContainsString(error, "message", "GoogleDriveTokenRefresher")) { + message = error.getVal("message")->asString(); + debug(9, "GoogleDriveTokenRefresher: message = %s", message.c_str()); + } + + if (code == 401 || message == "Invalid Credentials") + irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpResponseCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<GoogleDriveTokenRefresher, Storage::BoolResponse>(this, &GoogleDriveTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void GoogleDriveTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void GoogleDriveTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledrivetokenrefresher.h b/backends/cloud/googledrive/googledrivetokenrefresher.h new file mode 100644 index 0000000000..4ec4c84340 --- /dev/null +++ b/backends/cloud/googledrive/googledrivetokenrefresher.h @@ -0,0 +1,52 @@ +/* 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 BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVETOKENREFRESHER_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVETOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace GoogleDrive { + +class GoogleDriveStorage; + +class GoogleDriveTokenRefresher: public Networking::CurlJsonRequest { + GoogleDriveStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); +public: + GoogleDriveTokenRefresher(GoogleDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~GoogleDriveTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/googledrive/googledriveuploadrequest.cpp b/backends/cloud/googledrive/googledriveuploadrequest.cpp new file mode 100644 index 0000000000..f921f5c96d --- /dev/null +++ b/backends/cloud/googledrive/googledriveuploadrequest.cpp @@ -0,0 +1,353 @@ +/* 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 "backends/cloud/googledrive/googledriveuploadrequest.h" +#include "backends/cloud/googledrive/googledrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "googledrivetokenrefresher.h" + +namespace Cloud { +namespace GoogleDrive { + +#define GOOGLEDRIVE_API_FILES "https://www.googleapis.com/upload/drive/v3/files" + +GoogleDriveUploadRequest::GoogleDriveUploadRequest(GoogleDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +GoogleDriveUploadRequest::~GoogleDriveUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void GoogleDriveUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (_contentsStream == nullptr || !_contentsStream->seek(0)) { + warning("GoogleDriveUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _resolvedId = ""; //used to update file contents + _parentId = ""; //used to create file within parent directory + _serverReceivedBytes = 0; + _ignoreCallback = false; + + resolveId(); +} + +void GoogleDriveUploadRequest::resolveId() { + //check whether such file already exists + Storage::UploadCallback innerCallback = new Common::Callback<GoogleDriveUploadRequest, Storage::UploadResponse>(this, &GoogleDriveUploadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_savePath, innerCallback, innerErrorCallback); +} + +void GoogleDriveUploadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + _resolvedId = response.value.id(); + startUpload(); +} + +void GoogleDriveUploadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //not resolved => error or no such file + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + _parentId = parentId; + startUpload(); + return; + } + + finishError(error); +} + +void GoogleDriveUploadRequest::startUpload() { + Common::String name = _savePath; + for (uint32 i = name.size(); i > 0; --i) { + if (name[i - 1] == '/' || name[i - 1] == '\\') { + name.erase(0, i); + break; + } + } + + Common::String url = GOOGLEDRIVE_API_FILES; + if (_resolvedId != "") + url += "/" + ConnMan.urlEncode(_resolvedId); + url += "?uploadType=resumable&fields=id,mimeType,modifiedTime,name,size"; + Networking::JsonCallback callback = new Common::Callback<GoogleDriveUploadRequest, Networking::JsonResponse>(this, &GoogleDriveUploadRequest::startUploadCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::startUploadErrorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->addHeader("Content-Type: application/json"); + if (_resolvedId != "") + request->usePatch(); + + Common::JSONObject jsonRequestParameters; + if (_resolvedId != "") { + jsonRequestParameters.setVal("id", new Common::JSONValue(_resolvedId)); + } else { + Common::JSONArray parentsArray; + parentsArray.push_back(new Common::JSONValue(_parentId)); + jsonRequestParameters.setVal("parents", new Common::JSONValue(parentsArray)); + } + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void GoogleDriveUploadRequest::startUploadCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + Common::String headers = stream->responseHeaders(); + if (code == 200) { + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "Location: "); + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + 10; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + _uploadUrl = result; + uploadNextPart(); + return; + } + } + + error.httpResponseCode = code; + } + } + + Common::JSONValue *json = response.value; + delete json; + + finishError(error); +} + +void GoogleDriveUploadRequest::startUploadErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void GoogleDriveUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + Common::String url = _uploadUrl; + + Networking::JsonCallback callback = new Common::Callback<GoogleDriveUploadRequest, Networking::JsonResponse>(this, &GoogleDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<GoogleDriveUploadRequest, Networking::ErrorResponse>(this, &GoogleDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new GoogleDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->usePut(); + + uint32 oldPos = _contentsStream->pos(); + if (oldPos != _serverReceivedBytes) { + if (!_contentsStream->seek(_serverReceivedBytes)) { + warning("GoogleDriveUploadRequest: cannot upload because stream couldn't seek(%lu)", _serverReceivedBytes); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + oldPos = _serverReceivedBytes; + } + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + if (size != 0) + request->setBuffer(buffer, size); + + if (_uploadUrl != "") { + if (_contentsStream->pos() == 0) + request->addHeader(Common::String::format("Content-Length: 0")); + else + request->addHeader(Common::String::format("Content-Range: bytes %u-%u/%u", oldPos, _contentsStream->pos() - 1, _contentsStream->size())); + } + + _workingRequest = ConnMan.addRequest(request); +} + +bool GoogleDriveUploadRequest::handleHttp308(const Networking::NetworkReadStream *stream) { + //308 Resume Incomplete, with Range: X-Y header + if (!stream) + return false; + if (stream->httpResponseCode() != 308) + return false; //seriously + + Common::String headers = stream->responseHeaders(); + const char *cstr = headers.c_str(); + for (int rangeTry = 0; rangeTry < 2; ++rangeTry) { + const char *needle = (rangeTry == 0 ? "Range: 0-" : "Range: bytes=0-"); + uint32 needleLength = (rangeTry == 0 ? 9 : 15); + + const char *position = strstr(cstr, needle); //if it lost the first part, I refuse to talk with it + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + needleLength; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + _serverReceivedBytes = result.asUint64() + 1; + uploadNextPart(); + return true; + } + } + + return false; +} + +void GoogleDriveUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + error.httpResponseCode = code; + if (code == 308 && handleHttp308(stream)) { + delete (Common::JSONValue *)response.value; + return; + } + } + } + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + if (object.contains("error")) { + warning("GoogleDrive returned error: %s", json->stringify(true).c_str()); + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "id", "GoogleDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "name", "GoogleDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "mimeType", "GoogleDriveUploadRequest")) { + //finished + Common::String id = object.getVal("id")->asString(); + Common::String name = object.getVal("name")->asString(); + bool isDirectory = (object.getVal("mimeType")->asString() == "application/vnd.google-apps.folder"); + uint32 size = 0, timestamp = 0; + if (Networking::CurlJsonRequest::jsonContainsString(object, "size", "GoogleDriveUploadRequest", true)) + size = object.getVal("size")->asString().asUint64(); + if (Networking::CurlJsonRequest::jsonContainsString(object, "modifiedTime", "GoogleDriveUploadRequest", true)) + timestamp = ISO8601::convertToTimestamp(object.getVal("modifiedTime")->asString()); + + finishUpload(StorageFile(id, _savePath, name, size, timestamp, isDirectory)); + return; + } + } + + if (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1) { + warning("GoogleDriveUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void GoogleDriveUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)error.request; + if (rq) { + const Networking::NetworkReadStream *stream = rq->getNetworkReadStream(); + if (stream) { + long code = stream->httpResponseCode(); + if (code == 308 && handleHttp308(stream)) { + return; + } + } + } + + finishError(error); +} + +void GoogleDriveUploadRequest::handle() {} + +void GoogleDriveUploadRequest::restart() { start(); } + +void GoogleDriveUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace GoogleDrive +} // End of namespace Cloud diff --git a/backends/cloud/googledrive/googledriveuploadrequest.h b/backends/cloud/googledrive/googledriveuploadrequest.h new file mode 100644 index 0000000000..d1775dfc66 --- /dev/null +++ b/backends/cloud/googledrive/googledriveuploadrequest.h @@ -0,0 +1,70 @@ +/* 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 BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVEUPLOADREQUEST_H +#define BACKENDS_CLOUD_GOOGLEDRIVE_GOOGLEDRIVEUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace GoogleDrive { +class GoogleDriveStorage; + +class GoogleDriveUploadRequest: public Networking::Request { + GoogleDriveStorage *_storage; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _resolvedId, _parentId; + Common::String _uploadUrl; + uint64 _serverReceivedBytes; + + void start(); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void startUpload(); + void startUploadCallback(Networking::JsonResponse response); + void startUploadErrorCallback(Networking::ErrorResponse error); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + bool handleHttp308(const Networking::NetworkReadStream *stream); + void finishUpload(StorageFile status); + +public: + GoogleDriveUploadRequest(GoogleDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~GoogleDriveUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace GoogleDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idcreatedirectoryrequest.cpp b/backends/cloud/id/idcreatedirectoryrequest.cpp new file mode 100644 index 0000000000..86b93cc1f5 --- /dev/null +++ b/backends/cloud/id/idcreatedirectoryrequest.cpp @@ -0,0 +1,163 @@ +/* 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 "backends/cloud/id/idcreatedirectoryrequest.h" +#include "backends/cloud/id/idstorage.h" +#include "common/debug.h" + +namespace Cloud { +namespace Id { + +IdCreateDirectoryRequest::IdCreateDirectoryRequest(IdStorage *storage, Common::String parentPath, Common::String directoryName, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), + _requestedParentPath(parentPath), _requestedDirectoryName(directoryName), _storage(storage), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdCreateDirectoryRequest::~IdCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void IdCreateDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //the only exception when we create parent folder - is when it's ScummVM/ base folder + Common::String prefix = _requestedParentPath; + if (prefix.size() > 7) + prefix.erase(7); + if (prefix.equalsIgnoreCase("ScummVM")) { + Storage::BoolCallback callback = new Common::Callback<IdCreateDirectoryRequest, Storage::BoolResponse>(this, &IdCreateDirectoryRequest::createdBaseDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::createdBaseDirectoryErrorCallback); + _workingRequest = _storage->createDirectory("ScummVM", callback, failureCallback); + return; + } + + resolveId(); +} + +void IdCreateDirectoryRequest::createdBaseDirectoryCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + resolveId(); +} + +void IdCreateDirectoryRequest::createdBaseDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdCreateDirectoryRequest::resolveId() { + //check whether such folder already exists + Storage::UploadCallback innerCallback = new Common::Callback<IdCreateDirectoryRequest, Storage::UploadResponse>(this, &IdCreateDirectoryRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::idResolveFailedCallback); + Common::String path = _requestedParentPath; + if (_requestedParentPath != "") + path += "/"; + path += _requestedDirectoryName; + _workingRequest = _storage->resolveFileId(path, innerCallback, innerErrorCallback); +} + +void IdCreateDirectoryRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + //resolved => folder already exists + finishCreation(false); +} + +void IdCreateDirectoryRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + + //not resolved => folder not exists + if (error.response.contains("no such file found in its parent directory")) { + //parent's id after the '\n' + Common::String parentId = error.response; + for (uint32 i = 0; i < parentId.size(); ++i) + if (parentId[i] == '\n') { + parentId.erase(0, i + 1); + break; + } + + Storage::BoolCallback callback = new Common::Callback<IdCreateDirectoryRequest, Storage::BoolResponse>(this, &IdCreateDirectoryRequest::createdDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdCreateDirectoryRequest, Networking::ErrorResponse>(this, &IdCreateDirectoryRequest::createdDirectoryErrorCallback); + _workingRequest = _storage->createDirectoryWithParentId(parentId, _requestedDirectoryName, callback, failureCallback); + return; + } + + finishError(error); +} + +void IdCreateDirectoryRequest::createdDirectoryCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + finishCreation(response.value); +} + +void IdCreateDirectoryRequest::createdDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdCreateDirectoryRequest::handle() {} + +void IdCreateDirectoryRequest::restart() { start(); } + +Common::String IdCreateDirectoryRequest::date() const { return _date; } + +void IdCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idcreatedirectoryrequest.h b/backends/cloud/id/idcreatedirectoryrequest.h new file mode 100644 index 0000000000..b84918eb5e --- /dev/null +++ b/backends/cloud/id/idcreatedirectoryrequest.h @@ -0,0 +1,65 @@ +/* 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 BACKENDS_CLOUD_ID_IDCREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ID_IDCREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdCreateDirectoryRequest: public Networking::Request { + Common::String _requestedParentPath; + Common::String _requestedDirectoryName; + IdStorage *_storage; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void createdBaseDirectoryCallback(Storage::BoolResponse response); + void createdBaseDirectoryErrorCallback(Networking::ErrorResponse error); + void resolveId(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void createdDirectoryCallback(Storage::BoolResponse response); + void createdDirectoryErrorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + IdCreateDirectoryRequest(IdStorage *storage, Common::String parentPath, Common::String directoryName, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~IdCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/iddownloadrequest.cpp b/backends/cloud/id/iddownloadrequest.cpp new file mode 100644 index 0000000000..0652091b14 --- /dev/null +++ b/backends/cloud/id/iddownloadrequest.cpp @@ -0,0 +1,108 @@ +/* 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 "backends/cloud/id/iddownloadrequest.h" +#include "backends/cloud/id/idstorage.h" +#include "backends/cloud/downloadrequest.h" + +namespace Cloud { +namespace Id { + +IdDownloadRequest::IdDownloadRequest(IdStorage *storage, Common::String remotePath, Common::String localPath, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedFile(remotePath), _requestedLocalFile(localPath), _storage(storage), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdDownloadRequest::~IdDownloadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void IdDownloadRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //find file's id + Storage::UploadCallback innerCallback = new Common::Callback<IdDownloadRequest, Storage::UploadResponse>(this, &IdDownloadRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdDownloadRequest, Networking::ErrorResponse>(this, &IdDownloadRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_requestedFile, innerCallback, innerErrorCallback); +} + +void IdDownloadRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Storage::BoolCallback innerCallback = new Common::Callback<IdDownloadRequest, Storage::BoolResponse>(this, &IdDownloadRequest::downloadCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdDownloadRequest, Networking::ErrorResponse>(this, &IdDownloadRequest::downloadErrorCallback); + _workingRequest = _storage->downloadById(response.value.id(), _requestedLocalFile, innerCallback, innerErrorCallback); +} + +void IdDownloadRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdDownloadRequest::downloadCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishDownload(response.value); +} + +void IdDownloadRequest::downloadErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdDownloadRequest::handle() {} + +void IdDownloadRequest::restart() { start(); } + +void IdDownloadRequest::finishDownload(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +double IdDownloadRequest::getProgress() const { + DownloadRequest *downloadRequest = dynamic_cast<DownloadRequest *>(_workingRequest); + if (downloadRequest == nullptr) + return 0; // resolving id still + + // id resolve is 10 % and download is the other 90 % + return 0.1 + 0.9 * downloadRequest->getProgress(); // downloading +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/iddownloadrequest.h b/backends/cloud/id/iddownloadrequest.h new file mode 100644 index 0000000000..9c235176f9 --- /dev/null +++ b/backends/cloud/id/iddownloadrequest.h @@ -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. + * + */ + +#ifndef BACKENDS_CLOUD_ID_IDDOWNLOADREQUEST_H +#define BACKENDS_CLOUD_ID_IDDOWNLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdDownloadRequest: public Networking::Request { + Common::String _requestedFile, _requestedLocalFile; + IdStorage *_storage; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void downloadCallback(Storage::BoolResponse response); + void downloadErrorCallback(Networking::ErrorResponse error); + void finishDownload(bool success); +public: + IdDownloadRequest(IdStorage *storage, Common::String remotePath, Common::String localPath, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~IdDownloadRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idlistdirectoryrequest.cpp b/backends/cloud/id/idlistdirectoryrequest.cpp new file mode 100644 index 0000000000..d56d5f5049 --- /dev/null +++ b/backends/cloud/id/idlistdirectoryrequest.cpp @@ -0,0 +1,141 @@ +/* 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 "backends/cloud/id/idlistdirectoryrequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdListDirectoryRequest::IdListDirectoryRequest(IdStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _requestedRecursive(recursive), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdListDirectoryRequest::~IdListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void IdListDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _files.clear(); + _directoriesQueue.clear(); + _currentDirectory = StorageFile(); + _ignoreCallback = false; + + //find out that directory's id + Storage::UploadCallback innerCallback = new Common::Callback<IdListDirectoryRequest, Storage::UploadResponse>(this, &IdListDirectoryRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdListDirectoryRequest, Networking::ErrorResponse>(this, &IdListDirectoryRequest::idResolveErrorCallback); + _workingRequest = _storage->resolveFileId(_requestedPath, innerCallback, innerErrorCallback); +} + +void IdListDirectoryRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + StorageFile directory = response.value; + directory.setPath(_requestedPath); + _directoriesQueue.push_back(directory); + listNextDirectory(); +} + +void IdListDirectoryRequest::idResolveErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdListDirectoryRequest::listNextDirectory() { + if (_directoriesQueue.empty()) { + finishListing(_files); + return; + } + + _currentDirectory = _directoriesQueue.back(); + _directoriesQueue.pop_back(); + + Storage::FileArrayCallback callback = new Common::Callback<IdListDirectoryRequest, Storage::FileArrayResponse>(this, &IdListDirectoryRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdListDirectoryRequest, Networking::ErrorResponse>(this, &IdListDirectoryRequest::listedDirectoryErrorCallback); + _workingRequest = _storage->listDirectoryById(_currentDirectory.id(), callback, failureCallback); +} + +void IdListDirectoryRequest::listedDirectoryCallback(Storage::FileArrayResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (response.request) + _date = response.request->date(); + + for (uint32 i = 0; i < response.value.size(); ++i) { + StorageFile &file = response.value[i]; + Common::String path = _currentDirectory.path(); + if (path.size() && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + path += file.name(); + file.setPath(path); + _files.push_back(file); + if (_requestedRecursive && file.isDirectory()) { + _directoriesQueue.push_back(file); + } + } + + listNextDirectory(); +} + +void IdListDirectoryRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void IdListDirectoryRequest::handle() {} + +void IdListDirectoryRequest::restart() { start(); } + +Common::String IdListDirectoryRequest::date() const { return _date; } + +void IdListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idlistdirectoryrequest.h b/backends/cloud/id/idlistdirectoryrequest.h new file mode 100644 index 0000000000..f3d49d2043 --- /dev/null +++ b/backends/cloud/id/idlistdirectoryrequest.h @@ -0,0 +1,66 @@ +/* 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 BACKENDS_CLOUD_ID_IDLISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ID_IDLISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + IdStorage *_storage; + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Common::Array<StorageFile> _directoriesQueue; + StorageFile _currentDirectory; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveErrorCallback(Networking::ErrorResponse error); + void listNextDirectory(); + void listedDirectoryCallback(Storage::FileArrayResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void finishListing(Common::Array<StorageFile> &files); +public: + IdListDirectoryRequest(IdStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~IdListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idresolveidrequest.cpp b/backends/cloud/id/idresolveidrequest.cpp new file mode 100644 index 0000000000..6039545b7e --- /dev/null +++ b/backends/cloud/id/idresolveidrequest.cpp @@ -0,0 +1,136 @@ +/* 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 "backends/cloud/id/idresolveidrequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdResolveIdRequest::IdResolveIdRequest(IdStorage *storage, Common::String path, Storage::UploadCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _storage(storage), _uploadCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdResolveIdRequest::~IdResolveIdRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _uploadCallback; +} + +void IdResolveIdRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _currentDirectory = ""; + _currentDirectoryId = _storage->getRootDirectoryId(); + _ignoreCallback = false; + + listNextDirectory(StorageFile(_currentDirectoryId, 0, 0, true)); +} + +void IdResolveIdRequest::listNextDirectory(StorageFile fileToReturn) { + if (_currentDirectory.equalsIgnoreCase(_requestedPath)) { + finishFile(fileToReturn); + return; + } + + Storage::FileArrayCallback callback = new Common::Callback<IdResolveIdRequest, Storage::FileArrayResponse>(this, &IdResolveIdRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<IdResolveIdRequest, Networking::ErrorResponse>(this, &IdResolveIdRequest::listedDirectoryErrorCallback); + _workingRequest = _storage->listDirectoryById(_currentDirectoryId, callback, failureCallback); +} + +void IdResolveIdRequest::listedDirectoryCallback(Storage::FileArrayResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Common::String currentLevelName = _requestedPath; + ///debug(9, "'%s'", currentLevelName.c_str()); + if (_currentDirectory.size()) + currentLevelName.erase(0, _currentDirectory.size()); + if (currentLevelName.size() && (currentLevelName[0] == '/' || currentLevelName[0] == '\\')) + currentLevelName.erase(0, 1); + ///debug(9, "'%s'", currentLevelName.c_str()); + for (uint32 i = 0; i < currentLevelName.size(); ++i) { + if (currentLevelName[i] == '/' || currentLevelName[i] == '\\') { + currentLevelName.erase(i); + ///debug(9, "'%s'", currentLevelName.c_str()); + break; + } + } + + Common::String path = _currentDirectory; + if (path != "") + path += "/"; + path += currentLevelName; + bool lastLevel = (path.equalsIgnoreCase(_requestedPath)); + + ///debug(9, "IdResolveIdRequest: searching for '%s' in '%s'", currentLevelName.c_str(), _currentDirectory.c_str()); + + Common::Array<StorageFile> &files = response.value; + bool found = false; + for (uint32 i = 0; i < files.size(); ++i) { + if ((files[i].isDirectory() || lastLevel) && files[i].name().equalsIgnoreCase(currentLevelName)) { + if (_currentDirectory != "") + _currentDirectory += "/"; + _currentDirectory += files[i].name(); + _currentDirectoryId = files[i].id(); + ///debug(9, "IdResolveIdRequest: found it! new directory and its id: '%s', '%s'", _currentDirectory.c_str(), _currentDirectoryId.c_str()); + listNextDirectory(files[i]); + found = true; + break; + } + } + + if (!found) { + if (lastLevel) + finishError(Networking::ErrorResponse(this, false, true, Common::String("no such file found in its parent directory\n") + _currentDirectoryId, 404)); + else + finishError(Networking::ErrorResponse(this, false, true, "subdirectory not found", 400)); + } +} + +void IdResolveIdRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdResolveIdRequest::handle() {} + +void IdResolveIdRequest::restart() { start(); } + +void IdResolveIdRequest::finishFile(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idresolveidrequest.h b/backends/cloud/id/idresolveidrequest.h new file mode 100644 index 0000000000..009b1e8055 --- /dev/null +++ b/backends/cloud/id/idresolveidrequest.h @@ -0,0 +1,60 @@ +/* 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 BACKENDS_CLOUD_ID_IDRESOLVEIDREQUEST_H +#define BACKENDS_CLOUD_ID_IDRESOLVEIDREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdResolveIdRequest: public Networking::Request { + Common::String _requestedPath; + IdStorage *_storage; + Storage::UploadCallback _uploadCallback; + Common::String _currentDirectory; + Common::String _currentDirectoryId; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void listNextDirectory(StorageFile fileToReturn); + void listedDirectoryCallback(Storage::FileArrayResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void finishFile(StorageFile file); +public: + IdResolveIdRequest(IdStorage *storage, Common::String path, Storage::UploadCallback cb, Networking::ErrorCallback ecb, bool recursive = false); //TODO: why upload? + virtual ~IdResolveIdRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idstorage.cpp b/backends/cloud/id/idstorage.cpp new file mode 100644 index 0000000000..44427ac4d2 --- /dev/null +++ b/backends/cloud/id/idstorage.cpp @@ -0,0 +1,110 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/id/idstorage.h" +#include "backends/cloud/id/idcreatedirectoryrequest.h" +#include "backends/cloud/id/iddownloadrequest.h" +#include "backends/cloud/id/idlistdirectoryrequest.h" +#include "backends/cloud/id/idresolveidrequest.h" +#include "backends/cloud/id/idstreamfilerequest.h" +#include "common/debug.h" + +namespace Cloud { +namespace Id { + +IdStorage::~IdStorage() {} + +void IdStorage::printFiles(FileArrayResponse response) { + debug(9, "IdStorage: files:"); + Common::Array<StorageFile> &files = response.value; + for (uint32 i = 0; i < files.size(); ++i) { + debug(9, "\t%s%s", files[i].name().c_str(), files[i].isDirectory() ? " (directory)" : ""); + debug(9, "\t%s", files[i].path().c_str()); + debug(9, "\t%s", files[i].id().c_str()); + debug(9, " "); + } +} + +void IdStorage::printBool(BoolResponse response) { + debug(9, "IdStorage: bool: %s", response.value ? "true" : "false"); +} + +void IdStorage::printFile(UploadResponse response) { + debug(9, "\nIdStorage: uploaded file info:"); + debug(9, "\tid: %s", response.value.path().c_str()); + debug(9, "\tname: %s", response.value.name().c_str()); + debug(9, "\tsize: %u", response.value.size()); + debug(9, "\ttimestamp: %u", response.value.timestamp()); +} + +Storage::ListDirectoryCallback IdStorage::getPrintFilesCallback() { + return new Common::Callback<IdStorage, FileArrayResponse>(this, &IdStorage::printFiles); +} + +Networking::Request *IdStorage::resolveFileId(Common::String path, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, UploadResponse>(this, &IdStorage::printFile); + return addRequest(new IdResolveIdRequest(this, path, callback, errorCallback)); +} + +Networking::Request *IdStorage::listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, FileArrayResponse>(this, &IdStorage::printFiles); + return addRequest(new IdListDirectoryRequest(this, path, callback, errorCallback, recursive)); +} + +Networking::Request *IdStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + if (!callback) + callback = new Common::Callback<IdStorage, BoolResponse>(this, &IdStorage::printBool); + + //find out the parent path and directory name + Common::String parentPath = "", directoryName = path; + for (uint32 i = path.size(); i > 0; --i) { + if (path[i - 1] == '/' || path[i - 1] == '\\') { + parentPath = path; + parentPath.erase(i - 1); + directoryName.erase(0, i); + break; + } + } + + return addRequest(new IdCreateDirectoryRequest(this, parentPath, directoryName, callback, errorCallback)); +} + +Networking::Request *IdStorage::streamFile(Common::String path, Networking::NetworkReadStreamCallback outerCallback, Networking::ErrorCallback errorCallback) { + return addRequest(new IdStreamFileRequest(this, path, outerCallback, errorCallback)); +} + +Networking::Request *IdStorage::download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new IdDownloadRequest(this, remotePath, localPath, callback, errorCallback)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idstorage.h b/backends/cloud/id/idstorage.h new file mode 100644 index 0000000000..946a792b42 --- /dev/null +++ b/backends/cloud/id/idstorage.h @@ -0,0 +1,83 @@ +/* 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 BACKENDS_CLOUD_ID_IDSTORAGE_H +#define BACKENDS_CLOUD_ID_IDSTORAGE_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +/* + * Id::IdStorage is a special base class, which is created + * to simplify adding new storages which use ids instead of + * paths in their API. + * + * Some Requests are already implemented, and Storage based + * on IdStorage needs to override/implement a few basic things. + * + * For example, ListDirectoryRequest and ResolveIdRequests are + * based on listDirectoryById() and getRootDirectoryId() methods. + * Implementing these you'll get id resolving and directory + * listing by path. + */ + +namespace Cloud { +namespace Id { + +class IdStorage: public Cloud::Storage { +protected: + void printFiles(FileArrayResponse response); + void printBool(BoolResponse response); + void printFile(UploadResponse response); + + ListDirectoryCallback getPrintFilesCallback(); + +public: + virtual ~IdStorage(); + + /** Public Cloud API comes down there. */ + + /** Returns StorageFile with the resolved file's id. */ + virtual Networking::Request *resolveFileId(Common::String path, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + virtual Networking::Request *listDirectoryById(Common::String id, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *createDirectoryWithParentId(Common::String parentId, Common::String name, BoolCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + + virtual Common::String getRootDirectoryId() = 0; +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/id/idstreamfilerequest.cpp b/backends/cloud/id/idstreamfilerequest.cpp new file mode 100644 index 0000000000..6aa7af349d --- /dev/null +++ b/backends/cloud/id/idstreamfilerequest.cpp @@ -0,0 +1,98 @@ +/* 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 "backends/cloud/id/idstreamfilerequest.h" +#include "backends/cloud/id/idstorage.h" + +namespace Cloud { +namespace Id { + +IdStreamFileRequest::IdStreamFileRequest(IdStorage *storage, Common::String path, Networking::NetworkReadStreamCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _requestedFile(path), _storage(storage), _streamCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +IdStreamFileRequest::~IdStreamFileRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _streamCallback; +} + +void IdStreamFileRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + + //find file's id + Storage::UploadCallback innerCallback = new Common::Callback<IdStreamFileRequest, Storage::UploadResponse>(this, &IdStreamFileRequest::idResolvedCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdStreamFileRequest, Networking::ErrorResponse>(this, &IdStreamFileRequest::idResolveFailedCallback); + _workingRequest = _storage->resolveFileId(_requestedFile, innerCallback, innerErrorCallback); +} + +void IdStreamFileRequest::idResolvedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::NetworkReadStreamCallback innerCallback = new Common::Callback<IdStreamFileRequest, Networking::NetworkReadStreamResponse>(this, &IdStreamFileRequest::streamFileCallback); + Networking::ErrorCallback innerErrorCallback = new Common::Callback<IdStreamFileRequest, Networking::ErrorResponse>(this, &IdStreamFileRequest::streamFileErrorCallback); + _workingRequest = _storage->streamFileById(response.value.id(), innerCallback, innerErrorCallback); +} + +void IdStreamFileRequest::idResolveFailedCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdStreamFileRequest::streamFileCallback(Networking::NetworkReadStreamResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishStream(response.value); +} + +void IdStreamFileRequest::streamFileErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void IdStreamFileRequest::handle() {} + +void IdStreamFileRequest::restart() { start(); } + +void IdStreamFileRequest::finishStream(Networking::NetworkReadStream *stream) { + Request::finishSuccess(); + if (_streamCallback) + (*_streamCallback)(Networking::NetworkReadStreamResponse(this, stream)); +} + +} // End of namespace Id +} // End of namespace Cloud diff --git a/backends/cloud/id/idstreamfilerequest.h b/backends/cloud/id/idstreamfilerequest.h new file mode 100644 index 0000000000..4df9ab3820 --- /dev/null +++ b/backends/cloud/id/idstreamfilerequest.h @@ -0,0 +1,59 @@ +/* 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 BACKENDS_CLOUD_ID_IDSTREAMFILEREQUEST_H +#define BACKENDS_CLOUD_ID_IDSTREAMFILEREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace Id { + +class IdStorage; + +class IdStreamFileRequest: public Networking::Request { + Common::String _requestedFile; + IdStorage *_storage; + Networking::NetworkReadStreamCallback _streamCallback; + Request *_workingRequest; + bool _ignoreCallback; + + void start(); + void idResolvedCallback(Storage::UploadResponse response); + void idResolveFailedCallback(Networking::ErrorResponse error); + void streamFileCallback(Networking::NetworkReadStreamResponse response); + void streamFileErrorCallback(Networking::ErrorResponse error); + void finishStream(Networking::NetworkReadStream *stream); +public: + IdStreamFileRequest(IdStorage *storage, Common::String path, Networking::NetworkReadStreamCallback cb, Networking::ErrorCallback ecb); + virtual ~IdStreamFileRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace Id +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/iso8601.cpp b/backends/cloud/iso8601.cpp new file mode 100644 index 0000000000..6c51c49609 --- /dev/null +++ b/backends/cloud/iso8601.cpp @@ -0,0 +1,100 @@ +/* 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 "backends/cloud/iso8601.h" +#include "common/str.h" + +namespace { + +Common::String getSubstring(const Common::String &s, uint32 beginning, uint32 ending) { + //beginning inclusive, ending exclusive + Common::String result = s; + result.erase(ending); + result.erase(0, beginning); + return result; +} + +int find(const char *cstr, uint32 startPosition, char needle) { + const char *res = strchr(cstr + startPosition, needle); + if (res == nullptr) + return -1; + return res - cstr; +} + +} + +namespace Cloud { +namespace ISO8601 { + +uint32 convertToTimestamp(const Common::String &iso8601Date) { + //2015-05-12T15:50:38Z + const char *cstr = iso8601Date.c_str(); + int firstHyphen = find(cstr, 0, '-'); + int secondHyphen = find(cstr, firstHyphen + 1, '-'); + int tSeparator = find(cstr, secondHyphen + 1, 'T'); + int firstColon = find(cstr, tSeparator + 1, ':'); + int secondColon = find(cstr, firstColon + 1, ':'); + int zSeparator = find(cstr, secondColon + 1, 'Z'); + if (zSeparator == -1) + zSeparator = find(cstr, secondColon + 1, '-'); // Box's RFC 3339 + //now note '+1' which means if there ever was '-1' result of find(), we still did a valid find() from 0th char + + Common::String year = getSubstring(iso8601Date, 0, firstHyphen); + Common::String month = getSubstring(iso8601Date, firstHyphen + 1, secondHyphen); + Common::String day = getSubstring(iso8601Date, secondHyphen + 1, tSeparator); + Common::String hour = getSubstring(iso8601Date, tSeparator + 1, firstColon); + Common::String minute = getSubstring(iso8601Date, firstColon + 1, secondColon); + Common::String second = getSubstring(iso8601Date, secondColon + 1, zSeparator); + //now note only 'ending' argument was not '+1' (which means I could've make that function such that -1 means 'until the end') + + int Y = atoi(year.c_str()); + int M = atoi(month.c_str()); + int D = atoi(day.c_str()); + int h = atoi(hour.c_str()); + int m = atoi(minute.c_str()); + int s = atoi(second.c_str()); + + //ok, now I compose a timestamp based on my basic perception of time/date + //yeah, I know about leap years and leap seconds and all, but still we don't care there + + uint32 days = D - 1; + for (int i = 1970; i < Y; ++i) + if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) + days += 366; + else + days += 365; + + int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + for (int i = 1; i < M; ++i) { + days += mdays[i - 1]; + if (i == 2) + if ((Y % 4 == 0 && Y % 100 != 0) || (Y % 400 == 0)) + days += 1; + } + + uint32 hours = days * 24 + h; + uint32 minutes = hours * 60 + m; + return minutes * 60 + s; +} + +} // End of namespace ISO8601 +} // End of namespace Cloud diff --git a/backends/graphics/opengl/extensions.h b/backends/cloud/iso8601.h index 87452429e2..5d9939b944 100644 --- a/backends/graphics/opengl/extensions.h +++ b/backends/cloud/iso8601.h @@ -20,22 +20,18 @@ * */ -#ifndef BACKENDS_GRAPHICS_OPENGL_EXTENSIONS_H -#define BACKENDS_GRAPHICS_OPENGL_EXTENSIONS_H +#ifndef BACKENDS_CLOUD_ISO8601_H +#define BACKENDS_CLOUD_ISO8601_H -namespace OpenGL { +#include "common/str.h" -/** - * Checks for availability of extensions we want to use and initializes them - * when available. - */ -void initializeGLExtensions(); +namespace Cloud { +namespace ISO8601 { -/** - * Whether non power of two textures are supported - */ -extern bool g_extNPOTSupported; +/** Returns timestamp corresponding to given ISO 8601 date */ +uint32 convertToTimestamp(const Common::String &iso8601Date); -} // End of namespace OpenGL +} // End of namespace ISO8601 +} // End of namespace Cloud #endif diff --git a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp new file mode 100644 index 0000000000..da67266d12 --- /dev/null +++ b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.cpp @@ -0,0 +1,150 @@ +/* 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 "backends/cloud/onedrive/onedrivecreatedirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot" + +OneDriveCreateDirectoryRequest::OneDriveCreateDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _path(path), _boolCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveCreateDirectoryRequest::~OneDriveCreateDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void OneDriveCreateDirectoryRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _ignoreCallback = false; + + Common::String name = _path, parent = _path; + if (name.size() != 0) { + uint32 i = name.size() - 1; + while (true) { + parent.deleteLastChar(); + if (name[i] == '/' || name[i] == '\\') { + name.erase(0, i + 1); + break; + } + if (i == 0) + break; + --i; + } + } + + Common::String url = ONEDRIVE_API_SPECIAL_APPROOT; + if (parent != "") + url += ":/" + ConnMan.urlEncode(parent) + ":"; + url += "/children"; + Networking::JsonCallback innerCallback = new Common::Callback<OneDriveCreateDirectoryRequest, Networking::JsonResponse>(this, &OneDriveCreateDirectoryRequest::responseCallback); + Networking::ErrorCallback errorCallback = new Common::Callback<OneDriveCreateDirectoryRequest, Networking::ErrorResponse>(this, &OneDriveCreateDirectoryRequest::errorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->addHeader("Content-Type: application/json"); + + Common::JSONObject jsonRequestParameters; + jsonRequestParameters.setVal("name", new Common::JSONValue(name)); + jsonRequestParameters.setVal("folder", new Common::JSONValue(Common::JSONObject())); + Common::JSONValue value(jsonRequestParameters); + request->addPostField(Common::JSON::stringify(&value)); + + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveCreateDirectoryRequest::responseCallback(Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + _workingRequest = nullptr; + if (_ignoreCallback) { + delete json; + return; + } + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject info = json->asObject(); + if (info.contains("id")) { + finishCreation(true); + } else { + error.response = json->stringify(true); + finishError(error); + } + + delete json; +} + +void OneDriveCreateDirectoryRequest::errorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void OneDriveCreateDirectoryRequest::handle() {} + +void OneDriveCreateDirectoryRequest::restart() { start(); } + +Common::String OneDriveCreateDirectoryRequest::date() const { return _date; } + +void OneDriveCreateDirectoryRequest::finishCreation(bool success) { + Request::finishSuccess(); + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h new file mode 100644 index 0000000000..a04009c27d --- /dev/null +++ b/backends/cloud/onedrive/onedrivecreatedirectoryrequest.h @@ -0,0 +1,59 @@ +/* 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 BACKENDS_CLOUD_ONEDRIVE_ONEDRIVECREATEDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVECREATEDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveCreateDirectoryRequest: public Networking::Request { + OneDriveStorage *_storage; + Common::String _path; + Storage::BoolCallback _boolCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void responseCallback(Networking::JsonResponse response); + void errorCallback(Networking::ErrorResponse error); + void finishCreation(bool success); +public: + OneDriveCreateDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::BoolCallback cb, Networking::ErrorCallback ecb); + virtual ~OneDriveCreateDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp new file mode 100644 index 0000000000..953845d343 --- /dev/null +++ b/backends/cloud/onedrive/onedrivelistdirectoryrequest.cpp @@ -0,0 +1,193 @@ +/* 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 "backends/cloud/onedrive/onedrivelistdirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/iso8601.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/children" + +OneDriveListDirectoryRequest::OneDriveListDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive): + Networking::Request(nullptr, ecb), + _requestedPath(path), _requestedRecursive(recursive), _storage(storage), _listDirectoryCallback(cb), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveListDirectoryRequest::~OneDriveListDirectoryRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _listDirectoryCallback; +} + +void OneDriveListDirectoryRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _workingRequest = nullptr; + _files.clear(); + _directoriesQueue.clear(); + _currentDirectory = ""; + _ignoreCallback = false; + + _directoriesQueue.push_back(_requestedPath); + listNextDirectory(); +} + +void OneDriveListDirectoryRequest::listNextDirectory() { + if (_directoriesQueue.empty()) { + finishListing(_files); + return; + } + + _currentDirectory = _directoriesQueue.back(); + _directoriesQueue.pop_back(); + + if (_currentDirectory != "" && _currentDirectory.lastChar() != '/' && _currentDirectory.lastChar() != '\\') + _currentDirectory += '/'; + + Common::String dir = _currentDirectory; + dir.deleteLastChar(); + Common::String url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_CHILDREN, ConnMan.urlEncode(dir).c_str()); + makeRequest(url); +} + +void OneDriveListDirectoryRequest::makeRequest(Common::String url) { + Networking::JsonCallback callback = new Common::Callback<OneDriveListDirectoryRequest, Networking::JsonResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveListDirectoryRequest, Networking::ErrorResponse>(this, &OneDriveListDirectoryRequest::listedDirectoryErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveListDirectoryRequest::listedDirectoryCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + Common::JSONValue *json = response.value; + + if (_ignoreCallback) { + delete json; + return; + } + + if (response.request) + _date = response.request->date(); + + Networking::ErrorResponse error(this); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (!json->isObject()) { + error.response = "Passed JSON is not an object!"; + finishError(error); + delete json; + return; + } + + Common::JSONObject object = json->asObject(); + + //check that ALL keys exist AND HAVE RIGHT TYPE to avoid segfaults + if (!Networking::CurlJsonRequest::jsonContainsArray(object, "value", "OneDriveListDirectoryRequest")) { + error.response = "\"value\" not found or that's not an array!"; + finishError(error); + delete json; + return; + } + + Common::JSONArray items = object.getVal("value")->asArray(); + for (uint32 i = 0; i < items.size(); ++i) { + if (!Networking::CurlJsonRequest::jsonIsObject(items[i], "OneDriveListDirectoryRequest")) continue; + + Common::JSONObject item = items[i]->asObject(); + + if (!Networking::CurlJsonRequest::jsonContainsAttribute(item, "folder", "OneDriveListDirectoryRequest", true)) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "name", "OneDriveListDirectoryRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsIntegerNumber(item, "size", "OneDriveListDirectoryRequest")) continue; + if (!Networking::CurlJsonRequest::jsonContainsString(item, "lastModifiedDateTime", "OneDriveListDirectoryRequest")) continue; + + Common::String path = _currentDirectory + item.getVal("name")->asString(); + bool isDirectory = item.contains("folder"); + uint32 size = item.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(item.getVal("lastModifiedDateTime")->asString()); + + StorageFile file(path, size, timestamp, isDirectory); + _files.push_back(file); + if (_requestedRecursive && file.isDirectory()) { + _directoriesQueue.push_back(file.path()); + } + } + + bool hasMore = object.contains("@odata.nextLink"); + if (hasMore) { + if (!Networking::CurlJsonRequest::jsonContainsString(object, "@odata.nextLink", "OneDriveListDirectoryRequest")) { + error.response = "\"@odata.nextLink\" is not a string!"; + finishError(error); + delete json; + return; + } + + makeRequest(object.getVal("@odata.nextLink")->asString()); + } else { + listNextDirectory(); + } + + delete json; +} + +void OneDriveListDirectoryRequest::listedDirectoryErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + if (error.request) + _date = error.request->date(); + finishError(error); +} + +void OneDriveListDirectoryRequest::handle() {} + +void OneDriveListDirectoryRequest::restart() { start(); } + +Common::String OneDriveListDirectoryRequest::date() const { return _date; } + +void OneDriveListDirectoryRequest::finishListing(Common::Array<StorageFile> &files) { + Request::finishSuccess(); + if (_listDirectoryCallback) + (*_listDirectoryCallback)(Storage::ListDirectoryResponse(this, files)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivelistdirectoryrequest.h b/backends/cloud/onedrive/onedrivelistdirectoryrequest.h new file mode 100644 index 0000000000..6fd0ad9fa0 --- /dev/null +++ b/backends/cloud/onedrive/onedrivelistdirectoryrequest.h @@ -0,0 +1,66 @@ +/* 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 BACKENDS_CLOUD_ONEDRIVE_ONEDRIVELISTDIRECTORYREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVELISTDIRECTORYREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveListDirectoryRequest: public Networking::Request { + Common::String _requestedPath; + bool _requestedRecursive; + OneDriveStorage *_storage; + Storage::ListDirectoryCallback _listDirectoryCallback; + Common::Array<StorageFile> _files; + Common::Array<Common::String> _directoriesQueue; + Common::String _currentDirectory; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _date; + + void start(); + void listNextDirectory(); + void listedDirectoryCallback(Networking::JsonResponse response); + void listedDirectoryErrorCallback(Networking::ErrorResponse error); + void makeRequest(Common::String url); + void finishListing(Common::Array<StorageFile> &files); +public: + OneDriveListDirectoryRequest(OneDriveStorage *storage, Common::String path, Storage::ListDirectoryCallback cb, Networking::ErrorCallback ecb, bool recursive = false); + virtual ~OneDriveListDirectoryRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivestorage.cpp b/backends/cloud/onedrive/onedrivestorage.cpp new file mode 100644 index 0000000000..8799f3d69f --- /dev/null +++ b/backends/cloud/onedrive/onedrivestorage.cpp @@ -0,0 +1,327 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/cloud/onedrive/onedrivecreatedirectoryrequest.h" +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/onedrive/onedrivelistdirectoryrequest.h" +#include "backends/cloud/onedrive/onedriveuploadrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +#ifdef ENABLE_RELEASE +#include "dists/clouds/cloud_keys.h" +#endif + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_OAUTH2_TOKEN "https://login.live.com/oauth20_token.srf" +#define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/" +#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot" + +char *OneDriveStorage::KEY = nullptr; //can't use CloudConfig there yet, loading it on instance creation/auth +char *OneDriveStorage::SECRET = nullptr; + +void OneDriveStorage::loadKeyAndSecret() { +#ifdef ENABLE_RELEASE + KEY = RELEASE_ONEDRIVE_KEY; + SECRET = RELEASE_ONEDRIVE_SECRET; +#else + Common::String k = ConfMan.get("ONEDRIVE_KEY", ConfMan.kCloudDomain); + KEY = new char[k.size() + 1]; + memcpy(KEY, k.c_str(), k.size()); + KEY[k.size()] = 0; + + k = ConfMan.get("ONEDRIVE_SECRET", ConfMan.kCloudDomain); + SECRET = new char[k.size() + 1]; + memcpy(SECRET, k.c_str(), k.size()); + SECRET[k.size()] = 0; +#endif +} + +OneDriveStorage::OneDriveStorage(Common::String accessToken, Common::String userId, Common::String refreshToken): + _token(accessToken), _uid(userId), _refreshToken(refreshToken) {} + +OneDriveStorage::OneDriveStorage(Common::String code) { + getAccessToken( + new Common::Callback<OneDriveStorage, BoolResponse>(this, &OneDriveStorage::codeFlowComplete), + new Common::Callback<OneDriveStorage, Networking::ErrorResponse>(this, &OneDriveStorage::codeFlowFailed), + code + ); +} + +OneDriveStorage::~OneDriveStorage() {} + +void OneDriveStorage::getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback, Common::String code) { + if (!KEY || !SECRET) + loadKeyAndSecret(); + bool codeFlow = (code != ""); + + if (!codeFlow && _refreshToken == "") { + warning("OneDriveStorage: no refresh token available to get new access token."); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + return; + } + + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, BoolResponse, Networking::JsonResponse>(this, &OneDriveStorage::tokenRefreshed, callback); + if (errorCallback == nullptr) + errorCallback = getErrorPrintingCallback(); + Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, ONEDRIVE_OAUTH2_TOKEN); + if (codeFlow) { + request->addPostField("code=" + code); + request->addPostField("grant_type=authorization_code"); + } else { + request->addPostField("refresh_token=" + _refreshToken); + request->addPostField("grant_type=refresh_token"); + } + request->addPostField("client_id=" + Common::String(KEY)); + request->addPostField("client_secret=" + Common::String(SECRET)); + if (Cloud::CloudManager::couldUseLocalServer()) { + request->addPostField("&redirect_uri=http%3A%2F%2Flocalhost%3A12345%2F"); + } else { + request->addPostField("&redirect_uri=https%3A%2F%2Fwww.scummvm.org/c/code"); + } + addRequest(request); +} + +void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage: got NULL instead of JSON"); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete callback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage")) { + if (callback) + (*callback)(BoolResponse(nullptr, false)); + delete json; + delete callback; + return; + } + + Common::JSONObject result = json->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "OneDriveStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "user_id", "OneDriveStorage") || + !Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "OneDriveStorage")) { + warning("OneDriveStorage: bad response, no token or user_id passed"); + debug(9, "%s", json->stringify().c_str()); + if (callback) + (*callback)(BoolResponse(nullptr, false)); + } else { + _token = result.getVal("access_token")->asString(); + _uid = result.getVal("user_id")->asString(); + _refreshToken = result.getVal("refresh_token")->asString(); + CloudMan.save(); //ask CloudManager to save our new refreshToken + if (callback) + (*callback)(BoolResponse(nullptr, true)); + } + delete json; + delete callback; +} + +void OneDriveStorage::codeFlowComplete(BoolResponse response) { + if (!response.value) { + warning("OneDriveStorage: failed to get access token through code flow"); + CloudMan.removeStorage(this); + return; + } + + ConfMan.removeKey("onedrive_code", ConfMan.kCloudDomain); + CloudMan.replaceStorage(this, kStorageOneDriveId); + ConfMan.flushToDisk(); +} + +void OneDriveStorage::codeFlowFailed(Networking::ErrorResponse error) { + debug(9, "OneDriveStorage: code flow failed (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); + CloudMan.removeStorage(this); +} + +void OneDriveStorage::saveConfig(Common::String keyPrefix) { + ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "user_id", _uid, ConfMan.kCloudDomain); + ConfMan.set(keyPrefix + "refresh_token", _refreshToken, ConfMan.kCloudDomain); +} + +Common::String OneDriveStorage::name() const { + return "OneDrive"; +} + +void OneDriveStorage::infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage::infoInnerCallback: NULL passed instead of JSON"); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage::infoInnerCallback")) { + delete json; + delete outerCallback; + return; + } + + Common::JSONObject info = json->asObject(); + + Common::String uid, name, email; + uint64 quotaUsed = 0, quotaAllocated = 26843545600LL; // 25 GB, because I actually don't know any way to find out the real one + + if (Networking::CurlJsonRequest::jsonContainsObject(info, "createdBy", "OneDriveStorage::infoInnerCallback")) { + Common::JSONObject createdBy = info.getVal("createdBy")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsObject(createdBy, "user", "OneDriveStorage::infoInnerCallback")) { + Common::JSONObject user = createdBy.getVal("user")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "id", "OneDriveStorage::infoInnerCallback")) + uid = user.getVal("id")->asString(); + if (Networking::CurlJsonRequest::jsonContainsString(user, "displayName", "OneDriveStorage::infoInnerCallback")) + name = user.getVal("displayName")->asString(); + } + } + + if (Networking::CurlJsonRequest::jsonContainsIntegerNumber(info, "size", "OneDriveStorage::infoInnerCallback")) { + quotaUsed = info.getVal("size")->asIntegerNumber(); + } + + Common::String username = email; + if (username == "") + username = name; + if (username == "") + username = uid; + CloudMan.setStorageUsername(kStorageOneDriveId, username); + + if (outerCallback) { + (*outerCallback)(StorageInfoResponse(nullptr, StorageInfo(uid, name, email, quotaUsed, quotaAllocated))); + delete outerCallback; + } + + delete json; +} + +void OneDriveStorage::fileInfoCallback(Networking::NetworkReadStreamCallback outerCallback, Networking::JsonResponse response) { + Common::JSONValue *json = response.value; + if (!json) { + warning("OneDriveStorage::fileInfoCallback: NULL passed instead of JSON"); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete outerCallback; + return; + } + + if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage::fileInfoCallback")) { + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete json; + delete outerCallback; + return; + } + + Common::JSONObject result = response.value->asObject(); + if (!Networking::CurlJsonRequest::jsonContainsString(result, "@content.downloadUrl", "OneDriveStorage::fileInfoCallback")) { + warning("OneDriveStorage: downloadUrl not found in passed JSON"); + debug(9, "%s", response.value->stringify().c_str()); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse(response.request, nullptr)); + delete json; + delete outerCallback; + return; + } + + const char *url = result.getVal("@content.downloadUrl")->asString().c_str(); + if (outerCallback) + (*outerCallback)(Networking::NetworkReadStreamResponse( + response.request, + new Networking::NetworkReadStream(url, nullptr, "") + )); + + delete json; + delete outerCallback; +} + +Networking::Request *OneDriveStorage::listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + return addRequest(new OneDriveListDirectoryRequest(this, path, callback, errorCallback, recursive)); +} + +Networking::Request *OneDriveStorage::upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) { + return addRequest(new OneDriveUploadRequest(this, path, contents, callback, errorCallback)); +} + +Networking::Request *OneDriveStorage::streamFileById(Common::String path, Networking::NetworkReadStreamCallback outerCallback, Networking::ErrorCallback errorCallback) { + Common::String url = ONEDRIVE_API_SPECIAL_APPROOT_ID + ConnMan.urlEncode(path); + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, Networking::NetworkReadStreamResponse, Networking::JsonResponse>(this, &OneDriveStorage::fileInfoCallback, outerCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _token); + return addRequest(request); +} + +Networking::Request *OneDriveStorage::createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new OneDriveCreateDirectoryRequest(this, path, callback, errorCallback)); +} + +Networking::Request *OneDriveStorage::info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) { + Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, StorageInfoResponse, Networking::JsonResponse>(this, &OneDriveStorage::infoInnerCallback, callback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(this, innerCallback, errorCallback, ONEDRIVE_API_SPECIAL_APPROOT); + request->addHeader("Authorization: bearer " + _token); + return addRequest(request); +} + +Common::String OneDriveStorage::savesDirectoryPath() { return "saves/"; } + +OneDriveStorage *OneDriveStorage::loadFromConfig(Common::String keyPrefix) { + loadKeyAndSecret(); + + if (!ConfMan.hasKey(keyPrefix + "access_token", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no access_token found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "user_id", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no user_id found"); + return nullptr; + } + + if (!ConfMan.hasKey(keyPrefix + "refresh_token", ConfMan.kCloudDomain)) { + warning("OneDriveStorage: no refresh_token found"); + return nullptr; + } + + Common::String accessToken = ConfMan.get(keyPrefix + "access_token", ConfMan.kCloudDomain); + Common::String userId = ConfMan.get(keyPrefix + "user_id", ConfMan.kCloudDomain); + Common::String refreshToken = ConfMan.get(keyPrefix + "refresh_token", ConfMan.kCloudDomain); + return new OneDriveStorage(accessToken, userId, refreshToken); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivestorage.h b/backends/cloud/onedrive/onedrivestorage.h new file mode 100644 index 0000000000..5d24eb2c2e --- /dev/null +++ b/backends/cloud/onedrive/onedrivestorage.h @@ -0,0 +1,113 @@ +/* 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 BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVESTORAGE_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage: public Cloud::Storage { + static char *KEY, *SECRET; + + static void loadKeyAndSecret(); + + Common::String _token, _uid, _refreshToken; + + /** This private constructor is called from loadFromConfig(). */ + OneDriveStorage(Common::String token, Common::String uid, Common::String refreshToken); + + void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response); + void codeFlowComplete(BoolResponse response); + void codeFlowFailed(Networking::ErrorResponse error); + + /** Constructs StorageInfo based on JSON response from cloud. */ + void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json); + + void fileInfoCallback(Networking::NetworkReadStreamCallback outerCallback, Networking::JsonResponse response); +public: + /** This constructor uses OAuth code flow to get tokens. */ + OneDriveStorage(Common::String code); + virtual ~OneDriveStorage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix); + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const; + + /** Public Cloud API comes down there. */ + + /** Returns ListDirectoryStatus struct with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Returns UploadStatus struct with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFileById(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns the StorageInfo struct. */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath(); + + /** + * Load token and user id from configs and return OneDriveStorage for those. + * @return pointer to the newly created OneDriveStorage or 0 if some problem occured. + */ + static OneDriveStorage *loadFromConfig(Common::String keyPrefix); + + /** + * Gets new access_token. If <code> passed is "", refresh_token is used. + * Use "" in order to refresh token and pass a callback, so you could + * continue your work when new token is available. + */ + void getAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr, Common::String code = ""); + + Common::String accessToken() const { return _token; } +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.cpp b/backends/cloud/onedrive/onedrivetokenrefresher.cpp new file mode 100644 index 0000000000..7104dc6bb6 --- /dev/null +++ b/backends/cloud/onedrive/onedrivetokenrefresher.cpp @@ -0,0 +1,131 @@ +/* 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_ALLOW_ALL + +#include "backends/cloud/onedrive/onedrivetokenrefresher.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Cloud { +namespace OneDrive { + +OneDriveTokenRefresher::OneDriveTokenRefresher(OneDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url): + CurlJsonRequest(callback, ecb, url), _parentStorage(parent) {} + +OneDriveTokenRefresher::~OneDriveTokenRefresher() {} + +void OneDriveTokenRefresher::tokenRefreshed(Storage::BoolResponse response) { + if (!response.value) { + //failed to refresh token, notify user with NULL in original callback + warning("OneDriveTokenRefresher: failed to refresh token"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update headers: first change header with token, then pass those to request + for (uint32 i = 0; i < _headers.size(); ++i) { + if (_headers[i].contains("Authorization")) { + _headers[i] = "Authorization: bearer " + _parentStorage->accessToken(); + } + } + setHeaders(_headers); + + //successfully received refreshed token, can restart the original request now + retry(0); +} + +void OneDriveTokenRefresher::finishJson(Common::JSONValue *json) { + if (!json) { + //that's probably not an error (200 OK) + CurlJsonRequest::finishJson(nullptr); + return; + } + + if (jsonIsObject(json, "OneDriveTokenRefresher")) { + Common::JSONObject result = json->asObject(); + long httpResponseCode = -1; + if (result.contains("error") && jsonIsObject(result.getVal("error"), "OneDriveTokenRefresher")) { + //new token needed => request token & then retry original request + if (_stream) { + httpResponseCode = _stream->httpResponseCode(); + debug(9, "OneDriveTokenRefresher: code = %ld", httpResponseCode); + } + + Common::JSONObject error = result.getVal("error")->asObject(); + bool irrecoverable = true; + + Common::String code, message; + if (jsonContainsString(error, "code", "OneDriveTokenRefresher")) { + code = error.getVal("code")->asString(); + debug(9, "OneDriveTokenRefresher: code = %s", code.c_str()); + } + + if (jsonContainsString(error, "message", "OneDriveTokenRefresher")) { + message = error.getVal("message")->asString(); + debug(9, "OneDriveTokenRefresher: message = %s", message.c_str()); + } + + //determine whether token refreshing would help in this situation + if (code == "itemNotFound") { + if (message.contains("application ID")) + irrecoverable = false; + } + + if (code == "unauthenticated") + irrecoverable = false; + + if (irrecoverable) { + finishError(Networking::ErrorResponse(this, false, true, json->stringify(true), httpResponseCode)); + delete json; + return; + } + + pause(); + delete json; + _parentStorage->getAccessToken(new Common::Callback<OneDriveTokenRefresher, Storage::BoolResponse>(this, &OneDriveTokenRefresher::tokenRefreshed)); + return; + } + } + + //notify user of success + CurlJsonRequest::finishJson(json); +} + +void OneDriveTokenRefresher::setHeaders(Common::Array<Common::String> &headers) { + _headers = headers; + curl_slist_free_all(_headersList); + _headersList = 0; + for (uint32 i = 0; i < headers.size(); ++i) + CurlJsonRequest::addHeader(headers[i]); +} + +void OneDriveTokenRefresher::addHeader(Common::String header) { + _headers.push_back(header); + CurlJsonRequest::addHeader(header); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedrivetokenrefresher.h b/backends/cloud/onedrive/onedrivetokenrefresher.h new file mode 100644 index 0000000000..d190bc4666 --- /dev/null +++ b/backends/cloud/onedrive/onedrivetokenrefresher.h @@ -0,0 +1,52 @@ +/* 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 BACKENDS_CLOUD_ONEDRIVE_ONEDRIVETOKENREFRESHER_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVETOKENREFRESHER_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" + +namespace Cloud { +namespace OneDrive { + +class OneDriveStorage; + +class OneDriveTokenRefresher: public Networking::CurlJsonRequest { + OneDriveStorage *_parentStorage; + Common::Array<Common::String> _headers; + + void tokenRefreshed(Storage::BoolResponse response); + + virtual void finishJson(Common::JSONValue *json); +public: + OneDriveTokenRefresher(OneDriveStorage *parent, Networking::JsonCallback callback, Networking::ErrorCallback ecb, const char *url); + virtual ~OneDriveTokenRefresher(); + + virtual void setHeaders(Common::Array<Common::String> &headers); + virtual void addHeader(Common::String header); +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/onedrive/onedriveuploadrequest.cpp b/backends/cloud/onedrive/onedriveuploadrequest.cpp new file mode 100644 index 0000000000..ebf387fca2 --- /dev/null +++ b/backends/cloud/onedrive/onedriveuploadrequest.cpp @@ -0,0 +1,191 @@ +/* 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 "backends/cloud/onedrive/onedriveuploadrequest.h" +#include "backends/cloud/onedrive/onedrivestorage.h" +#include "backends/cloud/iso8601.h" +#include "backends/cloud/storage.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/json.h" +#include "onedrivetokenrefresher.h" + +namespace Cloud { +namespace OneDrive { + +#define ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/upload.createSession" +#define ONEDRIVE_API_SPECIAL_APPROOT_CONTENT "https://api.onedrive.com/v1.0/drive/special/approot:/%s:/content" + +OneDriveUploadRequest::OneDriveUploadRequest(OneDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb): + Networking::Request(nullptr, ecb), _storage(storage), _savePath(path), _contentsStream(contents), _uploadCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +OneDriveUploadRequest::~OneDriveUploadRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _contentsStream; + delete _uploadCallback; +} + +void OneDriveUploadRequest::start() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + if (_contentsStream == nullptr) { + warning("OneDriveUploadRequest: cannot restart because no stream given"); + finishError(Networking::ErrorResponse(this, false, true, "No stream given", -1)); + return; + } + if (!_contentsStream->seek(0)) { + warning("OneDriveUploadRequest: cannot restart because stream couldn't seek(0)"); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + _ignoreCallback = false; + + uploadNextPart(); +} + +void OneDriveUploadRequest::uploadNextPart() { + const uint32 UPLOAD_PER_ONE_REQUEST = 10 * 1024 * 1024; + + if (_uploadUrl == "" && (uint32)_contentsStream->size() > UPLOAD_PER_ONE_REQUEST) { + Common::String url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_UPLOAD, ConnMan.urlEncode(_savePath).c_str()); //folder must exist + Networking::JsonCallback callback = new Common::Callback<OneDriveUploadRequest, Networking::JsonResponse>(this, &OneDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveUploadRequest, Networking::ErrorResponse>(this, &OneDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->setBuffer(new byte[1], 0); //use POST + _workingRequest = ConnMan.addRequest(request); + return; + } + + Common::String url; + if (_uploadUrl == "") { + url = Common::String::format(ONEDRIVE_API_SPECIAL_APPROOT_CONTENT, ConnMan.urlEncode(_savePath).c_str()); + } else { + url = _uploadUrl; + } + + Networking::JsonCallback callback = new Common::Callback<OneDriveUploadRequest, Networking::JsonResponse>(this, &OneDriveUploadRequest::partUploadedCallback); + Networking::ErrorCallback failureCallback = new Common::Callback<OneDriveUploadRequest, Networking::ErrorResponse>(this, &OneDriveUploadRequest::partUploadedErrorCallback); + Networking::CurlJsonRequest *request = new OneDriveTokenRefresher(_storage, callback, failureCallback, url.c_str()); + request->addHeader("Authorization: Bearer " + _storage->accessToken()); + request->usePut(); + + uint32 oldPos = _contentsStream->pos(); + + byte *buffer = new byte[UPLOAD_PER_ONE_REQUEST]; + uint32 size = _contentsStream->read(buffer, UPLOAD_PER_ONE_REQUEST); + request->setBuffer(buffer, size); + + if (_uploadUrl != "") { + request->addHeader(Common::String::format("Content-Range: bytes %u-%u/%u", oldPos, _contentsStream->pos() - 1, _contentsStream->size())); + } else if (_contentsStream->size() == 0) { + warning("\"Sorry, OneDrive can't upload empty files\""); + finishUpload(StorageFile(_savePath, 0, 0, false)); + delete request; + return; + } + + _workingRequest = ConnMan.addRequest(request); +} + +void OneDriveUploadRequest::partUploadedCallback(Networking::JsonResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + Networking::ErrorResponse error(this, false, true, "", -1); + Networking::CurlJsonRequest *rq = (Networking::CurlJsonRequest *)response.request; + if (rq && rq->getNetworkReadStream()) + error.httpResponseCode = rq->getNetworkReadStream()->httpResponseCode(); + + Common::JSONValue *json = response.value; + if (json == nullptr) { + error.response = "Failed to parse JSON, null passed!"; + finishError(error); + return; + } + + if (json->isObject()) { + Common::JSONObject object = json->asObject(); + + if (object.contains("error")) { + warning("OneDriveUploadRequest: error: %s", json->stringify(true).c_str()); + error.response = json->stringify(true); + finishError(error); + delete json; + return; + } + + if (Networking::CurlJsonRequest::jsonContainsString(object, "id", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "name", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsIntegerNumber(object, "size", "OneDriveUploadRequest") && + Networking::CurlJsonRequest::jsonContainsString(object, "lastModifiedDateTime", "OneDriveUploadRequest")) { + //finished + Common::String path = _savePath; + uint32 size = object.getVal("size")->asIntegerNumber(); + uint32 timestamp = ISO8601::convertToTimestamp(object.getVal("lastModifiedDateTime")->asString()); + finishUpload(StorageFile(path, size, timestamp, false)); + return; + } + + if (_uploadUrl == "") { + if (Networking::CurlJsonRequest::jsonContainsString(object, "uploadUrl", "OneDriveUploadRequest")) + _uploadUrl = object.getVal("uploadUrl")->asString(); + } + } + + if (_contentsStream->eos() || _contentsStream->pos() >= _contentsStream->size() - 1) { + warning("OneDriveUploadRequest: no file info to return"); + finishUpload(StorageFile(_savePath, 0, 0, false)); + } else { + uploadNextPart(); + } + + delete json; +} + +void OneDriveUploadRequest::partUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + finishError(error); +} + +void OneDriveUploadRequest::handle() {} + +void OneDriveUploadRequest::restart() { start(); } + +void OneDriveUploadRequest::finishUpload(StorageFile file) { + Request::finishSuccess(); + if (_uploadCallback) + (*_uploadCallback)(Storage::UploadResponse(this, file)); +} + +} // End of namespace OneDrive +} // End of namespace Cloud diff --git a/backends/cloud/onedrive/onedriveuploadrequest.h b/backends/cloud/onedrive/onedriveuploadrequest.h new file mode 100644 index 0000000000..ba3c013f71 --- /dev/null +++ b/backends/cloud/onedrive/onedriveuploadrequest.h @@ -0,0 +1,61 @@ +/* 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 BACKENDS_CLOUD_ONEDRIVE_ONEDRIVEUPLOADREQUEST_H +#define BACKENDS_CLOUD_ONEDRIVE_ONEDRIVEUPLOADREQUEST_H + +#include "backends/cloud/storage.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/request.h" +#include "common/callback.h" + +namespace Cloud { +namespace OneDrive { +class OneDriveStorage; + +class OneDriveUploadRequest: public Networking::Request { + OneDriveStorage *_storage; + Common::String _savePath; + Common::SeekableReadStream *_contentsStream; + Storage::UploadCallback _uploadCallback; + Request *_workingRequest; + bool _ignoreCallback; + Common::String _uploadUrl; + + void start(); + void uploadNextPart(); + void partUploadedCallback(Networking::JsonResponse response); + void partUploadedErrorCallback(Networking::ErrorResponse error); + void finishUpload(StorageFile status); + +public: + OneDriveUploadRequest(OneDriveStorage *storage, Common::String path, Common::SeekableReadStream *contents, Storage::UploadCallback callback, Networking::ErrorCallback ecb); + virtual ~OneDriveUploadRequest(); + + virtual void handle(); + virtual void restart(); +}; + +} // End of namespace OneDrive +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/savessyncrequest.cpp b/backends/cloud/savessyncrequest.cpp new file mode 100644 index 0000000000..f9b16b355b --- /dev/null +++ b/backends/cloud/savessyncrequest.cpp @@ -0,0 +1,403 @@ +/* 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 "backends/cloud/savessyncrequest.h" +#include "backends/cloud/cloudmanager.h" +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/saves/default/default-saves.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/file.h" +#include "common/json.h" +#include "common/savefile.h" +#include "common/system.h" +#include "gui/saveload-dialog.h" + +namespace Cloud { + +SavesSyncRequest::SavesSyncRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb): + Request(nullptr, ecb), CommandSender(nullptr), _storage(storage), _boolCallback(callback), + _workingRequest(nullptr), _ignoreCallback(false) { + start(); +} + +SavesSyncRequest::~SavesSyncRequest() { + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + delete _boolCallback; +} + +void SavesSyncRequest::start() { + //cleanup + _ignoreCallback = true; + if (_workingRequest) + _workingRequest->finish(); + _currentDownloadingFile = StorageFile(); + _currentUploadingFile = ""; + _filesToDownload.clear(); + _filesToUpload.clear(); + _localFilesTimestamps.clear(); + _totalFilesToHandle = 0; + _ignoreCallback = false; + + //load timestamps + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + + //list saves directory + Common::String dir = _storage->savesDirectoryPath(); + if (dir.lastChar() == '/') + dir.deleteLastChar(); + _workingRequest = _storage->listDirectory( + dir, + new Common::Callback<SavesSyncRequest, Storage::ListDirectoryResponse>(this, &SavesSyncRequest::directoryListedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::directoryListedErrorCallback) + ); + if (!_workingRequest) finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::directoryListedCallback(Storage::ListDirectoryResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + if (response.request) _date = response.request->date(); + + Common::HashMap<Common::String, bool> localFileNotAvailableInCloud; + for (Common::HashMap<Common::String, uint32>::iterator i = _localFilesTimestamps.begin(); i != _localFilesTimestamps.end(); ++i) { + localFileNotAvailableInCloud[i->_key] = true; + } + + //determine which files to download and which files to upload + Common::Array<StorageFile> &remoteFiles = response.value; + uint64 totalSize = 0; + for (uint32 i = 0; i < remoteFiles.size(); ++i) { + StorageFile &file = remoteFiles[i]; + if (file.isDirectory()) + continue; + totalSize += file.size(); + if (file.name() == DefaultSaveFileManager::TIMESTAMPS_FILENAME) + continue; + + Common::String name = file.name(); + if (!_localFilesTimestamps.contains(name)) { + _filesToDownload.push_back(file); + } else { + localFileNotAvailableInCloud[name] = false; + + if (_localFilesTimestamps[name] == file.timestamp()) + continue; + + //we actually can have some files not only with timestamp < remote + //but also with timestamp > remote (when we have been using ANOTHER CLOUD and then switched back) + if (_localFilesTimestamps[name] > file.timestamp() || _localFilesTimestamps[name] == DefaultSaveFileManager::INVALID_TIMESTAMP) + _filesToUpload.push_back(file.name()); + else + _filesToDownload.push_back(file); + } + } + + CloudMan.setStorageUsedSpace(CloudMan.getStorageIndex(), totalSize); + + //upload files which are unavailable in cloud + for (Common::HashMap<Common::String, bool>::iterator i = localFileNotAvailableInCloud.begin(); i != localFileNotAvailableInCloud.end(); ++i) { + if (i->_key == DefaultSaveFileManager::TIMESTAMPS_FILENAME) + continue; + if (i->_value) + _filesToUpload.push_back(i->_key); + } + + debug(9, "\nSavesSyncRequest: download files:"); + for (uint32 i = 0; i < _filesToDownload.size(); ++i) { + debug(9, "%s", _filesToDownload[i].name().c_str()); + } + debug(9, "\nSavesSyncRequest: upload files:"); + for (uint32 i = 0; i < _filesToUpload.size(); ++i) { + debug(9, "%s", _filesToUpload[i].c_str()); + } + _totalFilesToHandle = _filesToDownload.size() + _filesToUpload.size(); + + //start downloading files + downloadNextFile(); +} + +void SavesSyncRequest::directoryListedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + bool irrecoverable = error.interrupted || error.failed; + if (error.failed) { + Common::JSONValue *value = Common::JSON::parse(error.response.c_str()); + if (value) { + if (value->isObject()) { + Common::JSONObject object = value->asObject(); + + //Dropbox-related error: + if (object.contains("error_summary") && object.getVal("error_summary")->isString()) { + Common::String summary = object.getVal("error_summary")->asString(); + if (summary.contains("not_found")) { + irrecoverable = false; + } + } + + //OneDrive-related error: + if (object.contains("error") && object.getVal("error")->isObject()) { + Common::JSONObject errorNode = object.getVal("error")->asObject(); + if (Networking::CurlJsonRequest::jsonContainsString(errorNode, "code", "SavesSyncRequest")) { + Common::String code = errorNode.getVal("code")->asString(); + if (code == "itemNotFound") { + irrecoverable = false; + } + } + } + } + delete value; + } + + //Google Drive and Box-related ScummVM-based error + if (error.response.contains("subdirectory not found")) { + irrecoverable = false; //base "/ScummVM/" folder not found + } else if (error.response.contains("no such file found in its parent directory")) { + irrecoverable = false; //"Saves" folder within "/ScummVM/" not found + } + } + + if (irrecoverable) { + finishError(error); + return; + } + + //we're lucky - user just lacks his "/cloud/" folder - let's create one + Common::String dir = _storage->savesDirectoryPath(); + if (dir.lastChar() == '/') + dir.deleteLastChar(); + debug(9, "SavesSyncRequest: creating %s", dir.c_str()); + _workingRequest = _storage->createDirectory( + dir, + new Common::Callback<SavesSyncRequest, Storage::BoolResponse>(this, &SavesSyncRequest::directoryCreatedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::directoryCreatedErrorCallback) + ); + if (!_workingRequest) + finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::directoryCreatedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if failed to create saves directory + if (!response.value) { + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //continue with empty files list + Common::Array<StorageFile> files; + directoryListedCallback(Storage::ListDirectoryResponse(response.request, files)); +} + +void SavesSyncRequest::directoryCreatedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if failed to create saves directory + finishError(error); +} + +void SavesSyncRequest::downloadNextFile() { + if (_filesToDownload.empty()) { + _currentDownloadingFile = StorageFile("", 0, 0, false); //so getFilesToDownload() would return an empty array + sendCommand(GUI::kSavesSyncEndedCmd, 0); + uploadNextFile(); + return; + } + + _currentDownloadingFile = _filesToDownload.back(); + _filesToDownload.pop_back(); + + sendCommand(GUI::kSavesSyncProgressCmd, (int)(getDownloadingProgress() * 100)); + + debug(9, "SavesSyncRequest: downloading %s (%d %%)", _currentDownloadingFile.name().c_str(), (int)(getProgress() * 100)); + _workingRequest = _storage->downloadById( + _currentDownloadingFile.id(), + DefaultSaveFileManager::concatWithSavesPath(_currentDownloadingFile.name()), + new Common::Callback<SavesSyncRequest, Storage::BoolResponse>(this, &SavesSyncRequest::fileDownloadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileDownloadedErrorCallback) + ); + if (!_workingRequest) + finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::fileDownloadedCallback(Storage::BoolResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if download failed + if (!response.value) { + //delete the incomplete file + g_system->getSavefileManager()->removeSavefile(_currentDownloadingFile.name()); + finishError(Networking::ErrorResponse(this, false, true, "", -1)); + return; + } + + //update local timestamp for downloaded file + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + _localFilesTimestamps[_currentDownloadingFile.name()] = _currentDownloadingFile.timestamp(); + DefaultSaveFileManager::saveTimestamps(_localFilesTimestamps); + + //continue downloading files + downloadNextFile(); +} + +void SavesSyncRequest::fileDownloadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if download failed + finishError(error); +} + +void SavesSyncRequest::uploadNextFile() { + if (_filesToUpload.empty()) { + finishSync(true); + return; + } + + _currentUploadingFile = _filesToUpload.back(); + _filesToUpload.pop_back(); + + debug(9, "SavesSyncRequest: uploading %s (%d %%)", _currentUploadingFile.c_str(), (int)(getProgress() * 100)); + if (_storage->uploadStreamSupported()) { + _workingRequest = _storage->upload( + _storage->savesDirectoryPath() + _currentUploadingFile, + g_system->getSavefileManager()->openRawFile(_currentUploadingFile), + new Common::Callback<SavesSyncRequest, Storage::UploadResponse>(this, &SavesSyncRequest::fileUploadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileUploadedErrorCallback) + ); + } else { + _workingRequest = _storage->upload( + _storage->savesDirectoryPath() + _currentUploadingFile, + DefaultSaveFileManager::concatWithSavesPath(_currentUploadingFile), + new Common::Callback<SavesSyncRequest, Storage::UploadResponse>(this, &SavesSyncRequest::fileUploadedCallback), + new Common::Callback<SavesSyncRequest, Networking::ErrorResponse>(this, &SavesSyncRequest::fileUploadedErrorCallback) + ); + } + if (!_workingRequest) finishError(Networking::ErrorResponse(this)); +} + +void SavesSyncRequest::fileUploadedCallback(Storage::UploadResponse response) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //update local timestamp for the uploaded file + _localFilesTimestamps = DefaultSaveFileManager::loadTimestamps(); + _localFilesTimestamps[_currentUploadingFile] = response.value.timestamp(); + DefaultSaveFileManager::saveTimestamps(_localFilesTimestamps); + + //continue uploading files + uploadNextFile(); +} + +void SavesSyncRequest::fileUploadedErrorCallback(Networking::ErrorResponse error) { + _workingRequest = nullptr; + if (_ignoreCallback) + return; + + //stop syncing if upload failed + finishError(error); +} + +void SavesSyncRequest::handle() {} + +void SavesSyncRequest::restart() { start(); } + +double SavesSyncRequest::getDownloadingProgress() const { + if (_totalFilesToHandle == 0) { + if (_state == Networking::FINISHED) + return 1; //nothing to upload and download => Request ends soon + return 0; //directory not listed yet + } + + if (_totalFilesToHandle == _filesToUpload.size()) + return 1; //nothing to download => download complete + + uint32 totalFilesToDownload = _totalFilesToHandle - _filesToUpload.size(); + uint32 filesLeftToDownload = _filesToDownload.size() + (_currentDownloadingFile.name() != "" ? 1 : 0); + return (double)(totalFilesToDownload - filesLeftToDownload) / (double)(totalFilesToDownload); +} + +double SavesSyncRequest::getProgress() const { + if (_totalFilesToHandle == 0) { + if (_state == Networking::FINISHED) + return 1; //nothing to upload and download => Request ends soon + return 0; //directory not listed yet + } + + return (double)(_totalFilesToHandle - _filesToDownload.size() - _filesToUpload.size()) / (double)(_totalFilesToHandle); +} + +Common::Array<Common::String> SavesSyncRequest::getFilesToDownload() { + Common::Array<Common::String> result; + for (uint32 i = 0; i < _filesToDownload.size(); ++i) + result.push_back(_filesToDownload[i].name()); + if (_currentDownloadingFile.name() != "") + result.push_back(_currentDownloadingFile.name()); + return result; +} + +void SavesSyncRequest::finishError(Networking::ErrorResponse error) { + debug(9, "SavesSync::finishError"); + //if we were downloading a file - remember the name + //and make the Request close() it, so we can delete it + Common::String name = _currentDownloadingFile.name(); + if (_workingRequest) { + _ignoreCallback = true; + _workingRequest->finish(); + _workingRequest = nullptr; + _ignoreCallback = false; + } + //unlock all the files by making getFilesToDownload() return empty array + _currentDownloadingFile = StorageFile(); + _filesToDownload.clear(); + //delete the incomplete file + if (name != "") + g_system->getSavefileManager()->removeSavefile(name); + Request::finishError(error); +} + +void SavesSyncRequest::finishSync(bool success) { + Request::finishSuccess(); + + //update last successful sync date + CloudMan.setStorageLastSync(CloudMan.getStorageIndex(), _date); + + if (_boolCallback) + (*_boolCallback)(Storage::BoolResponse(this, success)); +} + +} // End of namespace Cloud diff --git a/backends/cloud/savessyncrequest.h b/backends/cloud/savessyncrequest.h new file mode 100644 index 0000000000..501a46d582 --- /dev/null +++ b/backends/cloud/savessyncrequest.h @@ -0,0 +1,80 @@ +/* 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 BACKENDS_CLOUD_SAVESSYNCREQUEST_H +#define BACKENDS_CLOUD_SAVESSYNCREQUEST_H + +#include "backends/networking/curl/request.h" +#include "backends/cloud/storage.h" +#include "common/hashmap.h" +#include "common/hash-str.h" +#include "gui/object.h" + +namespace Cloud { + +class SavesSyncRequest: public Networking::Request, public GUI::CommandSender { + Storage *_storage; + Storage::BoolCallback _boolCallback; + Common::HashMap<Common::String, uint32> _localFilesTimestamps; + Common::Array<StorageFile> _filesToDownload; + Common::Array<Common::String> _filesToUpload; + StorageFile _currentDownloadingFile; + Common::String _currentUploadingFile; + Request *_workingRequest; + bool _ignoreCallback; + uint32 _totalFilesToHandle; + Common::String _date; + + void start(); + void directoryListedCallback(Storage::ListDirectoryResponse response); + void directoryListedErrorCallback(Networking::ErrorResponse error); + void directoryCreatedCallback(Storage::BoolResponse response); + void directoryCreatedErrorCallback(Networking::ErrorResponse error); + void fileDownloadedCallback(Storage::BoolResponse response); + void fileDownloadedErrorCallback(Networking::ErrorResponse error); + void fileUploadedCallback(Storage::UploadResponse response); + void fileUploadedErrorCallback(Networking::ErrorResponse error); + void downloadNextFile(); + void uploadNextFile(); + virtual void finishError(Networking::ErrorResponse error); + void finishSync(bool success); + +public: + SavesSyncRequest(Storage *storage, Storage::BoolCallback callback, Networking::ErrorCallback ecb); + virtual ~SavesSyncRequest(); + + virtual void handle(); + virtual void restart(); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getDownloadingProgress() const; + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Returns an array of saves names which are not downloaded yet. */ + Common::Array<Common::String> getFilesToDownload(); +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storage.cpp b/backends/cloud/storage.cpp new file mode 100644 index 0000000000..3a9ae53a43 --- /dev/null +++ b/backends/cloud/storage.cpp @@ -0,0 +1,343 @@ +/* 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 "backends/cloud/storage.h" +#include "backends/cloud/downloadrequest.h" +#include "backends/cloud/folderdownloadrequest.h" +#include "backends/cloud/savessyncrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "common/debug.h" +#include "common/file.h" +#include <common/translation.h> +#include "common/osd_message_queue.h" + +namespace Cloud { + +Storage::Storage(): + _runningRequestsCount(0), _savesSyncRequest(nullptr), _syncRestartRequestsed(false), + _downloadFolderRequest(nullptr) {} + +Storage::~Storage() {} + +Networking::ErrorCallback Storage::getErrorPrintingCallback() { + return new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::printErrorResponse); +} + +void Storage::printErrorResponse(Networking::ErrorResponse error) { + debug(9, "Storage: error response (%s, %ld):", (error.failed ? "failed" : "interrupted"), error.httpResponseCode); + debug(9, "%s", error.response.c_str()); +} + +Networking::Request *Storage::addRequest(Networking::Request *request) { + _runningRequestsMutex.lock(); + ++_runningRequestsCount; + if (_runningRequestsCount == 1) + debug(9, "Storage is working now"); + _runningRequestsMutex.unlock(); + return ConnMan.addRequest(request, new Common::Callback<Storage, Networking::Request *>(this, &Storage::requestFinishedCallback)); +} + +void Storage::requestFinishedCallback(Networking::Request *invalidRequestPointer) { + bool restartSync = false; + + _runningRequestsMutex.lock(); + if (invalidRequestPointer == _savesSyncRequest) + _savesSyncRequest = nullptr; + --_runningRequestsCount; + if (_syncRestartRequestsed) + restartSync = true; + if (_runningRequestsCount == 0 && !restartSync) + debug(9, "Storage is not working now"); + _runningRequestsMutex.unlock(); + + if (restartSync) + syncSaves(nullptr, nullptr); +} + +Networking::Request *Storage::upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) errorCallback = getErrorPrintingCallback(); + + Common::File *f = new Common::File(); + if (!f->open(localPath)) { + warning("Storage: unable to open file to upload from"); + if (errorCallback) + (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "", -1)); + delete errorCallback; + delete callback; + delete f; + return nullptr; + } + + return upload(remotePath, f, callback, errorCallback); +} + +bool Storage::uploadStreamSupported() { + return true; +} + +Networking::Request *Storage::streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) { + //most Storages use paths instead of ids, so this should work + return streamFileById(path, callback, errorCallback); +} + +Networking::Request *Storage::download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + //most Storages use paths instead of ids, so this should work + return downloadById(remotePath, localPath, callback, errorCallback); +} + +Networking::Request *Storage::downloadById(Common::String remoteId, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback) { + if (!errorCallback) errorCallback = getErrorPrintingCallback(); + + Common::DumpFile *f = new Common::DumpFile(); + if (!f->open(localPath, true)) { + warning("Storage: unable to open file to download into"); + if (errorCallback) (*errorCallback)(Networking::ErrorResponse(nullptr, false, true, "", -1)); + delete errorCallback; + delete callback; + delete f; + return nullptr; + } + + return addRequest(new DownloadRequest(this, callback, errorCallback, remoteId, f)); +} + +Networking::Request *Storage::downloadFolder(Common::String remotePath, Common::String localPath, FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive) { + if (!errorCallback) + errorCallback = getErrorPrintingCallback(); + return addRequest(new FolderDownloadRequest(this, callback, errorCallback, remotePath, localPath, recursive)); +} + +SavesSyncRequest *Storage::syncSaves(BoolCallback callback, Networking::ErrorCallback errorCallback) { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) { + warning("Storage::syncSaves: there is a sync in progress already"); + _syncRestartRequestsed = true; + _runningRequestsMutex.unlock(); + return _savesSyncRequest; + } + if (!callback) + callback = new Common::Callback<Storage, BoolResponse>(this, &Storage::savesSyncDefaultCallback); + if (!errorCallback) + errorCallback = new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::savesSyncDefaultErrorCallback); + _savesSyncRequest = new SavesSyncRequest(this, callback, errorCallback); + _syncRestartRequestsed = false; + _runningRequestsMutex.unlock(); + return (SavesSyncRequest *)addRequest(_savesSyncRequest); //who knows what that ConnMan could return in the future +} + +bool Storage::isWorking() { + _runningRequestsMutex.lock(); + bool working = _runningRequestsCount > 0; + _runningRequestsMutex.unlock(); + return working; +} + +///// SavesSyncRequest-related ///// + +bool Storage::isSyncing() { + _runningRequestsMutex.lock(); + bool syncing = _savesSyncRequest != nullptr; + _runningRequestsMutex.unlock(); + return syncing; +} + +double Storage::getSyncDownloadingProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getDownloadingProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +double Storage::getSyncProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::Array<Common::String> Storage::getSyncingFiles() { + Common::Array<Common::String> result; + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + result = _savesSyncRequest->getFilesToDownload(); + _runningRequestsMutex.unlock(); + return result; +} + +void Storage::cancelSync() { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + _savesSyncRequest->finish(); + _runningRequestsMutex.unlock(); +} + +void Storage::setSyncTarget(GUI::CommandReceiver *target) { + _runningRequestsMutex.lock(); + if (_savesSyncRequest) + _savesSyncRequest->setTarget(target); + _runningRequestsMutex.unlock(); +} + +void Storage::savesSyncDefaultCallback(BoolResponse response) { + _runningRequestsMutex.lock(); + _savesSyncRequest = nullptr; + _runningRequestsMutex.unlock(); + + if (!response.value) + warning("SavesSyncRequest called success callback with `false` argument"); + Common::OSDMessageQueue::instance().addMessage(_("Saved games sync complete.")); +} + +void Storage::savesSyncDefaultErrorCallback(Networking::ErrorResponse error) { + _runningRequestsMutex.lock(); + _savesSyncRequest = nullptr; + _runningRequestsMutex.unlock(); + + printErrorResponse(error); + + if (error.interrupted) + Common::OSDMessageQueue::instance().addMessage(_("Saved games sync was cancelled.")); + else + Common::OSDMessageQueue::instance().addMessage(_("Saved games sync failed.\nCheck your Internet connection.")); +} + +///// DownloadFolderRequest-related ///// + +bool Storage::startDownload(Common::String remotePath, Common::String localPath) { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) { + warning("Storage::startDownload: there is a download in progress already"); + _runningRequestsMutex.unlock(); + return false; + } + _downloadFolderRequest = (FolderDownloadRequest *)downloadFolder( + remotePath, localPath, + new Common::Callback<Storage, FileArrayResponse>(this, &Storage::directoryDownloadedCallback), + new Common::Callback<Storage, Networking::ErrorResponse>(this, &Storage::directoryDownloadedErrorCallback), + true + ); + _runningRequestsMutex.unlock(); + return true; +} + +void Storage::cancelDownload() { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + _downloadFolderRequest->finish(); + _runningRequestsMutex.unlock(); +} + +void Storage::setDownloadTarget(GUI::CommandReceiver *target) { + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + _downloadFolderRequest->setTarget(target); + _runningRequestsMutex.unlock(); +} + +bool Storage::isDownloading() { + _runningRequestsMutex.lock(); + bool syncing = _downloadFolderRequest != nullptr; + _runningRequestsMutex.unlock(); + return syncing; +} + +double Storage::getDownloadingProgress() { + double result = 1; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getProgress(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadBytesNumber() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getDownloadedBytes(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadTotalBytesNumber() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getTotalBytesToDownload(); + _runningRequestsMutex.unlock(); + return result; +} + +uint64 Storage::getDownloadSpeed() { + uint64 result = 0; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getDownloadSpeed(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::String Storage::getDownloadRemoteDirectory() { + Common::String result = ""; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getRemotePath(); + _runningRequestsMutex.unlock(); + return result; +} + +Common::String Storage::getDownloadLocalDirectory() { + Common::String result = ""; + _runningRequestsMutex.lock(); + if (_downloadFolderRequest) + result = _downloadFolderRequest->getLocalPath(); + _runningRequestsMutex.unlock(); + return result; +} + +void Storage::directoryDownloadedCallback(FileArrayResponse response) { + _runningRequestsMutex.lock(); + _downloadFolderRequest = nullptr; + _runningRequestsMutex.unlock(); + + Common::String message; + if (response.value.size()) { + message = Common::String::format(_("Download complete.\nFailed to download %u files."), response.value.size()); + } else { + message = _("Download complete."); + } + Common::OSDMessageQueue::instance().addMessage(message.c_str()); +} + +void Storage::directoryDownloadedErrorCallback(Networking::ErrorResponse error) { + _runningRequestsMutex.lock(); + _downloadFolderRequest = nullptr; + _runningRequestsMutex.unlock(); + + Common::OSDMessageQueue::instance().addMessage(_("Download failed.")); +} + +} // End of namespace Cloud diff --git a/backends/cloud/storage.h b/backends/cloud/storage.h new file mode 100644 index 0000000000..e914834dae --- /dev/null +++ b/backends/cloud/storage.h @@ -0,0 +1,238 @@ +/* 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 BACKENDS_CLOUD_STORAGE_H +#define BACKENDS_CLOUD_STORAGE_H + +#include "backends/cloud/storagefile.h" +#include "backends/cloud/storageinfo.h" +#include "backends/networking/curl/request.h" +#include "backends/networking/curl/curlrequest.h" +#include "common/array.h" +#include "common/callback.h" +#include "common/mutex.h" +#include "common/stream.h" +#include "common/str.h" + +namespace GUI { + +class CommandReceiver; + +} + +namespace Cloud { + +class SavesSyncRequest; +class FolderDownloadRequest; + +class Storage { +public: + typedef Networking::Response<Common::Array<StorageFile>&> FileArrayResponse; + typedef Networking::Response<StorageInfo> StorageInfoResponse; + typedef Networking::Response<bool> BoolResponse; + typedef Networking::Response<StorageFile> UploadResponse; + typedef Networking::Response<Common::Array<StorageFile> &> ListDirectoryResponse; + + typedef Common::BaseCallback<FileArrayResponse> *FileArrayCallback; + typedef Common::BaseCallback<StorageInfoResponse> *StorageInfoCallback; + typedef Common::BaseCallback<BoolResponse> *BoolCallback; + typedef Common::BaseCallback<UploadResponse> *UploadCallback; + typedef Common::BaseCallback<ListDirectoryResponse> *ListDirectoryCallback; + +protected: + /** Keeps track of running requests. */ + uint32 _runningRequestsCount; + Common::Mutex _runningRequestsMutex; + + /** SavesSyncRequest-related */ + SavesSyncRequest *_savesSyncRequest; + bool _syncRestartRequestsed; + + /** FolderDownloadRequest-related */ + FolderDownloadRequest *_downloadFolderRequest; + + /** Returns default error callback (printErrorResponse). */ + virtual Networking::ErrorCallback getErrorPrintingCallback(); + + /** Prints ErrorResponse contents with debug(). */ + virtual void printErrorResponse(Networking::ErrorResponse error); + + /** + * Adds request to the ConnMan, but also increases _runningRequestsCount. + * This method should be used by Storage implementations instead of + * direct ConnMan.addRequest() call. + * + * @return the same Request pointer, just as a shortcut + */ + virtual Networking::Request *addRequest(Networking::Request *request); + + /** + * Decreases _runningRequestCount. It's called from ConnMan automatically. + * Passed pointer is dangling, but one can use the address to determine + * some special Requests (which addresses were remembered somewhere). + */ + virtual void requestFinishedCallback(Networking::Request *invalidRequestPointer); + +public: + Storage(); + virtual ~Storage(); + + /** + * Storage methods, which are used by CloudManager to save + * storage in configuration file. + */ + + /** + * Save storage data using ConfMan. + * @param keyPrefix all saved keys must start with this prefix. + * @note every Storage must write keyPrefix + "type" key + * with common value (e.g. "Dropbox"). + */ + virtual void saveConfig(Common::String keyPrefix) = 0; + + /** + * Return unique storage name. + * @returns some unique storage name (for example, "Dropbox (user@example.com)") + */ + virtual Common::String name() const = 0; + + /** + * Public Cloud API comes down there. + * + * All Cloud API methods return Networking::Request *, which + * might be used to control request. All methods also accept + * a callback, which is called, when request is complete. + */ + + /** Returns ListDirectoryResponse with list of files. */ + virtual Networking::Request *listDirectory(Common::String path, ListDirectoryCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false) = 0; + + /** Returns StorageFile with info about uploaded file. */ + virtual Networking::Request *upload(Common::String path, Common::SeekableReadStream *contents, UploadCallback callback, Networking::ErrorCallback errorCallback) = 0; + virtual Networking::Request *upload(Common::String remotePath, Common::String localPath, UploadCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns whether Storage supports upload(ReadStream). */ + virtual bool uploadStreamSupported(); + + /** Returns pointer to Networking::NetworkReadStream. */ + virtual Networking::Request *streamFile(Common::String path, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *streamFileById(Common::String id, Networking::NetworkReadStreamCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Calls the callback when finished. */ + virtual Networking::Request *download(Common::String remotePath, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + virtual Networking::Request *downloadById(Common::String remoteId, Common::String localPath, BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Returns Common::Array<StorageFile> with list of files, which were not downloaded. */ + virtual Networking::Request *downloadFolder(Common::String remotePath, Common::String localPath, FileArrayCallback callback, Networking::ErrorCallback errorCallback, bool recursive = false); + + /** Calls the callback when finished. */ + virtual SavesSyncRequest *syncSaves(BoolCallback callback, Networking::ErrorCallback errorCallback); + + /** Calls the callback when finished. */ + virtual Networking::Request *createDirectory(Common::String path, BoolCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** + * Returns the StorageInfo struct via <callback>. + * Calls the <errorCallback> if failed to get information. + * + * @note on success Storage should also call + * CloudMan.setStorageUsername(). + */ + virtual Networking::Request *info(StorageInfoCallback callback, Networking::ErrorCallback errorCallback) = 0; + + /** Returns storage's saves directory path with the trailing slash. */ + virtual Common::String savesDirectoryPath() = 0; + + /** Returns whether there are any requests running. */ + virtual bool isWorking(); + + ///// SavesSyncRequest-related ///// + + /** Returns whether there is a SavesSyncRequest running. */ + virtual bool isSyncing(); + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + virtual double getSyncDownloadingProgress(); + + /** Returns a number in [0, 1] range which represents current sync progress (1 = complete). */ + virtual double getSyncProgress(); + + /** Returns an array of saves names which are not yet synced (thus cannot be used). */ + virtual Common::Array<Common::String> getSyncingFiles(); + + /** Cancels running sync. */ + virtual void cancelSync(); + + /** Sets SavesSyncRequest's target to given CommandReceiver. */ + virtual void setSyncTarget(GUI::CommandReceiver *target); + +protected: + /** Finishes the sync. Shows an OSD message. */ + virtual void savesSyncDefaultCallback(BoolResponse response); + + /** Finishes the sync. Shows an OSD message. */ + virtual void savesSyncDefaultErrorCallback(Networking::ErrorResponse error); + +public: + ///// DownloadFolderRequest-related ///// + + /** Starts a folder download. */ + virtual bool startDownload(Common::String remotePath, Common::String localPath); + + /** Cancels running download. */ + virtual void cancelDownload(); + + /** Sets FolderDownloadRequest's target to given CommandReceiver. */ + virtual void setDownloadTarget(GUI::CommandReceiver *target); + + /** Returns whether there is a FolderDownloadRequest running. */ + virtual bool isDownloading(); + + /** Returns a number in [0, 1] range which represents current download progress (1 = complete). */ + virtual double getDownloadingProgress(); + + /** Returns a number of bytes that is downloaded in current download progress. */ + virtual uint64 getDownloadBytesNumber(); + + /** Returns a total number of bytes to be downloaded in current download progress. */ + virtual uint64 getDownloadTotalBytesNumber(); + + /** Returns download speed of current download progress. */ + virtual uint64 getDownloadSpeed(); + + /** Returns remote directory path. */ + virtual Common::String getDownloadRemoteDirectory(); + + /** Returns local directory path. */ + virtual Common::String getDownloadLocalDirectory(); + +protected: + /** Finishes the download. Shows an OSD message. */ + virtual void directoryDownloadedCallback(FileArrayResponse response); + + /** Finishes the download. Shows an OSD message. */ + virtual void directoryDownloadedErrorCallback(Networking::ErrorResponse error); +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storagefile.cpp b/backends/cloud/storagefile.cpp new file mode 100644 index 0000000000..2428d8ff80 --- /dev/null +++ b/backends/cloud/storagefile.cpp @@ -0,0 +1,68 @@ +/* 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 "backends/cloud/storagefile.h" + +namespace Cloud { + +StorageFile::StorageFile() { + _id = ""; + _path = ""; + _name = ""; + _size = 0; + _timestamp = 0; + _isDirectory = false; +} + +StorageFile::StorageFile(Common::String pth, uint32 sz, uint32 ts, bool dir) { + _id = pth; + _path = pth; + + _name = pth; + if (_name.size() != 0) { + uint32 i = _name.size() - 1; + while (true) { + if (_name[i] == '/' || _name[i] == '\\') { + _name.erase(0, i + 1); + break; + } + if (i == 0) + break; + --i; + } + } + + _size = sz; + _timestamp = ts; + _isDirectory = dir; +} + +StorageFile::StorageFile(Common::String id, Common::String path, Common::String name, uint32 sz, uint32 ts, bool dir) { + _id = id; + _path = path; + _name = name; + _size = sz; + _timestamp = ts; + _isDirectory = dir; +} + +} // End of namespace Cloud diff --git a/backends/cloud/storagefile.h b/backends/cloud/storagefile.h new file mode 100644 index 0000000000..44f16c3689 --- /dev/null +++ b/backends/cloud/storagefile.h @@ -0,0 +1,65 @@ +/* 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 BACKENDS_CLOUD_STORAGEFILE_H +#define BACKENDS_CLOUD_STORAGEFILE_H + +#include "common/str.h" + +namespace Cloud { + +/** + * StorageFile represents a file storaged on remote cloud storage. + * It contains basic information about a file, and might be used + * when listing directories or syncing files. + * + * Some storages (Google Drive, for example) don't have an actual + * path notation to address files. Instead, they are using ids. + * As resolving id by path is not a fast operation, it's required + * to use ids if they are known, but user-friendly paths are + * necessary too, because these are used by Requests. + * + * If storage supports path notation, id would actually contain path. + */ +class StorageFile { + Common::String _id, _path, _name; + uint32 _size, _timestamp; + bool _isDirectory; + +public: + StorageFile(); //invalid empty file + StorageFile(Common::String pth, uint32 sz, uint32 ts, bool dir); + StorageFile(Common::String id, Common::String path, Common::String name, uint32 sz, uint32 ts, bool dir); + + Common::String id() const { return _id; } + Common::String path() const { return _path; } + Common::String name() const { return _name; } + uint32 size() const { return _size; } + uint32 timestamp() const { return _timestamp; } + bool isDirectory() const { return _isDirectory; } + + void setPath(Common::String path_) { _path = path_; } +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/cloud/storageinfo.h b/backends/cloud/storageinfo.h new file mode 100644 index 0000000000..cef9265f91 --- /dev/null +++ b/backends/cloud/storageinfo.h @@ -0,0 +1,53 @@ +/* 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 BACKENDS_CLOUD_STORAGEINFO_H +#define BACKENDS_CLOUD_STORAGEINFO_H + +#include "common/str.h" + +namespace Cloud { + +/** +* StorageInfo contains information about remote cloud storage. +* It's disk quota usage, owner name, and such. +*/ + +class StorageInfo { + Common::String _uid, _name, _email; + uint64 _usedBytes, _allocatedBytes; + +public: + StorageInfo(Common::String uid_, Common::String name_, Common::String email_, uint64 used_, uint64 allocated): + _uid(uid_), _name(name_), _email(email_), _usedBytes(used_), _allocatedBytes(allocated) {} + + Common::String uid() const { return _uid; } + Common::String name() const { return _name; } + Common::String email() const { return _email; } + uint64 used() const { return _usedBytes; } + uint64 available() const { return _allocatedBytes; } + +}; + +} // End of namespace Cloud + +#endif diff --git a/backends/events/androidsdl/androidsdl-events.cpp b/backends/events/androidsdl/androidsdl-events.cpp new file mode 100644 index 0000000000..0adcff817e --- /dev/null +++ b/backends/events/androidsdl/androidsdl-events.cpp @@ -0,0 +1,102 @@ +/* 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(ANDROIDSDL) + +#include "backends/events/androidsdl/androidsdl-events.h" +#include "backends/platform/androidsdl/androidsdl-sdl.h" +#include <SDL_screenkeyboard.h> + +bool AndroidSdlEventSource::handleMouseButtonDown(SDL_Event &ev, Common::Event &event) { + if (ev.button.button == SDL_BUTTON_LEFT) + event.type = Common::EVENT_LBUTTONDOWN; + else if (ev.button.button == SDL_BUTTON_RIGHT) + event.type = Common::EVENT_RBUTTONDOWN; +#if defined(SDL_BUTTON_WHEELUP) && defined(SDL_BUTTON_WHEELDOWN) + else if (ev.button.button == SDL_BUTTON_WHEELUP) + event.type = Common::EVENT_WHEELUP; + else if (ev.button.button == SDL_BUTTON_WHEELDOWN) + event.type = Common::EVENT_WHEELDOWN; +#endif +#if defined(SDL_BUTTON_MIDDLE) + else if (ev.button.button == SDL_BUTTON_MIDDLE) { + event.type = Common::EVENT_MBUTTONDOWN; + + static int show_onscreen = 0; + if (show_onscreen == 0) { + SDL_ANDROID_SetScreenKeyboardShown(0); + show_onscreen++; + } else if (show_onscreen==1) { + SDL_ANDROID_SetScreenKeyboardShown(1); + show_onscreen++; + } + if (show_onscreen == 2) + show_onscreen = 0; + } +#endif + else + return false; + + processMouseEvent(event, ev.button.x, ev.button.y); + + return true; +} + +bool AndroidSdlEventSource::remapKey(SDL_Event &ev, Common::Event &event) { + if (false) {} + + if (ev.key.keysym.sym == SDLK_F13) { + event.type = Common::EVENT_MAINMENU; + return true; + } else { + // Let the events fall through if we didn't change them, this may not be the best way to + // set it up, but i'm not sure how sdl would like it if we let if fall through then redid it though. + // and yes i have an huge terminal size so i dont wrap soon enough. + event.type = Common::EVENT_KEYDOWN; + event.kbd.keycode = (Common::KeyCode)ev.key.keysym.sym; + event.kbd.ascii = mapKey(ev.key.keysym.sym, ev.key.keysym.mod, ev.key.keysym.unicode); + } + + return false; +} + +int AndroidSdlEventSource::mapKey(SDLKey sdlKey, SDLMod mod, Uint16 unicode) { + Common::KeyCode key = SDLToOSystemKeycode(sdlKey); + + if (key >= Common::KEYCODE_F1 && key <= Common::KEYCODE_F9) { + return key - Common::KEYCODE_F1 + Common::ASCII_F1; + } else if (key >= Common::KEYCODE_KP0 && key <= Common::KEYCODE_KP9) { + return key - Common::KEYCODE_KP0 + '0'; + } else if (key >= Common::KEYCODE_UP && key <= Common::KEYCODE_PAGEDOWN) { + return key; + } else if (key >= 'a' && key <= 'z' && (mod & KMOD_SHIFT)) { + return key & ~0x20; + } else if (key >= Common::KEYCODE_NUMLOCK && key <= Common::KEYCODE_EURO) { + return 0; + } else { + return key; + } +} + +#endif diff --git a/backends/events/androidsdl/androidsdl-events.h b/backends/events/androidsdl/androidsdl-events.h new file mode 100644 index 0000000000..723a23b351 --- /dev/null +++ b/backends/events/androidsdl/androidsdl-events.h @@ -0,0 +1,38 @@ +/* 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(BACKEND_EVENTS_SDL_ANDROIDSDL_H) && !defined(DISABLE_DEFAULT_EVENTMANAGER) +#define BACKEND_EVENTS_SDL_ANDROIDSDL_H + +#include "backends/events/sdl/sdl-events.h" + +/** + * SDL events manager for ANDROIDSDL + */ +class AndroidSdlEventSource : public SdlEventSource { +protected: + virtual bool handleMouseButtonDown(SDL_Event &ev, Common::Event &event); + virtual bool remapKey(SDL_Event &ev, Common::Event &event); + virtual int mapKey(SDLKey key, SDLMod mod, Uint16 unicode); +}; + +#endif diff --git a/backends/events/sdl/sdl-events.cpp b/backends/events/sdl/sdl-events.cpp index 7b56a0a955..5fb66a7ec4 100644 --- a/backends/events/sdl/sdl-events.cpp +++ b/backends/events/sdl/sdl-events.cpp @@ -110,13 +110,51 @@ SdlEventSource::~SdlEventSource() { int SdlEventSource::mapKey(SDLKey sdlKey, SDLMod mod, Uint16 unicode) { Common::KeyCode key = SDLToOSystemKeycode(sdlKey); + // Keep unicode in case it's regular ASCII text or in case we didn't get a valid keycode + // + // We need to use unicode in those cases, simply because SDL1.x passes us non-layout-adjusted keycodes. + // So unicode is the only way to get layout-adjusted keys. + if (unicode < 0x20) { + // don't use unicode, in case it's control characters + unicode = 0; + } else { + // Use unicode, in case keycode is invalid. + // Umlauts and others will set KEYCODE_INVALID on SDL2, so in such a case always keep unicode. + if (key != Common::KEYCODE_INVALID) { + // keycode is valid, check further also depending on modifiers + if (mod & (KMOD_CTRL | KMOD_ALT)) { + // Ctrl and/or Alt is active + // + // We need to restrict unicode to only up to 0x7E, because on macOS the option/alt key will switch to + // an alternate keyboard, which will cause us to receive Unicode characters for some keys, which are outside + // of the ASCII range (e.g. alt-x will get us U+2248). We need to return 'x' for alt-x, so using unicode + // in that case would break alt-shortcuts. + if (unicode > 0x7E) + unicode = 0; // do not allow any characters above 0x7E + } else { + // We must not restrict as much as when Ctrl/Alt-modifiers are active, otherwise + // we wouldn't let umlauts through for SDL1. For SDL1 umlauts may set for example KEYCODE_QUOTE, KEYCODE_MINUS, etc. + if (unicode > 0xFF) + unicode = 0; // do not allow any characters above 0xFF + } + } + } + + // Attention: + // When using SDL1.x, we will get scancodes via sdlKey, that are raw scancodes, so NOT adjusted to keyboard layout/ + // mapping. So for example for certain locales, we will get KEYCODE_y, when 'z' is pressed and so on. + // When using SDL2.x however, we will get scancodes based on the keyboard layout. + if (key >= Common::KEYCODE_F1 && key <= Common::KEYCODE_F9) { return key - Common::KEYCODE_F1 + Common::ASCII_F1; } else if (key >= Common::KEYCODE_KP0 && key <= Common::KEYCODE_KP9) { + if ((mod & KMOD_NUM) == 0) + return 0; // In case Num-Lock is NOT enabled, return 0 for ascii, so that directional keys on numpad work return key - Common::KEYCODE_KP0 + '0'; } else if (key >= Common::KEYCODE_UP && key <= Common::KEYCODE_PAGEDOWN) { return key; } else if (unicode) { + // Return unicode in case it's stil set and wasn't filtered. return unicode; } else if (key >= 'a' && key <= 'z' && (mod & KMOD_SHIFT)) { return key & ~0x20; @@ -517,15 +555,17 @@ bool SdlEventSource::handleKeyDown(SDL_Event &ev, Common::Event &event) { SDLModToOSystemKeyFlags(SDL_GetModState(), event); + SDLKey sdlKeycode = obtainKeycode(ev.key.keysym); + // Handle scroll lock as a key modifier - if (ev.key.keysym.sym == SDLK_SCROLLOCK) + if (sdlKeycode == SDLK_SCROLLOCK) _scrollLock = !_scrollLock; if (_scrollLock) event.kbd.flags |= Common::KBD_SCRL; // Ctrl-m toggles mouse capture - if (event.kbd.hasFlags(Common::KBD_CTRL) && ev.key.keysym.sym == 'm') { + if (event.kbd.hasFlags(Common::KBD_CTRL) && sdlKeycode == 'm') { if (_graphicsManager) { _graphicsManager->getWindow()->toggleMouseGrab(); } @@ -534,26 +574,26 @@ bool SdlEventSource::handleKeyDown(SDL_Event &ev, Common::Event &event) { #if defined(MACOSX) // On Macintosh, Cmd-Q quits - if ((ev.key.keysym.mod & KMOD_META) && ev.key.keysym.sym == 'q') { + if ((ev.key.keysym.mod & KMOD_META) && sdlKeycode == 'q') { event.type = Common::EVENT_QUIT; return true; } #elif defined(POSIX) // On other *nix systems, Control-Q quits - if ((ev.key.keysym.mod & KMOD_CTRL) && ev.key.keysym.sym == 'q') { + if ((ev.key.keysym.mod & KMOD_CTRL) && sdlKeycode == 'q') { event.type = Common::EVENT_QUIT; return true; } #else // Ctrl-z quits - if ((event.kbd.hasFlags(Common::KBD_CTRL) && ev.key.keysym.sym == 'z')) { + if ((event.kbd.hasFlags(Common::KBD_CTRL) && sdlKeycode == 'z')) { event.type = Common::EVENT_QUIT; return true; } #ifdef WIN32 // On Windows, also use the default Alt-F4 quit combination - if ((ev.key.keysym.mod & KMOD_ALT) && ev.key.keysym.sym == SDLK_F4) { + if ((ev.key.keysym.mod & KMOD_ALT) && sdlKeycode == SDLK_F4) { event.type = Common::EVENT_QUIT; return true; } @@ -561,7 +601,7 @@ bool SdlEventSource::handleKeyDown(SDL_Event &ev, Common::Event &event) { #endif // Ctrl-u toggles mute - if ((ev.key.keysym.mod & KMOD_CTRL) && ev.key.keysym.sym == 'u') { + if ((ev.key.keysym.mod & KMOD_CTRL) && sdlKeycode == 'u') { event.type = Common::EVENT_MUTE; return true; } @@ -570,8 +610,8 @@ bool SdlEventSource::handleKeyDown(SDL_Event &ev, Common::Event &event) { return true; event.type = Common::EVENT_KEYDOWN; - event.kbd.keycode = SDLToOSystemKeycode(ev.key.keysym.sym); - event.kbd.ascii = mapKey(ev.key.keysym.sym, (SDLMod)ev.key.keysym.mod, obtainUnicode(ev.key.keysym)); + event.kbd.keycode = SDLToOSystemKeycode(sdlKeycode); + event.kbd.ascii = mapKey(sdlKeycode, (SDLMod)ev.key.keysym.mod, obtainUnicode(ev.key.keysym)); return true; } @@ -580,6 +620,7 @@ bool SdlEventSource::handleKeyUp(SDL_Event &ev, Common::Event &event) { if (remapKey(ev, event)) return true; + SDLKey sdlKeycode = obtainKeycode(ev.key.keysym); SDLMod mod = SDL_GetModState(); // Check if this is an event handled by handleKeyDown(), and stop if it is @@ -587,21 +628,21 @@ bool SdlEventSource::handleKeyUp(SDL_Event &ev, Common::Event &event) { // Check if the Ctrl key is down, so that we can trap cases where the // user has the Ctrl key down, and has just released a special key if (mod & KMOD_CTRL) { - if (ev.key.keysym.sym == 'm' || // Ctrl-m toggles mouse capture + if (sdlKeycode == 'm' || // Ctrl-m toggles mouse capture #if defined(MACOSX) // Meta - Q, handled below #elif defined(POSIX) - ev.key.keysym.sym == 'q' || // On other *nix systems, Control-Q quits + sdlKeycode == 'q' || // On other *nix systems, Control-Q quits #else - ev.key.keysym.sym == 'z' || // Ctrl-z quit + sdlKeycode == 'z' || // Ctrl-z quit #endif - ev.key.keysym.sym == 'u') // Ctrl-u toggles mute + sdlKeycode == 'u') // Ctrl-u toggles mute return false; } // Same for other keys (Meta and Alt) #if defined(MACOSX) - if ((mod & KMOD_META) && ev.key.keysym.sym == 'q') + if ((mod & KMOD_META) && sdlKeycode == 'q') return false; // On Macintosh, Cmd-Q quits #endif @@ -609,8 +650,8 @@ bool SdlEventSource::handleKeyUp(SDL_Event &ev, Common::Event &event) { // continue normally event.type = Common::EVENT_KEYUP; - event.kbd.keycode = SDLToOSystemKeycode(ev.key.keysym.sym); - event.kbd.ascii = mapKey(ev.key.keysym.sym, (SDLMod)ev.key.keysym.mod, 0); + event.kbd.keycode = SDLToOSystemKeycode(sdlKeycode); + event.kbd.ascii = mapKey(sdlKeycode, (SDLMod)ev.key.keysym.mod, 0); // Ctrl-Alt-<key> will change the GFX mode SDLModToOSystemKeyFlags(mod, event); @@ -845,7 +886,7 @@ bool SdlEventSource::remapKey(SDL_Event &ev, Common::Event &event) { return false; } -void SdlEventSource::resetKeyboadEmulation(int16 x_max, int16 y_max) { +void SdlEventSource::resetKeyboardEmulation(int16 x_max, int16 y_max) { _km.x_max = x_max; _km.y_max = y_max; _km.delay_time = 25; @@ -868,10 +909,63 @@ bool SdlEventSource::handleResizeEvent(Common::Event &event, int w, int h) { return false; } +SDLKey SdlEventSource::obtainKeycode(const SDL_keysym keySym) { +#if !SDL_VERSION_ATLEAST(2, 0, 0) && defined(WIN32) && !defined(_WIN32_WCE) + // WORKAROUND: SDL 1.2 on Windows does not use the user configured keyboard layout, + // resulting in "keySym.sym" values to always be those expected for an US keyboard. + // For example, SDL returns SDLK_Q when pressing the 'A' key on an AZERTY keyboard. + // This defeats the purpose of keycodes which is to be able to refer to a key without + // knowing where it is physically located. + // We work around this issue by querying the currently active Windows keyboard layout + // using the scancode provided by SDL. + + if (keySym.sym >= SDLK_0 && keySym.sym <= SDLK_9) { + // The keycode returned by SDL is kept for the number keys. + // Querying the keyboard layout for those would return the base key values + // for AZERTY keyboards, which are not numbers. For example, SDLK_1 would + // map to SDLK_AMPERSAND. This is theoretically correct but practically unhelpful, + // because it makes it impossible to handle key combinations such as "ctrl-1". + return keySym.sym; + } + + int vk = MapVirtualKey(keySym.scancode, MAPVK_VSC_TO_VK); + if (vk) { + int ch = (MapVirtualKey(vk, MAPVK_VK_TO_CHAR) & 0x7FFF); + // The top bit of the result of MapVirtualKey with MAPVK_VSC_TO_VK signals + // a dead key was pressed. In that case we keep the value of the accent alone. + if (ch) { + if (ch >= 'A' && ch <= 'Z') { + // Windows returns uppercase ASCII whereas SDL expects lowercase + return (SDLKey)(SDLK_a + (ch - 'A')); + } else { + return (SDLKey)ch; + } + } + } +#endif + + return keySym.sym; +} + uint32 SdlEventSource::obtainUnicode(const SDL_keysym keySym) { #if SDL_VERSION_ATLEAST(2, 0, 0) SDL_Event events[2]; + // Update the event queue here to give SDL a chance to insert TEXTINPUT + // events for KEYDOWN events. Otherwise we have a high chance that on + // Windows the TEXTINPUT event is not in the event queue at this point. + // In this case we will get two events with ascii values due to mapKey + // and dispatchSDLEvent. This results in nasty double input of characters + // in the GUI. + // + // FIXME: This is all a bit fragile because in mapKey we derive the ascii + // value from the key code if no unicode value is given. This is legacy + // behavior and should be removed anyway. If that is removed, we might not + // even need to do this peeking here but instead can rely on the + // SDL_TEXTINPUT case in dispatchSDLEvent to introduce keydown/keyup with + // proper ASCII values (but with KEYCODE_INVALID as keycode). + SDL_PumpEvents(); + // In SDL2, the unicode field has been removed from the keysym struct. // Instead a SDL_TEXTINPUT event is generated on key combinations that // generates unicode. diff --git a/backends/events/sdl/sdl-events.h b/backends/events/sdl/sdl-events.h index caa60c1354..c43699420b 100644 --- a/backends/events/sdl/sdl-events.h +++ b/backends/events/sdl/sdl-events.h @@ -47,7 +47,7 @@ public: /** * Resets keyboard emulation after a video screen change */ - virtual void resetKeyboadEmulation(int16 x_max, int16 y_max); + virtual void resetKeyboardEmulation(int16 x_max, int16 y_max); protected: /** @name Keyboard mouse emulation @@ -125,7 +125,7 @@ protected: /** * Maps the ASCII value of key */ - int mapKey(SDLKey key, SDLMod mod, Uint16 unicode); + virtual int mapKey(SDLKey key, SDLMod mod, Uint16 unicode); /** * Configures the key modifiers flags status @@ -148,6 +148,11 @@ protected: */ uint32 obtainUnicode(const SDL_keysym keySym); + /** + * Extracts the keycode for the specified key sym. + */ + SDLKey obtainKeycode(const SDL_keysym keySym); + #if SDL_VERSION_ATLEAST(2, 0, 0) /** * Whether _fakeKeyUp contains an event we need to send. diff --git a/backends/fs/abstract-fs.h b/backends/fs/abstract-fs.h index dcfdc08975..24286b649d 100644 --- a/backends/fs/abstract-fs.h +++ b/backends/fs/abstract-fs.h @@ -191,6 +191,15 @@ public: * @return pointer to the stream object, 0 in case of a failure */ virtual Common::WriteStream *createWriteStream() = 0; + + /** + * Creates a file referred by this node. + * + * @param isDirectory true if created file must be a directory + * + * @return true if file is created successfully + */ + virtual bool create(bool isDirectory) = 0; }; diff --git a/backends/fs/amigaos4/amigaos4-fs.cpp b/backends/fs/amigaos4/amigaos4-fs.cpp index 6d5b099736..24a8fb98ad 100644 --- a/backends/fs/amigaos4/amigaos4-fs.cpp +++ b/backends/fs/amigaos4/amigaos4-fs.cpp @@ -443,4 +443,9 @@ Common::WriteStream *AmigaOSFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool AmigaOSFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //defined(__amigaos4__) diff --git a/backends/fs/amigaos4/amigaos4-fs.h b/backends/fs/amigaos4/amigaos4-fs.h index 408354888e..3ed45d3f77 100644 --- a/backends/fs/amigaos4/amigaos4-fs.h +++ b/backends/fs/amigaos4/amigaos4-fs.h @@ -116,6 +116,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; diff --git a/backends/fs/chroot/chroot-fs.cpp b/backends/fs/chroot/chroot-fs.cpp index 2cbb4af9d6..be6805bbd8 100644 --- a/backends/fs/chroot/chroot-fs.cpp +++ b/backends/fs/chroot/chroot-fs.cpp @@ -108,6 +108,11 @@ Common::WriteStream *ChRootFilesystemNode::createWriteStream() { return _realNode->createWriteStream(); } +bool ChRootFilesystemNode::create(bool /*isDir*/) { + error("Not supported"); + return false; +} + Common::String ChRootFilesystemNode::addPathComponent(const Common::String &path, const Common::String &component) { const char sep = '/'; if (path.lastChar() == sep && component.firstChar() == sep) { diff --git a/backends/fs/chroot/chroot-fs.h b/backends/fs/chroot/chroot-fs.h index 9ff913be31..e0ecc1c47e 100644 --- a/backends/fs/chroot/chroot-fs.h +++ b/backends/fs/chroot/chroot-fs.h @@ -49,6 +49,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: static Common::String addPathComponent(const Common::String &path, const Common::String &component); diff --git a/backends/fs/ds/ds-fs.cpp b/backends/fs/ds/ds-fs.cpp index 3782caf432..83b1295838 100644 --- a/backends/fs/ds/ds-fs.cpp +++ b/backends/fs/ds/ds-fs.cpp @@ -211,6 +211,11 @@ Common::WriteStream *DSFileSystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool DSFileSystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + ////////////////////////////////////////////////////////////////////////// // GBAMPFileSystemNode - File system using GBA Movie Player and CF card // ////////////////////////////////////////////////////////////////////////// @@ -393,6 +398,11 @@ Common::WriteStream *GBAMPFileSystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool GBAMPFileSystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + DSFileStream::DSFileStream(void *handle) : _handle(handle) { diff --git a/backends/fs/ds/ds-fs.h b/backends/fs/ds/ds-fs.h index b9ccfcbe9c..939d1a1555 100644 --- a/backends/fs/ds/ds-fs.h +++ b/backends/fs/ds/ds-fs.h @@ -91,6 +91,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); /** * Returns the zip file this node points to. @@ -156,6 +157,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; struct fileHandle { diff --git a/backends/fs/n64/n64-fs.cpp b/backends/fs/n64/n64-fs.cpp index fe37dad467..d568500d30 100644 --- a/backends/fs/n64/n64-fs.cpp +++ b/backends/fs/n64/n64-fs.cpp @@ -160,4 +160,9 @@ Common::WriteStream *N64FilesystemNode::createWriteStream() { return RomfsStream::makeFromPath(getPath(), true); } +bool N64FilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#ifdef __N64__ diff --git a/backends/fs/n64/n64-fs.h b/backends/fs/n64/n64-fs.h index d503a6601e..d520ad5be6 100644 --- a/backends/fs/n64/n64-fs.h +++ b/backends/fs/n64/n64-fs.h @@ -73,6 +73,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/posix/posix-fs.cpp b/backends/fs/posix/posix-fs.cpp index 1a6c4e40a5..ce5715210a 100644 --- a/backends/fs/posix/posix-fs.cpp +++ b/backends/fs/posix/posix-fs.cpp @@ -39,6 +39,7 @@ #include <dirent.h> #include <stdio.h> #include <errno.h> +#include <fcntl.h> #ifdef __OS2__ #define INCL_DOS @@ -252,6 +253,35 @@ Common::WriteStream *POSIXFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool POSIXFilesystemNode::create(bool isDir) { + bool success; + + if (isDir) { + success = mkdir(_path.c_str(), 0755) == 0; + } else { + int fd = open(_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0755); + success = fd >= 0; + + if (fd >= 0) { + close(fd); + } + } + + if (success) { + setFlags(); + if (_isValid) { + if (_isDirectory != isDir) warning("failed to create %s: got %s", isDir ? "directory" : "file", _isDirectory ? "directory" : "file"); + return _isDirectory == isDir; + } + + warning("POSIXFilesystemNode: %s() was a success, but stat indicates there is no such %s", + isDir ? "mkdir" : "creat", isDir ? "directory" : "file"); + return false; + } + + return false; +} + namespace Posix { bool assureDirectoryExists(const Common::String &dir, const char *prefix) { diff --git a/backends/fs/posix/posix-fs.h b/backends/fs/posix/posix-fs.h index 0703ac5bf5..4ebce7e9d9 100644 --- a/backends/fs/posix/posix-fs.h +++ b/backends/fs/posix/posix-fs.h @@ -73,6 +73,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: /** diff --git a/backends/fs/ps2/ps2-fs.cpp b/backends/fs/ps2/ps2-fs.cpp index 9b6e1270f1..8391e063a0 100644 --- a/backends/fs/ps2/ps2-fs.cpp +++ b/backends/fs/ps2/ps2-fs.cpp @@ -441,4 +441,9 @@ Common::WriteStream *Ps2FilesystemNode::createWriteStream() { return PS2FileStream::makeFromPath(getPath(), true); } +bool Ps2FilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif diff --git a/backends/fs/ps2/ps2-fs.h b/backends/fs/ps2/ps2-fs.h index 63b866ba5b..c9da434535 100644 --- a/backends/fs/ps2/ps2-fs.h +++ b/backends/fs/ps2/ps2-fs.h @@ -96,6 +96,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); int getDev() { return 0; } }; diff --git a/backends/fs/psp/psp-fs.cpp b/backends/fs/psp/psp-fs.cpp index e8aad9fa98..6bd5e93435 100644 --- a/backends/fs/psp/psp-fs.cpp +++ b/backends/fs/psp/psp-fs.cpp @@ -239,4 +239,9 @@ Common::WriteStream *PSPFilesystemNode::createWriteStream() { return Common::wrapBufferedWriteStream(stream, WRITE_BUFFER_SIZE); } +bool PSPFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#ifdef __PSP__ diff --git a/backends/fs/psp/psp-fs.h b/backends/fs/psp/psp-fs.h index 1bb4543d19..648544650b 100644 --- a/backends/fs/psp/psp-fs.h +++ b/backends/fs/psp/psp-fs.h @@ -65,6 +65,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/symbian/symbian-fs.cpp b/backends/fs/symbian/symbian-fs.cpp index 8fbc3a402a..e4163caa33 100644 --- a/backends/fs/symbian/symbian-fs.cpp +++ b/backends/fs/symbian/symbian-fs.cpp @@ -231,4 +231,10 @@ Common::SeekableReadStream *SymbianFilesystemNode::createReadStream() { Common::WriteStream *SymbianFilesystemNode::createWriteStream() { return SymbianStdioStream::makeFromPath(getPath(), true); } + +bool SymbianFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#if defined(__SYMBIAN32__) diff --git a/backends/fs/symbian/symbian-fs.h b/backends/fs/symbian/symbian-fs.h index 339e998a28..1327f9f2e9 100644 --- a/backends/fs/symbian/symbian-fs.h +++ b/backends/fs/symbian/symbian-fs.h @@ -66,6 +66,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/wii/wii-fs.cpp b/backends/fs/wii/wii-fs.cpp index 43f4f592b7..e036b75019 100644 --- a/backends/fs/wii/wii-fs.cpp +++ b/backends/fs/wii/wii-fs.cpp @@ -168,7 +168,7 @@ bool WiiFilesystemNode::getChildren(AbstractFSList &list, ListMode mode, bool hi if (newPath.lastChar() != '/') newPath += '/'; newPath += pent->d_name; - + bool isDir = false; tmpdir = opendir(newPath.c_str()); if(tmpdir) @@ -176,17 +176,17 @@ bool WiiFilesystemNode::getChildren(AbstractFSList &list, ListMode mode, bool hi isDir = true; closedir(tmpdir); } - + if ((mode == Common::FSNode::kListFilesOnly && isDir) || (mode == Common::FSNode::kListDirectoriesOnly && !isDir)) continue; - + struct stat st; st.st_mode = 0; st.st_mode |= ( isDir ? S_IFDIR : 0 ); st.st_mode |= S_IRUSR; st.st_mode |= S_IWUSR; - + list.push_back(new WiiFilesystemNode(newPath, &st)); } @@ -213,4 +213,9 @@ Common::WriteStream *WiiFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool WiiFilesystemNode::create(bool isDirectory) { + error("Not supported"); + return false; +} + #endif //#if defined(__WII__) diff --git a/backends/fs/wii/wii-fs.h b/backends/fs/wii/wii-fs.h index c77c543dae..affb765884 100644 --- a/backends/fs/wii/wii-fs.h +++ b/backends/fs/wii/wii-fs.h @@ -69,6 +69,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); }; #endif diff --git a/backends/fs/windows/windows-fs.cpp b/backends/fs/windows/windows-fs.cpp index 49549b83cb..b43686f911 100644 --- a/backends/fs/windows/windows-fs.cpp +++ b/backends/fs/windows/windows-fs.cpp @@ -244,4 +244,36 @@ Common::WriteStream *WindowsFilesystemNode::createWriteStream() { return StdioStream::makeFromPath(getPath(), true); } +bool WindowsFilesystemNode::create(bool isDirectory) { + bool success; + + if (isDirectory) { + success = CreateDirectory(toUnicode(_path.c_str()), NULL) != 0; + } else { + success = CreateFile(toUnicode(_path.c_str()), GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL) != INVALID_HANDLE_VALUE; + } + + if (success) { + //this piece is copied from constructor, it checks that file exists and detects whether it's a directory + DWORD fileAttribs = GetFileAttributes(toUnicode(_path.c_str())); + if (fileAttribs != INVALID_FILE_ATTRIBUTES) { + _isDirectory = ((fileAttribs & FILE_ATTRIBUTE_DIRECTORY) != 0); + _isValid = true; + // Add a trailing slash, if necessary. + if (_isDirectory && _path.lastChar() != '\\') { + _path += '\\'; + } + + if (_isDirectory != isDirectory) warning("failed to create %s: got %s", isDirectory ? "directory" : "file", _isDirectory ? "directory" : "file"); + return _isDirectory == isDirectory; + } + + warning("WindowsFilesystemNode: Create%s() was a success, but GetFileAttributes() indicates there is no such %s", + isDirectory ? "Directory" : "File", isDirectory ? "directory" : "file"); + return false; + } + + return false; +} + #endif //#ifdef WIN32 diff --git a/backends/fs/windows/windows-fs.h b/backends/fs/windows/windows-fs.h index d06044603a..7c9f2c1e1a 100644 --- a/backends/fs/windows/windows-fs.h +++ b/backends/fs/windows/windows-fs.h @@ -88,6 +88,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream(); + virtual bool create(bool isDirectory); private: /** diff --git a/backends/graphics/androidsdl/androidsdl-graphics.cpp b/backends/graphics/androidsdl/androidsdl-graphics.cpp new file mode 100644 index 0000000000..23a1a86dd6 --- /dev/null +++ b/backends/graphics/androidsdl/androidsdl-graphics.cpp @@ -0,0 +1,42 @@ +/* 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(ANDROIDSDL) + +#include "backends/graphics/androidsdl/androidsdl-graphics.h" +#include "backends/events/androidsdl/androidsdl-events.h" +#include "common/mutex.h" +#include "common/textconsole.h" +#include "graphics/font.h" +#include "graphics/fontman.h" +#include "graphics/scaler.h" +#include "graphics/scaler/aspect.h" +#include "graphics/scaler/downscaler.h" +#include "graphics/surface.h" + +AndroidSdlGraphicsManager::AndroidSdlGraphicsManager(SdlEventSource *sdlEventSource, SdlWindow *window) + : SurfaceSdlGraphicsManager(sdlEventSource, window) { +} + +#endif diff --git a/backends/graphics/androidsdl/androidsdl-graphics.h b/backends/graphics/androidsdl/androidsdl-graphics.h new file mode 100644 index 0000000000..b7ca7c1de8 --- /dev/null +++ b/backends/graphics/androidsdl/androidsdl-graphics.h @@ -0,0 +1,34 @@ +/* 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 BACKENDS_GRAPHICS_SDL_ANDROIDSDL_H +#define BACKENDS_GRAPHICS_SDL_ANDROIDSDL_H + +#include "backends/graphics/surfacesdl/surfacesdl-graphics.h" + +class AndroidSdlGraphicsManager : public SurfaceSdlGraphicsManager { +public: + AndroidSdlGraphicsManager(SdlEventSource *sdlEventSource, SdlWindow *window); + +}; + +#endif diff --git a/backends/graphics/dinguxsdl/dinguxsdl-graphics.cpp b/backends/graphics/dinguxsdl/dinguxsdl-graphics.cpp index 0b9cc0c7e8..9e75dd10f4 100644 --- a/backends/graphics/dinguxsdl/dinguxsdl-graphics.cpp +++ b/backends/graphics/dinguxsdl/dinguxsdl-graphics.cpp @@ -35,6 +35,10 @@ static const OSystem::GraphicsMode s_supportedGraphicsModes[] = { {0, 0, 0} }; +#ifndef USE_SCALERS +#define DownscaleAllByHalf 0 +#endif + DINGUXSdlGraphicsManager::DINGUXSdlGraphicsManager(SdlEventSource *boss, SdlWindow *window) : SurfaceSdlGraphicsManager(boss, window) { } @@ -61,9 +65,11 @@ bool DINGUXSdlGraphicsManager::setGraphicsMode(int mode) { case GFX_NORMAL: newScaleFactor = 1; break; +#ifdef USE_SCALERS case GFX_HALF: newScaleFactor = 1; break; +#endif default: warning("unknown gfx mode %d", mode); return false; @@ -89,9 +95,11 @@ void DINGUXSdlGraphicsManager::setGraphicsModeIntern() { case GFX_NORMAL: newScalerProc = Normal1x; break; +#ifdef USE_SCALERS case GFX_HALF: newScalerProc = DownscaleAllByHalf; break; +#endif default: error("Unknown gfx mode %d", _videoMode.mode); @@ -249,26 +257,6 @@ void DINGUXSdlGraphicsManager::internUpdateScreen() { _forceFull = true; } -#ifdef USE_OSD - // OSD visible (i.e. non-transparent)? - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - // Updated alpha value - const int diff = SDL_GetTicks() - _osdFadeStartTime; - if (diff > 0) { - if (diff >= kOSDFadeOutDuration) { - // Back to full transparency - _osdAlpha = SDL_ALPHA_TRANSPARENT; - } else { - // Do a linear fade out... - const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; - } - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); - _forceFull = true; - } - } -#endif - if (!_overlayVisible) { origSurf = _screen; srcSurf = _tmpscreen; @@ -290,6 +278,10 @@ void DINGUXSdlGraphicsManager::internUpdateScreen() { if (_mouseNeedsRedraw) undrawMouse(); +#ifdef USE_OSD + updateOSD(); +#endif + // Force a full redraw if requested if (_forceFull) { _numDirtyRects = 1; @@ -397,9 +389,7 @@ void DINGUXSdlGraphicsManager::internUpdateScreen() { drawMouse(); #ifdef USE_OSD - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); - } + drawOSD(); #endif // Finally, blit all our changes to the screen SDL_UpdateRects(_hwscreen, _numDirtyRects, _dirtyRectList); diff --git a/backends/graphics/gph/gph-graphics.cpp b/backends/graphics/gph/gph-graphics.cpp index 65cb3d1d65..a19512110f 100644 --- a/backends/graphics/gph/gph-graphics.cpp +++ b/backends/graphics/gph/gph-graphics.cpp @@ -277,26 +277,6 @@ void GPHGraphicsManager::internUpdateScreen() { _forceFull = true; } -#ifdef USE_OSD - // OSD visible (i.e. non-transparent)? - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - // Updated alpha value - const int diff = SDL_GetTicks() - _osdFadeStartTime; - if (diff > 0) { - if (diff >= kOSDFadeOutDuration) { - // Back to full transparency - _osdAlpha = SDL_ALPHA_TRANSPARENT; - } else { - // Do a linear fade out... - const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; - } - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); - _forceFull = true; - } - } -#endif - if (!_overlayVisible) { origSurf = _screen; srcSurf = _tmpscreen; @@ -319,6 +299,10 @@ void GPHGraphicsManager::internUpdateScreen() { if (_mouseNeedsRedraw) undrawMouse(); +#ifdef USE_OSD + updateOSD(); +#endif + // Force a full redraw if requested if (_forceFull) { _numDirtyRects = 1; @@ -428,10 +412,9 @@ void GPHGraphicsManager::internUpdateScreen() { drawMouse(); #ifdef USE_OSD - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); - } + drawOSD(); #endif + // Finally, blit all our changes to the screen SDL_UpdateRects(_hwscreen, _numDirtyRects, _dirtyRectList); } @@ -459,7 +442,7 @@ void GPHGraphicsManager::hideOverlay() { bool GPHGraphicsManager::loadGFXMode() { - // We don't offer anything other than fullscreen on GPH devices so let’s not even pretend. + // We don't offer anything other than fullscreen on GPH devices so let's not even pretend. _videoMode.fullscreen = true; // Set the hardware stats to match the LCD. diff --git a/backends/graphics/graphics.h b/backends/graphics/graphics.h index 3671b9f0b9..db8b6a920b 100644 --- a/backends/graphics/graphics.h +++ b/backends/graphics/graphics.h @@ -84,6 +84,8 @@ public: virtual void setCursorPalette(const byte *colors, uint start, uint num) = 0; virtual void displayMessageOnOSD(const char *msg) {} + virtual void displayActivityIconOnOSD(const Graphics::Surface *icon) {} + // Graphics::PaletteManager interface //virtual void setPalette(const byte *colors, uint start, uint num) = 0; diff --git a/backends/graphics/linuxmotosdl/linuxmotosdl-graphics.cpp b/backends/graphics/linuxmotosdl/linuxmotosdl-graphics.cpp index 52e5b42e8b..6a920e9eae 100644 --- a/backends/graphics/linuxmotosdl/linuxmotosdl-graphics.cpp +++ b/backends/graphics/linuxmotosdl/linuxmotosdl-graphics.cpp @@ -290,26 +290,6 @@ void LinuxmotoSdlGraphicsManager::internUpdateScreen() { _forceFull = true; } -#ifdef USE_OSD - // OSD visible (i.e. non-transparent)? - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - // Updated alpha value - const int diff = SDL_GetTicks() - _osdFadeStartTime; - if (diff > 0) { - if (diff >= kOSDFadeOutDuration) { - // Back to full transparency - _osdAlpha = SDL_ALPHA_TRANSPARENT; - } else { - // Do a linear fade out... - const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; - } - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); - _forceFull = true; - } - } -#endif - if (!_overlayVisible) { origSurf = _screen; srcSurf = _tmpscreen; @@ -331,6 +311,10 @@ void LinuxmotoSdlGraphicsManager::internUpdateScreen() { if (_mouseNeedsRedraw) undrawMouse(); +#ifdef USE_OSD + updateOSD(); +#endif + // Force a full redraw if requested if (_forceFull) { _numDirtyRects = 1; @@ -439,10 +423,9 @@ void LinuxmotoSdlGraphicsManager::internUpdateScreen() { drawMouse(); #ifdef USE_OSD - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); - } + drawOSD(); #endif + // Finally, blit all our changes to the screen SDL_UpdateRects(_hwscreen, _numDirtyRects, _dirtyRectList); } diff --git a/backends/graphics/opengl/context.cpp b/backends/graphics/opengl/context.cpp new file mode 100644 index 0000000000..a7f640d37e --- /dev/null +++ b/backends/graphics/opengl/context.cpp @@ -0,0 +1,182 @@ +/* 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 "backends/graphics/opengl/opengl-sys.h" +#include "backends/graphics/opengl/opengl-graphics.h" +#include "backends/graphics/opengl/shader.h" +#include "backends/graphics/opengl/pipelines/pipeline.h" +#include "backends/graphics/opengl/framebuffer.h" + +#include "common/tokenizer.h" +#include "common/debug.h" + +namespace OpenGL { + +void Context::reset() { + maxTextureSize = 0; + + NPOTSupported = false; + shadersSupported = false; + multitextureSupported = false; + framebufferObjectSupported = false; + +#define GL_FUNC_DEF(ret, name, param) name = nullptr; +#include "backends/graphics/opengl/opengl-func.h" +#undef GL_FUNC_DEF + + activePipeline = nullptr; +} + +Pipeline *Context::setPipeline(Pipeline *pipeline) { + Pipeline *oldPipeline = activePipeline; + if (oldPipeline) { + oldPipeline->deactivate(); + } + + activePipeline = pipeline; + if (activePipeline) { + activePipeline->activate(); + } + + return oldPipeline; +} + +Context g_context; + +void OpenGLGraphicsManager::setContextType(ContextType type) { +#if USE_FORCED_GL + type = kContextGL; +#elif USE_FORCED_GLES + type = kContextGLES; +#elif USE_FORCED_GLES2 + type = kContextGLES2; +#endif + + g_context.type = type; +} + +void OpenGLGraphicsManager::initializeGLContext() { + // Initialize default state. + g_context.reset(); + + // Load all functions. + // We use horrible trickery to silence C++ compilers. + // See backends/plugins/sdl/sdl-provider.cpp for more information. + assert(sizeof(void (*)()) == sizeof(void *)); + void *fn = nullptr; + +#define LOAD_FUNC(name, loadName) \ + fn = getProcAddress(#loadName); \ + memcpy(&g_context.name, &fn, sizeof(fn)) + +#define GL_EXT_FUNC_DEF(ret, name, param) LOAD_FUNC(name, name) + +#ifdef USE_BUILTIN_OPENGL +#define GL_FUNC_DEF(ret, name, param) g_context.name = &name +#define GL_FUNC_2_DEF GL_FUNC_DEF +#else +#define GL_FUNC_DEF GL_EXT_FUNC_DEF +#define GL_FUNC_2_DEF(ret, name, extName, param) \ + if (g_context.type == kContextGL) { \ + LOAD_FUNC(name, extName); \ + } else { \ + LOAD_FUNC(name, name); \ + } +#endif +#include "backends/graphics/opengl/opengl-func.h" +#undef GL_FUNC_2_DEF +#undef GL_FUNC_DEF +#undef GL_EXT_FUNC_DEF +#undef LOAD_FUNC + + // Obtain maximum texture size. + GL_CALL(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &g_context.maxTextureSize)); + debug(5, "OpenGL maximum texture size: %d", g_context.maxTextureSize); + + const char *extString = (const char *)g_context.glGetString(GL_EXTENSIONS); + debug(5, "OpenGL extensions: %s", extString); + + bool ARBShaderObjects = false; + bool ARBShadingLanguage100 = false; + bool ARBVertexShader = false; + bool ARBFragmentShader = false; + + Common::StringTokenizer tokenizer(extString, " "); + while (!tokenizer.empty()) { + Common::String token = tokenizer.nextToken(); + + if (token == "GL_ARB_texture_non_power_of_two" || token == "GL_OES_texture_npot") { + g_context.NPOTSupported = true; + } else if (token == "GL_ARB_shader_objects") { + ARBShaderObjects = true; + } else if (token == "GL_ARB_shading_language_100") { + ARBShadingLanguage100 = true; + } else if (token == "GL_ARB_vertex_shader") { + ARBVertexShader = true; + } else if (token == "GL_ARB_fragment_shader") { + ARBFragmentShader = true; + } else if (token == "GL_ARB_multitexture") { + g_context.multitextureSupported = true; + } else if (token == "GL_EXT_framebuffer_object") { + g_context.framebufferObjectSupported = true; + } + } + + if (g_context.type == kContextGLES2) { + // GLES2 always has (limited) NPOT support. + g_context.NPOTSupported = true; + + // GLES2 always has shader support. + g_context.shadersSupported = true; + + // GLES2 always has multi texture support. + g_context.multitextureSupported = true; + + // GLES2 always has FBO support. + g_context.framebufferObjectSupported = true; + } else { + g_context.shadersSupported = ARBShaderObjects & ARBShadingLanguage100 & ARBVertexShader & ARBFragmentShader; + } + + // Log context type. + switch (g_context.type) { + case kContextGL: + debug(5, "OpenGL: GL context initialized"); + break; + + case kContextGLES: + debug(5, "OpenGL: GLES context initialized"); + break; + + case kContextGLES2: + debug(5, "OpenGL: GLES2 context initialized"); + break; + } + + // Log features supported by GL context. + debug(5, "OpenGL: NPOT texture support: %d", g_context.NPOTSupported); + debug(5, "OpenGL: Shader support: %d", g_context.shadersSupported); + debug(5, "OpenGL: Multitexture support: %d", g_context.multitextureSupported); + debug(5, "OpenGL: FBO support: %d", g_context.framebufferObjectSupported); +} + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/debug.cpp b/backends/graphics/opengl/debug.cpp index d5d73fb5ec..c4319f5e36 100644 --- a/backends/graphics/opengl/debug.cpp +++ b/backends/graphics/opengl/debug.cpp @@ -54,7 +54,7 @@ Common::String getGLErrStr(GLenum error) { void checkGLError(const char *expr, const char *file, int line) { GLenum error; - while ((error = glGetError()) != GL_NO_ERROR) { + while ((error = g_context.glGetError()) != GL_NO_ERROR) { // We cannot use error here because we do not know whether we have a // working screen or not. warning("GL ERROR: %s on %s (%s:%d)", getGLErrStr(error).c_str(), expr, file, line); diff --git a/backends/graphics/opengl/debug.h b/backends/graphics/opengl/debug.h index ff6b678870..abaa6544dc 100644 --- a/backends/graphics/opengl/debug.h +++ b/backends/graphics/opengl/debug.h @@ -31,9 +31,9 @@ namespace OpenGL { void checkGLError(const char *expr, const char *file, int line); } // End of namespace OpenGL -#define GLCALL(x) do { (x); OpenGL::checkGLError(#x, __FILE__, __LINE__); } while (false) +#define GL_WRAP_DEBUG(call, name) do { (call); OpenGL::checkGLError(#name, __FILE__, __LINE__); } while (false) #else -#define GLCALL(x) do { (x); } while (false) +#define GL_WRAP_DEBUG(call, name) do { (call); } while (false) #endif #endif diff --git a/backends/graphics/opengl/framebuffer.cpp b/backends/graphics/opengl/framebuffer.cpp new file mode 100644 index 0000000000..7191aab8bc --- /dev/null +++ b/backends/graphics/opengl/framebuffer.cpp @@ -0,0 +1,259 @@ +/* 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 "backends/graphics/opengl/framebuffer.h" +#include "backends/graphics/opengl/texture.h" +#include "backends/graphics/opengl/pipelines/pipeline.h" + +namespace OpenGL { + +Framebuffer::Framebuffer() + : _viewport(), _projectionMatrix(), _isActive(false), _clearColor(), + _blendState(false), _scissorTestState(false), _scissorBox() { +} + +void Framebuffer::activate() { + _isActive = true; + + applyViewport(); + applyProjectionMatrix(); + applyClearColor(); + applyBlendState(); + applyScissorTestState(); + applyScissorBox(); + + activateInternal(); +} + +void Framebuffer::deactivate() { + _isActive = false; + + deactivateInternal(); +} + +void Framebuffer::setClearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { + _clearColor[0] = r; + _clearColor[1] = g; + _clearColor[2] = b; + _clearColor[3] = a; + + // Directly apply changes when we are active. + if (isActive()) { + applyClearColor(); + } +} + +void Framebuffer::enableBlend(bool enable) { + _blendState = enable; + + // Directly apply changes when we are active. + if (isActive()) { + applyBlendState(); + } +} + +void Framebuffer::enableScissorTest(bool enable) { + _scissorTestState = enable; + + // Directly apply changes when we are active. + if (isActive()) { + applyScissorTestState(); + } +} + +void Framebuffer::setScissorBox(GLint x, GLint y, GLsizei w, GLsizei h) { + _scissorBox[0] = x; + _scissorBox[1] = y; + _scissorBox[2] = w; + _scissorBox[3] = h; + + // Directly apply changes when we are active. + if (isActive()) { + applyScissorBox(); + } +} + +void Framebuffer::applyViewport() { + GL_CALL(glViewport(_viewport[0], _viewport[1], _viewport[2], _viewport[3])); +} + +void Framebuffer::applyProjectionMatrix() { + g_context.getActivePipeline()->setProjectionMatrix(_projectionMatrix); +} + +void Framebuffer::applyClearColor() { + GL_CALL(glClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3])); +} + +void Framebuffer::applyBlendState() { + if (_blendState) { + GL_CALL(glEnable(GL_BLEND)); + } else { + GL_CALL(glDisable(GL_BLEND)); + } +} + +void Framebuffer::applyScissorTestState() { + if (_scissorTestState) { + GL_CALL(glEnable(GL_SCISSOR_TEST)); + } else { + GL_CALL(glDisable(GL_SCISSOR_TEST)); + } +} + +void Framebuffer::applyScissorBox() { + GL_CALL(glScissor(_scissorBox[0], _scissorBox[1], _scissorBox[2], _scissorBox[3])); +} + +// +// Backbuffer implementation +// + +void Backbuffer::activateInternal() { +#if !USE_FORCED_GLES + if (g_context.framebufferObjectSupported) { + GL_CALL(glBindFramebuffer(GL_FRAMEBUFFER, 0)); + } +#endif +} + +void Backbuffer::setDimensions(uint width, uint height) { + // Set viewport dimensions. + _viewport[0] = 0; + _viewport[1] = 0; + _viewport[2] = width; + _viewport[3] = height; + + // Setup orthogonal projection matrix. + _projectionMatrix[ 0] = 2.0f / width; + _projectionMatrix[ 1] = 0.0f; + _projectionMatrix[ 2] = 0.0f; + _projectionMatrix[ 3] = 0.0f; + + _projectionMatrix[ 4] = 0.0f; + _projectionMatrix[ 5] = -2.0f / height; + _projectionMatrix[ 6] = 0.0f; + _projectionMatrix[ 7] = 0.0f; + + _projectionMatrix[ 8] = 0.0f; + _projectionMatrix[ 9] = 0.0f; + _projectionMatrix[10] = 0.0f; + _projectionMatrix[11] = 0.0f; + + _projectionMatrix[12] = -1.0f; + _projectionMatrix[13] = 1.0f; + _projectionMatrix[14] = 0.0f; + _projectionMatrix[15] = 1.0f; + + // Directly apply changes when we are active. + if (isActive()) { + applyViewport(); + applyProjectionMatrix(); + } +} + +// +// Render to texture target implementation +// + +#if !USE_FORCED_GLES +TextureTarget::TextureTarget() + : _texture(new GLTexture(GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE)), _glFBO(0), _needUpdate(true) { +} + +TextureTarget::~TextureTarget() { + delete _texture; + GL_CALL_SAFE(glDeleteFramebuffers, (1, &_glFBO)); +} + +void TextureTarget::activateInternal() { + // Allocate framebuffer object if necessary. + if (!_glFBO) { + GL_CALL(glGenFramebuffers(1, &_glFBO)); + _needUpdate = true; + } + + // Attach destination texture to FBO. + GL_CALL(glBindFramebuffer(GL_FRAMEBUFFER, _glFBO)); + + // If required attach texture to FBO. + if (_needUpdate) { + GL_CALL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture->getGLTexture(), 0)); + _needUpdate = false; + } +} + +void TextureTarget::destroy() { + GL_CALL(glDeleteFramebuffers(1, &_glFBO)); + _glFBO = 0; + + _texture->destroy(); +} + +void TextureTarget::create() { + _texture->create(); + + _needUpdate = true; +} + +void TextureTarget::setSize(uint width, uint height) { + _texture->setSize(width, height); + + const uint texWidth = _texture->getWidth(); + const uint texHeight = _texture->getHeight(); + + // Set viewport dimensions. + _viewport[0] = 0; + _viewport[1] = 0; + _viewport[2] = texWidth; + _viewport[3] = texHeight; + + // Setup orthogonal projection matrix. + _projectionMatrix[ 0] = 2.0f / texWidth; + _projectionMatrix[ 1] = 0.0f; + _projectionMatrix[ 2] = 0.0f; + _projectionMatrix[ 3] = 0.0f; + + _projectionMatrix[ 4] = 0.0f; + _projectionMatrix[ 5] = 2.0f / texHeight; + _projectionMatrix[ 6] = 0.0f; + _projectionMatrix[ 7] = 0.0f; + + _projectionMatrix[ 8] = 0.0f; + _projectionMatrix[ 9] = 0.0f; + _projectionMatrix[10] = 0.0f; + _projectionMatrix[11] = 0.0f; + + _projectionMatrix[12] = -1.0f; + _projectionMatrix[13] = -1.0f; + _projectionMatrix[14] = 0.0f; + _projectionMatrix[15] = 1.0f; + + // Directly apply changes when we are active. + if (isActive()) { + applyViewport(); + applyProjectionMatrix(); + } +} +#endif // !USE_FORCED_GLES + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/framebuffer.h b/backends/graphics/opengl/framebuffer.h new file mode 100644 index 0000000000..c44c98ddc4 --- /dev/null +++ b/backends/graphics/opengl/framebuffer.h @@ -0,0 +1,175 @@ +/* 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 BACKENDS_GRAPHICS_OPENGL_FRAMEBUFFER_H +#define BACKENDS_GRAPHICS_OPENGL_FRAMEBUFFER_H + +#include "backends/graphics/opengl/opengl-sys.h" + +namespace OpenGL { + +/** + * Object describing a framebuffer OpenGL can render to. + */ +class Framebuffer { + friend class Pipeline; +public: + Framebuffer(); + virtual ~Framebuffer() {}; + +public: + /** + * Set the clear color of the framebuffer. + */ + void setClearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a); + + /** + * Enable/disable GL_BLEND. + */ + void enableBlend(bool enable); + + /** + * Enable/disable GL_SCISSOR_TEST. + */ + void enableScissorTest(bool enable); + + /** + * Set scissor box dimensions. + */ + void setScissorBox(GLint x, GLint y, GLsizei w, GLsizei h); + + /** + * Obtain projection matrix of the framebuffer. + */ + const GLfloat *getProjectionMatrix() const { return _projectionMatrix; } +protected: + bool isActive() const { return _isActive; } + + GLint _viewport[4]; + void applyViewport(); + + GLfloat _projectionMatrix[4*4]; + void applyProjectionMatrix(); + + /** + * Activate framebuffer. + * + * This is supposed to set all state associated with the framebuffer. + */ + virtual void activateInternal() = 0; + + /** + * Deactivate framebuffer. + * + * This is supposed to make any cleanup required when unbinding the + * framebuffer. + */ + virtual void deactivateInternal() {} + +private: + /** + * Accessor to activate framebuffer for pipeline. + */ + void activate(); + + /** + * Accessor to deactivate framebuffer from pipeline. + */ + void deactivate(); + +private: + bool _isActive; + + GLfloat _clearColor[4]; + void applyClearColor(); + + bool _blendState; + void applyBlendState(); + + bool _scissorTestState; + void applyScissorTestState(); + + GLint _scissorBox[4]; + void applyScissorBox(); +}; + +/** + * Default back buffer implementation. + */ +class Backbuffer : public Framebuffer { +public: + /** + * Set the dimensions (a.k.a. size) of the back buffer. + */ + void setDimensions(uint width, uint height); + +protected: + virtual void activateInternal(); +}; + +#if !USE_FORCED_GLES +class GLTexture; + +/** + * Render to texture framebuffer implementation. + * + * This target allows to render to a texture, which can then be used for + * further rendering. + */ +class TextureTarget : public Framebuffer { +public: + TextureTarget(); + virtual ~TextureTarget(); + + /** + * Notify that the GL context is about to be destroyed. + */ + void destroy(); + + /** + * Notify that the GL context has been created. + */ + void create(); + + /** + * Set size of the texture target. + */ + void setSize(uint width, uint height); + + /** + * Query pointer to underlying GL texture. + */ + GLTexture *getTexture() const { return _texture; } + +protected: + virtual void activateInternal(); + +private: + GLTexture *_texture; + GLuint _glFBO; + bool _needUpdate; +}; +#endif + +} // End of namespace OpenGL + +#endif diff --git a/backends/graphics/opengl/opengl-defs.h b/backends/graphics/opengl/opengl-defs.h new file mode 100644 index 0000000000..733fc2933c --- /dev/null +++ b/backends/graphics/opengl/opengl-defs.h @@ -0,0 +1,262 @@ +/* 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. + * + */ + +/* This file is based on Mesa 3-D's gl.h and GLES/gl.h from Khronos Registry. + * + * Mesa 3-D's gl.h file is distributed under the following license: + * Mesa 3-D graphics library + * + * Copyright (C) 1999-2006 Brian Paul All Rights Reserved. + * Copyright (C) 2009 VMware, Inc. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * + * GLES/gl.h from Khronos Registry is distributed under the following license: + * This document is licensed under the SGI Free Software B License Version + * 2.0. For details, see http://oss.sgi.com/projects/FreeB/ . + */ + +#ifndef BACKENDS_GRAPHICS_OPENGL_OPENGL_DEFS_H +#define BACKENDS_GRAPHICS_OPENGL_OPENGL_DEFS_H + +#include "common/scummsys.h" + +/* + * Datatypes + */ +typedef uint GLenum; +typedef uint8 GLboolean; +typedef uint GLbitfield; +typedef void GLvoid; +typedef int8 GLbyte; /* 1-byte signed */ +typedef int16 GLshort; /* 2-byte signed */ +typedef int32 GLint; /* 4-byte signed */ +typedef uint8 GLubyte; /* 1-byte unsigned */ +typedef uint16 GLushort; /* 2-byte unsigned */ +typedef uint32 GLuint; /* 4-byte unsigned */ +typedef int32 GLsizei; /* 4-byte signed */ +typedef float GLfloat; /* single precision float */ +typedef float GLclampf; /* single precision float in [0,1] */ +typedef double GLdouble; /* double precision float */ +typedef double GLclampd; /* double precision float in [0,1] */ +typedef char GLchar; +#if defined(MACOSX) +typedef void *GLhandleARB; +#else +typedef uint GLhandleARB; +#endif + +// This is an addition from us to alias ARB shader object extensions to +// OpenGL (ES) 2.0 style functions. It only works when GLhandleARB and GLuint +// are type compatible. +typedef GLhandleARB GLprogram; +typedef GLhandleARB GLshader; + +/* + * Constants + */ + +/* Boolean constants */ +#define GL_FALSE 0 +#define GL_TRUE 1 + +/* StringName */ +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 +#define GL_EXTENSIONS 0x1F03 + +/* ErrorCode */ +#define GL_NO_ERROR 0 +#define GL_INVALID_ENUM 0x0500 +#define GL_INVALID_VALUE 0x0501 +#define GL_INVALID_OPERATION 0x0502 +#define GL_STACK_OVERFLOW 0x0503 +#define GL_STACK_UNDERFLOW 0x0504 +#define GL_OUT_OF_MEMORY 0x0505 + +/* ClearBufferMask */ +#define GL_DEPTH_BUFFER_BIT 0x00000100 +#define GL_STENCIL_BUFFER_BIT 0x00000400 +#define GL_COLOR_BUFFER_BIT 0x00004000 + +/* Scissor box */ +#define GL_SCISSOR_BOX 0x0C10 +#define GL_SCISSOR_TEST 0x0C11 + +/* MatrixMode */ +#define GL_MATRIX_MODE 0x0BA0 +#define GL_MODELVIEW 0x1700 +#define GL_PROJECTION 0x1701 +#define GL_TEXTURE 0x1702 + +/* EnableCap */ +#define GL_FOG 0x0B60 +#define GL_LIGHTING 0x0B50 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_CULL_FACE 0x0B44 +#define GL_ALPHA_TEST 0x0BC0 +#define GL_BLEND 0x0BE2 +#define GL_DITHER 0x0BD0 +#define GL_DEPTH_TEST 0x0B71 +#define GL_VERTEX_ARRAY 0x8074 +#define GL_COLOR_ARRAY 0x8076 +#define GL_TEXTURE_COORD_ARRAY 0x8078 + +/* ShadingModel */ +#define GL_FLAT 0x1D00 +#define GL_SMOOTH 0x1D01 + +/* HintMode */ +#define GL_DONT_CARE 0x1100 +#define GL_FASTEST 0x1101 +#define GL_NICEST 0x1102 + +/* HintTarget */ +#define GL_PERSPECTIVE_CORRECTION_HINT 0x0C50 +#define GL_POINT_SMOOTH_HINT 0x0C51 +#define GL_LINE_SMOOTH_HINT 0x0C52 +#define GL_FOG_HINT 0x0C54 +#define GL_GENERATE_MIPMAP_HINT 0x8192 + +/* BlendingFactorDest */ +#define GL_ZERO 0 +#define GL_ONE 1 +#define GL_SRC_COLOR 0x0300 +#define GL_ONE_MINUS_SRC_COLOR 0x0301 +#define GL_SRC_ALPHA 0x0302 +#define GL_ONE_MINUS_SRC_ALPHA 0x0303 +#define GL_DST_ALPHA 0x0304 +#define GL_ONE_MINUS_DST_ALPHA 0x0305 + +/* BlendingFactorSrc */ +/* GL_ZERO */ +/* GL_ONE */ +#define GL_DST_COLOR 0x0306 +#define GL_ONE_MINUS_DST_COLOR 0x0307 +#define GL_SRC_ALPHA_SATURATE 0x0308 +/* GL_SRC_ALPHA */ +/* GL_ONE_MINUS_SRC_ALPHA */ +/* GL_DST_ALPHA */ +/* GL_ONE_MINUS_DST_ALPHA */ + +/* PixelFormat */ +#define GL_ALPHA 0x1906 +#define GL_RGB 0x1907 +#define GL_RGBA 0x1908 +#define GL_BGR 0x80E0 +#define GL_BGRA 0x80E1 + +#define GL_RED 0x1903 +#define GL_R8 0x8229 + +/* PixelStoreParameter */ +#define GL_UNPACK_ALIGNMENT 0x0CF5 +#define GL_PACK_ALIGNMENT 0x0D05 + +/* DataType */ +#define GL_BYTE 0x1400 +#define GL_UNSIGNED_BYTE 0x1401 +#define GL_SHORT 0x1402 +#define GL_UNSIGNED_SHORT 0x1403 +#define GL_FLOAT 0x1406 +#define GL_FIXED 0x140C + +/* PixelType */ +/* GL_UNSIGNED_BYTE */ +#define GL_UNSIGNED_SHORT_4_4_4_4 0x8033 +#define GL_UNSIGNED_SHORT_5_5_5_1 0x8034 +#define GL_UNSIGNED_SHORT_5_6_5 0x8363 + +#define GL_UNSIGNED_SHORT_4_4_4_4_REV 0x8365 +#define GL_UNSIGNED_SHORT_5_6_5_REV 0x8364 +#define GL_UNSIGNED_SHORT_1_5_5_5_REV 0x8366 +#define GL_UNSIGNED_INT_8_8_8_8 0x8035 +#define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367 + +/* Implementation limits */ +#define GL_MAX_TEXTURE_SIZE 0x0D33 + +/* TextureMagFilter */ +#define GL_NEAREST 0x2600 +#define GL_LINEAR 0x2601 + +/* TextureParameterName */ +#define GL_TEXTURE_MAG_FILTER 0x2800 +#define GL_TEXTURE_MIN_FILTER 0x2801 +#define GL_TEXTURE_WRAP_S 0x2802 +#define GL_TEXTURE_WRAP_T 0x2803 + +/* TextureWrapMode */ +#define GL_REPEAT 0x2901 +#define GL_CLAMP_TO_EDGE 0x812F + +/* BeginMode */ +#define GL_POINTS 0x0000 +#define GL_LINES 0x0001 +#define GL_LINE_LOOP 0x0002 +#define GL_LINE_STRIP 0x0003 +#define GL_TRIANGLES 0x0004 +#define GL_TRIANGLE_STRIP 0x0005 +#define GL_TRIANGLE_FAN 0x0006 + +/* Shaders */ +#define GL_FRAGMENT_SHADER 0x8B30 + +#define GL_VERTEX_SHADER 0x8B31 + +/* Programs */ +#define GL_COMPILE_STATUS 0x8B81 +#define GL_LINK_STATUS 0x8B82 +#define GL_INFO_LOG_LENGTH 0x8B84 +#define GL_CURRENT_PROGRAM 0x8B8D + +/* Textures */ +#define GL_TEXTURE0 0x84C0 +#define GL_TEXTURE1 0x84C1 + +/* GetPName */ +#define GL_VIEWPORT 0x0BA2 +#define GL_FRAMEBUFFER_BINDING 0x8CA6 + +/* Framebuffer objects */ +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_FRAMEBUFFER 0x8D40 + +#endif diff --git a/backends/graphics/opengl/opengl-func.h b/backends/graphics/opengl/opengl-func.h new file mode 100644 index 0000000000..4e44c13d0f --- /dev/null +++ b/backends/graphics/opengl/opengl-func.h @@ -0,0 +1,153 @@ +/* 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. + * + */ + +/* This file is based on Mesa 3-D's gl.h and GLES/gl.h from Khronos Registry. + * + * Mesa 3-D's gl.h file is distributed under the following license: + * Mesa 3-D graphics library + * + * Copyright (C) 1999-2006 Brian Paul All Rights Reserved. + * Copyright (C) 2009 VMware, Inc. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * + * GLES/gl.h from Khronos Registry is distributed under the following license: + * This document is licensed under the SGI Free Software B License Version + * 2.0. For details, see http://oss.sgi.com/projects/FreeB/ . + */ + +/* + * This file is a template file to be used inside specific locations in the + * OpenGL graphics code. It is not to be included otherwise. It intentionally + * does not contain include guards because it can be required to include it + * multiple times in a source file. + * + * Functions are defined by three different user supplied macros: + * GL_FUNC_DEF: Define a (builtin) OpenGL (ES) function. + * GL_FUNC_2_DEF: Define a OpenGL (ES) 2.0 function which can be provided by + * extensions in OpenGL 1.x contexts. + * GL_EXT_FUNC_DEF: Define an OpenGL (ES) extension function. + */ + +#if !defined(GL_FUNC_2_DEF) +#define GL_FUNC_2_DEF(ret, name, extName, param) GL_FUNC_DEF(ret, name, param) +#define DEFINED_GL_FUNC_2_DEF +#endif + +#if !defined(GL_EXT_FUNC_DEF) +#define GL_EXT_FUNC_DEF(ret, name, param) GL_FUNC_DEF(ret, name, param) +#define DEFINED_GL_EXT_FUNC_DEF +#endif + +GL_FUNC_DEF(void, glEnable, (GLenum cap)); +GL_FUNC_DEF(void, glDisable, (GLenum cap)); +GL_FUNC_DEF(GLboolean, glIsEnabled, (GLenum cap)); +GL_FUNC_DEF(void, glClear, (GLbitfield mask)); +GL_FUNC_DEF(void, glColor4f, (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha)); +GL_FUNC_DEF(void, glViewport, (GLint x, GLint y, GLsizei width, GLsizei height)); +GL_FUNC_DEF(void, glMatrixMode, (GLenum mode)); +GL_FUNC_DEF(void, glLoadIdentity, ()); +GL_FUNC_DEF(void, glLoadMatrixf, (const GLfloat *m)); +GL_FUNC_DEF(void, glShadeModel, (GLenum mode)); +GL_FUNC_DEF(void, glHint, (GLenum target, GLenum mode)); +GL_FUNC_DEF(void, glClearColor, (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha)); +GL_FUNC_DEF(void, glBlendFunc, (GLenum sfactor, GLenum dfactor)); +GL_FUNC_DEF(void, glEnableClientState, (GLenum array)); +GL_FUNC_DEF(void, glPixelStorei, (GLenum pname, GLint param)); +GL_FUNC_DEF(void, glScissor, (GLint x, GLint y, GLsizei width, GLsizei height)); +GL_FUNC_DEF(void, glReadPixels, (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *pixels)); +GL_FUNC_DEF(void, glGetIntegerv, (GLenum pname, GLint *params)); +GL_FUNC_DEF(void, glDeleteTextures, (GLsizei n, const GLuint *textures)); +GL_FUNC_DEF(void, glGenTextures, (GLsizei n, GLuint *textures)); +GL_FUNC_DEF(void, glBindTexture, (GLenum target, GLuint texture)); +GL_FUNC_DEF(void, glTexParameteri, (GLenum target, GLenum pname, GLint param)); +GL_FUNC_DEF(void, glTexImage2D, (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels)); +GL_FUNC_DEF(void, glTexCoordPointer, (GLint size, GLenum type, GLsizei stride, const GLvoid *pointer)); +GL_FUNC_DEF(void, glVertexPointer, (GLint size, GLenum type, GLsizei stride, const GLvoid *pointer)); +GL_FUNC_DEF(void, glDrawArrays, (GLenum mode, GLint first, GLsizei count)); +GL_FUNC_DEF(void, glTexSubImage2D, (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels)); +GL_FUNC_DEF(const GLubyte *, glGetString, (GLenum name)); +GL_FUNC_DEF(GLenum, glGetError, ()); + +#if !USE_FORCED_GLES +GL_FUNC_2_DEF(void, glEnableVertexAttribArray, glEnableVertexAttribArrayARB, (GLuint index)); +GL_FUNC_2_DEF(void, glDisableVertexAttribArray, glDisableVertexAttribArrayARB, (GLuint index)); +GL_FUNC_2_DEF(void, glUniform1i, glUniform1iARB, (GLint location, GLint v0)); +GL_FUNC_2_DEF(void, glUniform1f, glUniform1fARB, (GLint location, GLfloat v0)); +GL_FUNC_2_DEF(void, glUniformMatrix4fv, glUniformMatrix4fvARB, (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value)); +GL_FUNC_2_DEF(void, glVertexAttrib4f, glVertexAttrib4fARB, (GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w)); +GL_FUNC_2_DEF(void, glVertexAttribPointer, glVertexAttribPointerARB, (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer)); + +GL_FUNC_2_DEF(GLprogram, glCreateProgram, glCreateProgramObjectARB, ()); +GL_FUNC_2_DEF(void, glDeleteProgram, glDeleteObjectARB, (GLprogram program)); +GL_FUNC_2_DEF(void, glAttachShader, glAttachObjectARB, (GLprogram program, GLshader shader)); +GL_FUNC_2_DEF(void, glDetachShader, glDetachObjectARB, (GLprogram program, GLshader shader)); +GL_FUNC_2_DEF(void, glLinkProgram, glLinkProgramARB, (GLprogram program)); +GL_FUNC_2_DEF(void, glUseProgram, glUseProgramObjectARB, (GLprogram program)); +GL_FUNC_2_DEF(void, glGetProgramiv, glGetObjectParameterivARB, (GLprogram program, GLenum pname, GLint *params)); +GL_FUNC_2_DEF(void, glGetProgramInfoLog, glGetInfoLogARB, (GLprogram program, GLsizei bufSize, GLsizei *length, GLchar *infoLog)); +GL_FUNC_2_DEF(void, glBindAttribLocation, glBindAttribLocationARB, (GLprogram program, GLuint index, const GLchar *name)); +GL_FUNC_2_DEF(GLint, glGetAttribLocation, glGetAttribLocationARB, (GLprogram program, const GLchar *name)); +GL_FUNC_2_DEF(GLint, glGetUniformLocation, glGetUniformLocationARB, (GLprogram program, const GLchar *name)); + +GL_FUNC_2_DEF(GLshader, glCreateShader, glCreateShaderObjectARB, (GLenum type)); +GL_FUNC_2_DEF(void, glDeleteShader, glDeleteObjectARB, (GLshader shader)); +GL_FUNC_2_DEF(void, glGetShaderiv, glGetObjectParameterivARB, (GLshader shader, GLenum pname, GLint *params)); +GL_FUNC_2_DEF(void, glGetShaderInfoLog, glGetInfoLogARB, (GLshader shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog)); +GL_FUNC_2_DEF(void, glShaderSource, glShaderSourceARB, (GLshader shader, GLsizei count, const GLchar *const *string, const GLint *length)); +GL_FUNC_2_DEF(void, glCompileShader, glCompileShaderARB, (GLshader shader)); + +GL_FUNC_2_DEF(void, glBindFramebuffer, glBindFramebufferEXT, (GLenum target, GLuint renderbuffer)); +GL_FUNC_2_DEF(void, glDeleteFramebuffers, glDeleteFramebuffersEXT, (GLsizei n, const GLuint *framebuffers)); +GL_FUNC_2_DEF(void, glGenFramebuffers, glGenFramebuffersEXT, (GLsizei n, GLuint *renderbuffers)); +GL_FUNC_2_DEF(void, glFramebufferTexture2D, glFramebufferTexture2DEXT, (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)); +GL_FUNC_2_DEF(GLenum, glCheckFramebufferStatus, glCheckFramebufferStatusEXT, (GLenum target)); + +GL_FUNC_2_DEF(void, glActiveTexture, glActiveTextureARB, (GLenum texture)); +#endif + +#ifdef DEFINED_GL_EXT_FUNC_DEF +#undef DEFINED_GL_EXT_FUNC_DEF +#undef GL_EXT_FUNC_DEF +#endif + +#ifdef DEFINED_GL_FUNC_2_DEF +#undef DEFINED_GL_FUNC_2_DEF +#undef GL_FUNC_2_DEF +#endif diff --git a/backends/graphics/opengl/opengl-graphics.cpp b/backends/graphics/opengl/opengl-graphics.cpp index ac6d41d47d..7b41699e80 100644 --- a/backends/graphics/opengl/opengl-graphics.cpp +++ b/backends/graphics/opengl/opengl-graphics.cpp @@ -23,8 +23,10 @@ #include "backends/graphics/opengl/opengl-graphics.h" #include "backends/graphics/opengl/texture.h" -#include "backends/graphics/opengl/debug.h" -#include "backends/graphics/opengl/extensions.h" +#include "backends/graphics/opengl/pipelines/pipeline.h" +#include "backends/graphics/opengl/pipelines/fixed.h" +#include "backends/graphics/opengl/pipelines/shader.h" +#include "backends/graphics/opengl/shader.h" #include "common/textconsole.h" #include "common/translation.h" @@ -45,6 +47,7 @@ namespace OpenGL { OpenGLGraphicsManager::OpenGLGraphicsManager() : _currentState(), _oldState(), _transactionMode(kTransactionNone), _screenChangeID(1 << (sizeof(int) * 8 - 2)), + _pipeline(nullptr), _outputScreenWidth(0), _outputScreenHeight(0), _displayX(0), _displayY(0), _displayWidth(0), _displayHeight(0), _defaultFormat(), _defaultFormatAlpha(), _gameScreen(nullptr), _gameScreenShakeOffset(0), _overlay(nullptr), @@ -54,10 +57,12 @@ OpenGLGraphicsManager::OpenGLGraphicsManager() _cursorKeyColor(0), _cursorVisible(false), _cursorDontScale(false), _cursorPaletteEnabled(false), _forceRedraw(false), _scissorOverride(3) #ifdef USE_OSD - , _osdAlpha(0), _osdFadeStartTime(0), _osd(nullptr) + , _osdMessageChangeRequest(false), _osdMessageAlpha(0), _osdMessageFadeStartTime(0), _osdMessageSurface(nullptr), + _osdIconSurface(nullptr) #endif { memset(_gamePalette, 0, sizeof(_gamePalette)); + g_context.reset(); } OpenGLGraphicsManager::~OpenGLGraphicsManager() { @@ -65,7 +70,11 @@ OpenGLGraphicsManager::~OpenGLGraphicsManager() { delete _overlay; delete _cursor; #ifdef USE_OSD - delete _osd; + delete _osdMessageSurface; + delete _osdIconSurface; +#endif +#if !USE_FORCED_GLES + ShaderManager::destroy(); #endif } @@ -73,6 +82,7 @@ bool OpenGLGraphicsManager::hasFeature(OSystem::Feature f) { switch (f) { case OSystem::kFeatureAspectRatioCorrection: case OSystem::kFeatureCursorPalette: + case OSystem::kFeatureFilteringMode: return true; case OSystem::kFeatureOverlaySupportsAlpha: @@ -90,6 +100,20 @@ void OpenGLGraphicsManager::setFeatureState(OSystem::Feature f, bool enable) { _currentState.aspectRatioCorrection = enable; break; + case OSystem::kFeatureFilteringMode: + assert(_transactionMode != kTransactionNone); + _currentState.filtering = enable; + + if (_gameScreen) { + _gameScreen->enableLinearFiltering(enable); + } + + if (_cursor) { + _cursor->enableLinearFiltering(enable); + } + + break; + case OSystem::kFeatureCursorPalette: _cursorPaletteEnabled = enable; updateCursorPalette(); @@ -105,6 +129,9 @@ bool OpenGLGraphicsManager::getFeatureState(OSystem::Feature f) { case OSystem::kFeatureAspectRatioCorrection: return _currentState.aspectRatioCorrection; + case OSystem::kFeatureFilteringMode: + return _currentState.filtering; + case OSystem::kFeatureCursorPalette: return _cursorPaletteEnabled; @@ -116,8 +143,7 @@ bool OpenGLGraphicsManager::getFeatureState(OSystem::Feature f) { namespace { const OSystem::GraphicsMode glGraphicsModes[] = { - { "opengl_linear", _s("OpenGL"), GFX_LINEAR }, - { "opengl_nearest", _s("OpenGL (No filtering)"), GFX_NEAREST }, + { "opengl", _s("OpenGL"), GFX_OPENGL }, { nullptr, nullptr, 0 } }; @@ -128,25 +154,15 @@ const OSystem::GraphicsMode *OpenGLGraphicsManager::getSupportedGraphicsModes() } int OpenGLGraphicsManager::getDefaultGraphicsMode() const { - return GFX_LINEAR; + return GFX_OPENGL; } bool OpenGLGraphicsManager::setGraphicsMode(int mode) { assert(_transactionMode != kTransactionNone); switch (mode) { - case GFX_LINEAR: - case GFX_NEAREST: + case GFX_OPENGL: _currentState.graphicsMode = mode; - - if (_gameScreen) { - _gameScreen->enableLinearFiltering(mode == GFX_LINEAR); - } - - if (_cursor) { - _cursor->enableLinearFiltering(mode == GFX_LINEAR); - } - return true; default: @@ -215,8 +231,8 @@ OSystem::TransactionError OpenGLGraphicsManager::endGFXTransaction() { // a context existing before, which means we don't know the maximum // supported texture size before this. Thus, we check whether the // requested game resolution is supported over here. - || ( _currentState.gameWidth > (uint)Texture::getMaximumTextureSize() - || _currentState.gameHeight > (uint)Texture::getMaximumTextureSize())) { + || ( _currentState.gameWidth > (uint)g_context.maxTextureSize + || _currentState.gameHeight > (uint)g_context.maxTextureSize)) { if (_transactionMode == kTransactionActive) { // Try to setup the old state in case its valid and is // actually different from the new one. @@ -241,6 +257,10 @@ OSystem::TransactionError OpenGLGraphicsManager::endGFXTransaction() { transactionError |= OSystem::kTransactionModeSwitchFailed; } + if (_oldState.filtering != _currentState.filtering) { + transactionError |= OSystem::kTransactionFilteringFailed; + } + // Roll back to the old state. _currentState = _oldState; _transactionMode = kTransactionRollback; @@ -267,9 +287,9 @@ OSystem::TransactionError OpenGLGraphicsManager::endGFXTransaction() { _gameScreen = nullptr; #ifdef USE_RGB_COLOR - _gameScreen = createTexture(_currentState.gameFormat); + _gameScreen = createSurface(_currentState.gameFormat); #else - _gameScreen = createTexture(Graphics::PixelFormat::createFormatCLUT8()); + _gameScreen = createSurface(Graphics::PixelFormat::createFormatCLUT8()); #endif assert(_gameScreen); if (_gameScreen->hasPalette()) { @@ -277,7 +297,7 @@ OSystem::TransactionError OpenGLGraphicsManager::endGFXTransaction() { } _gameScreen->allocate(_currentState.gameWidth, _currentState.gameHeight); - _gameScreen->enableLinearFiltering(_currentState.graphicsMode == GFX_LINEAR); + _gameScreen->enableLinearFiltering(_currentState.filtering); // We fill the screen to all black or index 0 for CLUT8. #ifdef USE_RGB_COLOR if (_currentState.gameFormat.bytesPerPixel == 1) { @@ -355,39 +375,61 @@ void OpenGLGraphicsManager::updateScreen() { return; } +#ifdef USE_OSD + { + if (_osdMessageChangeRequest) { + osdMessageUpdateSurface(); + } + } + + if (_osdIconSurface) { + _osdIconSurface->updateGLTexture(); + } +#endif + // We only update the screen when there actually have been any changes. if ( !_forceRedraw && !_gameScreen->isDirty() && !(_overlayVisible && _overlay->isDirty()) && !(_cursorVisible && _cursor && _cursor->isDirty()) - && _osdAlpha == 0) { +#ifdef USE_OSD + && !_osdMessageSurface && !_osdIconSurface +#endif + ) { return; } _forceRedraw = false; + // Update changes to textures. + _gameScreen->updateGLTexture(); + if (_cursor) { + _cursor->updateGLTexture(); + } + _overlay->updateGLTexture(); + // Clear the screen buffer. if (_scissorOverride && !_overlayVisible) { // In certain cases we need to assure that the whole screen area is // cleared. For example, when switching from overlay visible to // invisible, we need to assure that all contents are cleared to // properly remove all overlay contents. - GLCALL(glDisable(GL_SCISSOR_TEST)); - GLCALL(glClear(GL_COLOR_BUFFER_BIT)); - GLCALL(glEnable(GL_SCISSOR_TEST)); + _backBuffer.enableScissorTest(false); + GL_CALL(glClear(GL_COLOR_BUFFER_BIT)); + _backBuffer.enableScissorTest(true); --_scissorOverride; } else { - GLCALL(glClear(GL_COLOR_BUFFER_BIT)); + GL_CALL(glClear(GL_COLOR_BUFFER_BIT)); } const GLfloat shakeOffset = _gameScreenShakeOffset * (GLfloat)_displayHeight / _gameScreen->getHeight(); // First step: Draw the (virtual) game screen. - _gameScreen->draw(_displayX, _displayY + shakeOffset, _displayWidth, _displayHeight); + g_context.getActivePipeline()->drawTexture(_gameScreen->getGLTexture(), _displayX, _displayY + shakeOffset, _displayWidth, _displayHeight); // Second step: Draw the overlay if visible. if (_overlayVisible) { - _overlay->draw(0, 0, _outputScreenWidth, _outputScreenHeight); + g_context.getActivePipeline()->drawTexture(_overlay->getGLTexture(), 0, 0, _outputScreenWidth, _outputScreenHeight); } // Third step: Draw the cursor if visible. @@ -396,36 +438,53 @@ void OpenGLGraphicsManager::updateScreen() { // visible. const GLfloat cursorOffset = _overlayVisible ? 0 : shakeOffset; - _cursor->draw(_cursorDisplayX - _cursorHotspotXScaled, - _cursorDisplayY - _cursorHotspotYScaled + cursorOffset, - _cursorWidthScaled, _cursorHeightScaled); + g_context.getActivePipeline()->drawTexture(_cursor->getGLTexture(), + _cursorDisplayX - _cursorHotspotXScaled, + _cursorDisplayY - _cursorHotspotYScaled + cursorOffset, + _cursorWidthScaled, _cursorHeightScaled); } #ifdef USE_OSD // Fourth step: Draw the OSD. - if (_osdAlpha > 0) { - Common::StackLock lock(_osdMutex); - + if (_osdMessageSurface) { // Update alpha value. - const int diff = g_system->getMillis(false) - _osdFadeStartTime; + const int diff = g_system->getMillis(false) - _osdMessageFadeStartTime; if (diff > 0) { - if (diff >= kOSDFadeOutDuration) { + if (diff >= kOSDMessageFadeOutDuration) { // Back to full transparency. - _osdAlpha = 0; + _osdMessageAlpha = 0; } else { // Do a fade out. - _osdAlpha = kOSDInitialAlpha - diff * kOSDInitialAlpha / kOSDFadeOutDuration; + _osdMessageAlpha = kOSDMessageInitialAlpha - diff * kOSDMessageInitialAlpha / kOSDMessageFadeOutDuration; } } // Set the OSD transparency. - GLCALL(glColor4f(1.0f, 1.0f, 1.0f, _osdAlpha / 100.0f)); + g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, _osdMessageAlpha / 100.0f); + + int dstX = (_outputScreenWidth - _osdMessageSurface->getWidth()) / 2; + int dstY = (_outputScreenHeight - _osdMessageSurface->getHeight()) / 2; // Draw the OSD texture. - _osd->draw(0, 0, _outputScreenWidth, _outputScreenHeight); + g_context.getActivePipeline()->drawTexture(_osdMessageSurface->getGLTexture(), + dstX, dstY, _osdMessageSurface->getWidth(), _osdMessageSurface->getHeight()); // Reset color. - GLCALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f)); + g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, 1.0f); + + if (_osdMessageAlpha <= 0) { + delete _osdMessageSurface; + _osdMessageSurface = nullptr; + } + } + + if (_osdIconSurface) { + int dstX = _outputScreenWidth - _osdIconSurface->getWidth() - kOSDIconRightMargin; + int dstY = kOSDIconTopMargin; + + // Draw the OSD icon texture. + g_context.getActivePipeline()->drawTexture(_osdIconSurface->getGLTexture(), + dstX, dstY, _osdIconSurface->getWidth(), _osdIconSurface->getHeight()); } #endif @@ -467,7 +526,7 @@ void OpenGLGraphicsManager::showOverlay() { _forceRedraw = true; // Allow drawing inside full screen area. - GLCALL(glDisable(GL_SCISSOR_TEST)); + _backBuffer.enableScissorTest(false); // Update cursor position. setMousePosition(_cursorX, _cursorY); @@ -478,7 +537,7 @@ void OpenGLGraphicsManager::hideOverlay() { _forceRedraw = true; // Limit drawing to screen area. - GLCALL(glEnable(GL_SCISSOR_TEST)); + _backBuffer.enableScissorTest(true); _scissorOverride = 3; // Update cursor position. @@ -609,9 +668,9 @@ void OpenGLGraphicsManager::setMouseCursor(const void *buf, uint w, uint h, int } else { textureFormat = _defaultFormatAlpha; } - _cursor = createTexture(textureFormat, true); + _cursor = createSurface(textureFormat, true); assert(_cursor); - _cursor->enableLinearFiltering(_currentState.graphicsMode == GFX_LINEAR); + _cursor->enableLinearFiltering(_currentState.filtering); } _cursorKeyColor = keycolor; @@ -681,57 +740,107 @@ void OpenGLGraphicsManager::setCursorPalette(const byte *colors, uint start, uin void OpenGLGraphicsManager::displayMessageOnOSD(const char *msg) { #ifdef USE_OSD - // HACK: Actually no client code should use graphics functions from - // another thread. But the MT-32 emulator still does, thus we need to - // make sure this doesn't happen while a updateScreen call is done. - Common::StackLock lock(_osdMutex); + _osdMessageChangeRequest = true; - // Slip up the lines. + _osdMessageNextData = msg; +#endif +} + +#ifdef USE_OSD +void OpenGLGraphicsManager::osdMessageUpdateSurface() { + // Split up the lines. Common::Array<Common::String> osdLines; - Common::StringTokenizer tokenizer(msg, "\n"); + Common::StringTokenizer tokenizer(_osdMessageNextData, "\n"); while (!tokenizer.empty()) { osdLines.push_back(tokenizer.nextToken()); } // Do the actual drawing like the SDL backend. const Graphics::Font *font = getFontOSD(); - Graphics::Surface *dst = _osd->getSurface(); - _osd->fill(0); - _osd->flagDirty(); // Determine a rect which would contain the message string (clipped to the // screen dimensions). const int vOffset = 6; const int lineSpacing = 1; const int lineHeight = font->getFontHeight() + 2 * lineSpacing; - int width = 0; - int height = lineHeight * osdLines.size() + 2 * vOffset; + uint width = 0; + uint height = lineHeight * osdLines.size() + 2 * vOffset; for (uint i = 0; i < osdLines.size(); i++) { - width = MAX(width, font->getStringWidth(osdLines[i]) + 14); + width = MAX<uint>(width, font->getStringWidth(osdLines[i]) + 14); } // Clip the rect - width = MIN<int>(width, dst->w); - height = MIN<int>(height, dst->h); + width = MIN<uint>(width, _displayWidth); + height = MIN<uint>(height, _displayHeight); + + delete _osdMessageSurface; + _osdMessageSurface = nullptr; - int dstX = (dst->w - width) / 2; - int dstY = (dst->h - height) / 2; + _osdMessageSurface = createSurface(_defaultFormatAlpha); + assert(_osdMessageSurface); + // We always filter the osd with GL_LINEAR. This assures it's + // readable in case it needs to be scaled and does not affect it + // otherwise. + _osdMessageSurface->enableLinearFiltering(true); + + _osdMessageSurface->allocate(width, height); + + Graphics::Surface *dst = _osdMessageSurface->getSurface(); // Draw a dark gray rect. const uint32 color = dst->format.RGBToColor(40, 40, 40); - dst->fillRect(Common::Rect(dstX, dstY, dstX + width, dstY + height), color); + dst->fillRect(Common::Rect(0, 0, width, height), color); - // Render the message, centered, and in white + // Render the message in white const uint32 white = dst->format.RGBToColor(255, 255, 255); for (uint i = 0; i < osdLines.size(); ++i) { font->drawString(dst, osdLines[i], - dstX, dstY + i * lineHeight + vOffset + lineSpacing, width, + 0, i * lineHeight + vOffset + lineSpacing, width, white, Graphics::kTextAlignCenter); } + _osdMessageSurface->updateGLTexture(); + // Init the OSD display parameters. - _osdAlpha = kOSDInitialAlpha; - _osdFadeStartTime = g_system->getMillis() + kOSDFadeOutDelay; + _osdMessageAlpha = kOSDMessageInitialAlpha; + _osdMessageFadeStartTime = g_system->getMillis() + kOSDMessageFadeOutDelay; + + // Clear the text update request + _osdMessageNextData.clear(); + _osdMessageChangeRequest = false; +} +#endif + +void OpenGLGraphicsManager::displayActivityIconOnOSD(const Graphics::Surface *icon) { +#ifdef USE_OSD + if (_osdIconSurface) { + delete _osdIconSurface; + _osdIconSurface = nullptr; + + // Make sure the icon is cleared on the next update + _forceRedraw = true; + } + + if (icon) { + Graphics::Surface *converted = icon->convertTo(_defaultFormatAlpha); + + _osdIconSurface = createSurface(_defaultFormatAlpha); + assert(_osdIconSurface); + // We always filter the osd with GL_LINEAR. This assures it's + // readable in case it needs to be scaled and does not affect it + // otherwise. + _osdIconSurface->enableLinearFiltering(true); + + _osdIconSurface->allocate(converted->w, converted->h); + + Graphics::Surface *dst = _osdIconSurface->getSurface(); + + // Copy the icon to the texture + dst->copyRectToSurface(*converted, 0, 0, Common::Rect(0, 0, converted->w, converted->h)); + + converted->free(); + delete converted; + } #endif } @@ -755,18 +864,8 @@ void OpenGLGraphicsManager::setActualScreenSize(uint width, uint height) { _outputScreenWidth = width; _outputScreenHeight = height; - // Setup coordinates system. - GLCALL(glViewport(0, 0, _outputScreenWidth, _outputScreenHeight)); - - GLCALL(glMatrixMode(GL_PROJECTION)); - GLCALL(glLoadIdentity()); -#ifdef USE_GLES - GLCALL(glOrthof(0, _outputScreenWidth, _outputScreenHeight, 0, -1, 1)); -#else - GLCALL(glOrtho(0, _outputScreenWidth, _outputScreenHeight, 0, -1, 1)); -#endif - GLCALL(glMatrixMode(GL_MODELVIEW)); - GLCALL(glLoadIdentity()); + // Setup backbuffer size. + _backBuffer.setDimensions(width, height); uint overlayWidth = width; uint overlayHeight = height; @@ -777,15 +876,15 @@ void OpenGLGraphicsManager::setActualScreenSize(uint width, uint height) { // possible and then scale it to the physical display size. This sounds // bad but actually all recent chips should support full HD resolution // anyway. Thus, it should not be a real issue for modern hardware. - if ( overlayWidth > (uint)Texture::getMaximumTextureSize() - || overlayHeight > (uint)Texture::getMaximumTextureSize()) { + if ( overlayWidth > (uint)g_context.maxTextureSize + || overlayHeight > (uint)g_context.maxTextureSize) { const frac_t outputAspect = intToFrac(_outputScreenWidth) / _outputScreenHeight; if (outputAspect > (frac_t)FRAC_ONE) { - overlayWidth = Texture::getMaximumTextureSize(); + overlayWidth = g_context.maxTextureSize; overlayHeight = intToFrac(overlayWidth) / outputAspect; } else { - overlayHeight = Texture::getMaximumTextureSize(); + overlayHeight = g_context.maxTextureSize; overlayWidth = fracToInt(overlayHeight * outputAspect); } } @@ -801,7 +900,7 @@ void OpenGLGraphicsManager::setActualScreenSize(uint width, uint height) { delete _overlay; _overlay = nullptr; - _overlay = createTexture(_defaultFormatAlpha); + _overlay = createSurface(_defaultFormatAlpha); assert(_overlay); // We always filter the overlay with GL_LINEAR. This assures it's // readable in case it needs to be scaled and does not affect it @@ -811,22 +910,6 @@ void OpenGLGraphicsManager::setActualScreenSize(uint width, uint height) { _overlay->allocate(overlayWidth, overlayHeight); _overlay->fill(0); -#ifdef USE_OSD - if (!_osd || _osd->getFormat() != _defaultFormatAlpha) { - delete _osd; - _osd = nullptr; - - _osd = createTexture(_defaultFormatAlpha); - assert(_osd); - // We always filter the osd with GL_LINEAR. This assures it's - // readable in case it needs to be scaled and does not affect it - // otherwise. - _osd->enableLinearFiltering(true); - } - _osd->allocate(_overlay->getWidth(), _overlay->getHeight()); - _osd->fill(0); -#endif - // Re-setup the scaling for the screen and cursor recalculateDisplayArea(); recalculateCursorScaling(); @@ -836,38 +919,48 @@ void OpenGLGraphicsManager::setActualScreenSize(uint width, uint height) { } void OpenGLGraphicsManager::notifyContextCreate(const Graphics::PixelFormat &defaultFormat, const Graphics::PixelFormat &defaultFormatAlpha) { - // Initialize all extensions. - initializeGLExtensions(); + // Initialize context for use. + initializeGLContext(); - // Disable 3D properties. - GLCALL(glDisable(GL_CULL_FACE)); - GLCALL(glDisable(GL_DEPTH_TEST)); - GLCALL(glDisable(GL_LIGHTING)); - GLCALL(glDisable(GL_FOG)); - GLCALL(glDisable(GL_DITHER)); - GLCALL(glShadeModel(GL_FLAT)); - GLCALL(glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST)); + // Initialize pipeline. + delete _pipeline; + _pipeline = nullptr; - // Default to black as clear color. - GLCALL(glClearColor(0.0f, 0.0f, 0.0f, 0.0f)); - GLCALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f)); +#if !USE_FORCED_GLES + if (g_context.shadersSupported) { + ShaderMan.notifyCreate(); + _pipeline = new ShaderPipeline(ShaderMan.query(ShaderManager::kDefault)); + } +#endif - // Setup alpha blend (for overlay and cursor). - GLCALL(glEnable(GL_BLEND)); - GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); +#if !USE_FORCED_GLES2 + if (_pipeline == nullptr) { + _pipeline = new FixedPipeline(); + } +#endif + + g_context.setPipeline(_pipeline); + + // Disable 3D properties. + GL_CALL(glDisable(GL_CULL_FACE)); + GL_CALL(glDisable(GL_DEPTH_TEST)); + GL_CALL(glDisable(GL_DITHER)); - // Enable rendering with vertex and coord arrays. - GLCALL(glEnableClientState(GL_VERTEX_ARRAY)); - GLCALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY)); + g_context.getActivePipeline()->setColor(1.0f, 1.0f, 1.0f, 1.0f); - GLCALL(glEnable(GL_TEXTURE_2D)); + GL_CALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + // Setup backbuffer state. + + // Default to black as clear color. + _backBuffer.setClearColor(0.0f, 0.0f, 0.0f, 0.0f); + // Setup alpha blend (for overlay and cursor). + _backBuffer.enableBlend(true); // Setup scissor state accordingly. - if (_overlayVisible) { - GLCALL(glDisable(GL_SCISSOR_TEST)); - } else { - GLCALL(glEnable(GL_SCISSOR_TEST)); - } + _backBuffer.enableScissorTest(!_overlayVisible); + + g_context.getActivePipeline()->setFramebuffer(&_backBuffer); + // Clear the whole screen for the first three frames to assure any // leftovers are cleared. _scissorOverride = 3; @@ -875,10 +968,7 @@ void OpenGLGraphicsManager::notifyContextCreate(const Graphics::PixelFormat &def // We use a "pack" alignment (when reading from textures) to 4 here, // since the only place where we really use it is the BMP screenshot // code and that requires the same alignment too. - GLCALL(glPixelStorei(GL_PACK_ALIGNMENT, 4)); - - // Query information needed by textures. - Texture::queryTextureInformation(); + GL_CALL(glPixelStorei(GL_PACK_ALIGNMENT, 4)); // Refresh the output screen dimensions if some are set up. if (_outputScreenWidth != 0 && _outputScreenHeight != 0) { @@ -892,42 +982,64 @@ void OpenGLGraphicsManager::notifyContextCreate(const Graphics::PixelFormat &def _defaultFormatAlpha = defaultFormatAlpha; if (_gameScreen) { - _gameScreen->recreateInternalTexture(); + _gameScreen->recreate(); } if (_overlay) { - _overlay->recreateInternalTexture(); + _overlay->recreate(); } if (_cursor) { - _cursor->recreateInternalTexture(); + _cursor->recreate(); } #ifdef USE_OSD - if (_osd) { - _osd->recreateInternalTexture(); + if (_osdMessageSurface) { + _osdMessageSurface->recreate(); + } + + if (_osdIconSurface) { + _osdIconSurface->recreate(); } #endif } void OpenGLGraphicsManager::notifyContextDestroy() { if (_gameScreen) { - _gameScreen->releaseInternalTexture(); + _gameScreen->destroy(); } if (_overlay) { - _overlay->releaseInternalTexture(); + _overlay->destroy(); } if (_cursor) { - _cursor->releaseInternalTexture(); + _cursor->destroy(); } #ifdef USE_OSD - if (_osd) { - _osd->releaseInternalTexture(); + if (_osdMessageSurface) { + _osdMessageSurface->destroy(); + } + + if (_osdIconSurface) { + _osdIconSurface->destroy(); } #endif + +#if !USE_FORCED_GLES + if (g_context.shadersSupported) { + ShaderMan.notifyDestroy(); + } +#endif + + // Destroy rendering pipeline. + g_context.setPipeline(nullptr); + delete _pipeline; + _pipeline = nullptr; + + // Rest our context description since the context is gone soon. + g_context.reset(); } void OpenGLGraphicsManager::adjustMousePosition(int16 &x, int16 &y) { @@ -970,9 +1082,15 @@ void OpenGLGraphicsManager::setMousePosition(int x, int y) { } } -Texture *OpenGLGraphicsManager::createTexture(const Graphics::PixelFormat &format, bool wantAlpha) { +Surface *OpenGLGraphicsManager::createSurface(const Graphics::PixelFormat &format, bool wantAlpha) { GLenum glIntFormat, glFormat, glType; if (format.bytesPerPixel == 1) { +#if !USE_FORCED_GLES + if (TextureCLUT8GPU::isSupportedByContext()) { + return new TextureCLUT8GPU(); + } +#endif + const Graphics::PixelFormat &virtFormat = wantAlpha ? _defaultFormatAlpha : _defaultFormat; const bool supported = getGLPixelFormat(virtFormat, glIntFormat, glFormat, glType); if (!supported) { @@ -980,6 +1098,15 @@ Texture *OpenGLGraphicsManager::createTexture(const Graphics::PixelFormat &forma } else { return new TextureCLUT8(glIntFormat, glFormat, glType, virtFormat); } +#if !USE_FORCED_GL + } else if (isGLESContext() && format == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) { + // OpenGL ES does not support a texture format usable for RGB555. + // Since SCUMM uses this pixel format for some games (and there is no + // hope for this to change anytime soon) we use pixel format + // conversion to a supported texture format. However, this is a one + // time exception. + return new TextureRGB555(); +#endif // !USE_FORCED_GL } else { const bool supported = getGLPixelFormat(format, glIntFormat, glFormat, glType); if (!supported) { @@ -1015,7 +1142,11 @@ bool OpenGLGraphicsManager::getGLPixelFormat(const Graphics::PixelFormat &pixelF glFormat = GL_RGBA; glType = GL_UNSIGNED_SHORT_4_4_4_4; return true; -#ifndef USE_GLES +#if !USE_FORCED_GLES && !USE_FORCED_GLES2 + // The formats below are not supported by every GLES implementation. + // Thus, we do not mark them as supported when a GLES context is setup. + } else if (isGLESContext()) { + return false; #ifdef SCUMM_LITTLE_ENDIAN } else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)) { // RGBA8888 glIntFormat = GL_RGBA; @@ -1024,17 +1155,10 @@ bool OpenGLGraphicsManager::getGLPixelFormat(const Graphics::PixelFormat &pixelF return true; #endif } else if (pixelFormat == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) { // RGB555 - // GL_BGRA does not exist in every GLES implementation so should not be configured if - // USE_GLES is set. glIntFormat = GL_RGB; glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_1_5_5_5_REV; return true; - } else if (pixelFormat == Graphics::PixelFormat(4, 8, 8, 8, 8, 16, 8, 0, 24)) { // ARGB8888 - glIntFormat = GL_RGBA; - glFormat = GL_BGRA; - glType = GL_UNSIGNED_INT_8_8_8_8_REV; - return true; } else if (pixelFormat == Graphics::PixelFormat(2, 4, 4, 4, 4, 8, 4, 0, 12)) { // ARGB4444 glIntFormat = GL_RGBA; glFormat = GL_BGRA; @@ -1054,8 +1178,8 @@ bool OpenGLGraphicsManager::getGLPixelFormat(const Graphics::PixelFormat &pixelF return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 6, 5, 0, 0, 5, 11, 0)) { // BGR565 glIntFormat = GL_RGB; - glFormat = GL_BGR; - glType = GL_UNSIGNED_SHORT_5_6_5; + glFormat = GL_RGB; + glType = GL_UNSIGNED_SHORT_5_6_5_REV; return true; } else if (pixelFormat == Graphics::PixelFormat(2, 5, 5, 5, 1, 1, 6, 11, 0)) { // BGRA5551 glIntFormat = GL_RGBA; @@ -1072,7 +1196,7 @@ bool OpenGLGraphicsManager::getGLPixelFormat(const Graphics::PixelFormat &pixelF glFormat = GL_BGRA; glType = GL_UNSIGNED_SHORT_4_4_4_4; return true; -#endif +#endif // !USE_FORCED_GLES && !USE_FORCED_GLES2 } else { return false; } @@ -1119,10 +1243,10 @@ void OpenGLGraphicsManager::recalculateDisplayArea() { // Setup drawing limitation for game graphics. // This invovles some trickery because OpenGL's viewport coordinate system // is upside down compared to ours. - GLCALL(glScissor(_displayX, - _outputScreenHeight - _displayHeight - _displayY, - _displayWidth, - _displayHeight)); + _backBuffer.setScissorBox(_displayX, + _outputScreenHeight - _displayHeight - _displayY, + _displayWidth, + _displayHeight); // Clear the whole screen for the first three frames to remove leftovers. _scissorOverride = 3; @@ -1144,20 +1268,7 @@ void OpenGLGraphicsManager::updateCursorPalette() { _cursor->setPalette(0, 256, _gamePalette); } - // We remove all alpha bits from the palette entry of the color key. - // This makes sure its properly handled as color key. - const Graphics::PixelFormat &hardwareFormat = _cursor->getHardwareFormat(); - const uint32 aMask = (0xFF >> hardwareFormat.aLoss) << hardwareFormat.aShift; - - if (hardwareFormat.bytesPerPixel == 2) { - uint16 *palette = (uint16 *)_cursor->getPalette() + _cursorKeyColor; - *palette &= ~aMask; - } else if (hardwareFormat.bytesPerPixel == 4) { - uint32 *palette = (uint32 *)_cursor->getPalette() + _cursorKeyColor; - *palette &= ~aMask; - } else { - warning("OpenGLGraphicsManager::updateCursorPalette: Unsupported pixel depth %d", hardwareFormat.bytesPerPixel); - } + _cursor->setColorKey(_cursorKeyColor); } void OpenGLGraphicsManager::recalculateCursorScaling() { @@ -1207,7 +1318,7 @@ void OpenGLGraphicsManager::saveScreenshot(const Common::String &filename) const uint8 *pixels = new uint8[lineSize * height]; // Get pixel data from OpenGL buffer - GLCALL(glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels)); + GL_CALL(glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels)); // BMP stores as BGR. Since we can't assume that GL_BGR is supported we // will swap the components from the RGB we read to BGR on our own. diff --git a/backends/graphics/opengl/opengl-graphics.h b/backends/graphics/opengl/opengl-graphics.h index 9578839383..d3f8d792ba 100644 --- a/backends/graphics/opengl/opengl-graphics.h +++ b/backends/graphics/opengl/opengl-graphics.h @@ -24,11 +24,14 @@ #define BACKENDS_GRAPHICS_OPENGL_OPENGL_GRAPHICS_H #include "backends/graphics/opengl/opengl-sys.h" +#include "backends/graphics/opengl/framebuffer.h" #include "backends/graphics/graphics.h" #include "common/frac.h" #include "common/mutex.h" +#include "graphics/surface.h" + namespace Graphics { class Font; } // End of namespace Graphics @@ -40,11 +43,14 @@ namespace OpenGL { // SurfaceSDL backend enables it and disabling it can cause issues in sdl.cpp. #define USE_OSD 1 -class Texture; +class Surface; +class Pipeline; +#if !USE_FORCED_GLES +class Shader; +#endif enum { - GFX_LINEAR = 0, - GFX_NEAREST = 1 + GFX_OPENGL = 0 }; class OpenGLGraphicsManager : virtual public GraphicsManager { @@ -110,6 +116,7 @@ public: virtual void setCursorPalette(const byte *colors, uint start, uint num); virtual void displayMessageOnOSD(const char *msg); + virtual void displayActivityIconOnOSD(const Graphics::Surface *icon); // PaletteManager interface virtual void setPalette(const byte *colors, uint start, uint num); @@ -117,6 +124,11 @@ public: protected: /** + * Whether an GLES or GLES2 context is active. + */ + bool isGLESContext() const { return g_context.type == kContextGLES || g_context.type == kContextGLES2; } + + /** * Set up the actual screen size available for the OpenGL code to do any * drawing. * @@ -126,6 +138,16 @@ protected: void setActualScreenSize(uint width, uint height); /** + * Sets the OpenGL (ES) type the graphics manager shall work with. + * + * This needs to be called at least once (and before ever calling + * notifyContextCreate). + * + * @param type Type of the OpenGL (ES) contexts to be created. + */ + void setContextType(ContextType type); + + /** * Notify the manager of a OpenGL context change. This should be the first * thing to call after you created an OpenGL (ES) context! * @@ -172,15 +194,15 @@ protected: private: /** - * Create a texture with the specified pixel format. + * Create a surface with the specified pixel format. * - * @param format The pixel format the Texture object should accept as + * @param format The pixel format the Surface object should accept as * input. - * @param wantAlpha For CLUT8 textures this marks whether an alpha + * @param wantAlpha For CLUT8 surfaces this marks whether an alpha * channel should be used. - * @return A pointer to the texture or nullptr on failure. + * @return A pointer to the surface or nullptr on failure. */ - Texture *createTexture(const Graphics::PixelFormat &format, bool wantAlpha = false); + Surface *createSurface(const Graphics::PixelFormat &format, bool wantAlpha = false); // // Transaction support @@ -190,7 +212,7 @@ private: #ifdef USE_RGB_COLOR gameFormat(), #endif - aspectRatioCorrection(false), graphicsMode(GFX_LINEAR) { + aspectRatioCorrection(false), graphicsMode(GFX_OPENGL), filtering(true) { } bool valid; @@ -201,6 +223,7 @@ private: #endif bool aspectRatioCorrection; int graphicsMode; + bool filtering; bool operator==(const VideoState &right) { return gameWidth == right.gameWidth && gameHeight == right.gameHeight @@ -208,7 +231,8 @@ private: && gameFormat == right.gameFormat #endif && aspectRatioCorrection == right.aspectRatioCorrection - && graphicsMode == right.graphicsMode; + && graphicsMode == right.graphicsMode + && filtering == right.filtering; } bool operator!=(const VideoState &right) { @@ -281,6 +305,36 @@ private: // /** + * Initialize the active context for use. + */ + void initializeGLContext(); + + /** + * Render back buffer. + */ + Backbuffer _backBuffer; + + /** + * OpenGL pipeline used for rendering. + */ + Pipeline *_pipeline; + +protected: + /** + * Query the address of an OpenGL function by name. + * + * This can only be used after a context has been created. + * Please note that this function can return valid addresses even if the + * OpenGL context does not support the function. + * + * @param name The name of the OpenGL function. + * @return An function pointer for the requested OpenGL function or + * nullptr in case of failure. + */ + virtual void *getProcAddress(const char *name) const = 0; + +private: + /** * Try to determine the internal parameters for a given pixel format. * * @return true when the format can be used, false otherwise. @@ -348,7 +402,7 @@ private: /** * The virtual game screen. */ - Texture *_gameScreen; + Surface *_gameScreen; /** * The game palette if in CLUT8 mode. @@ -367,7 +421,7 @@ private: /** * The overlay screen. */ - Texture *_overlay; + Surface *_overlay; /** * Whether the overlay is visible or not. @@ -386,7 +440,7 @@ private: /** * The cursor image. */ - Texture *_cursor; + Surface *_cursor; /** * X coordinate of the cursor in phyiscal coordinates. @@ -495,29 +549,52 @@ protected: private: /** - * The OSD's contents. + * Request for the OSD icon surface to be updated. + */ + bool _osdMessageChangeRequest; + + /** + * The next OSD message. + * + * If this value is not empty, the OSD message will be set + * to it on the next frame. */ - Texture *_osd; + Common::String _osdMessageNextData; /** - * Current opacity level of the OSD. + * Set the OSD message surface with the value of the next OSD message. */ - uint8 _osdAlpha; + void osdMessageUpdateSurface(); /** - * When fading the OSD has started. + * The OSD message's contents. */ - uint32 _osdFadeStartTime; + Surface *_osdMessageSurface; + + /** + * Current opacity level of the OSD message. + */ + uint8 _osdMessageAlpha; + + /** + * When fading the OSD message has started. + */ + uint32 _osdMessageFadeStartTime; + + enum { + kOSDMessageFadeOutDelay = 2 * 1000, + kOSDMessageFadeOutDuration = 500, + kOSDMessageInitialAlpha = 80 + }; /** - * Mutex to allow displayMessageOnOSD to be used from the audio thread. + * The OSD background activity icon's contents. */ - Common::Mutex _osdMutex; + Surface *_osdIconSurface; enum { - kOSDFadeOutDelay = 2 * 1000, - kOSDFadeOutDuration = 500, - kOSDInitialAlpha = 80 + kOSDIconTopMargin = 10, + kOSDIconRightMargin = 10 }; #endif }; diff --git a/backends/graphics/opengl/opengl-sys.h b/backends/graphics/opengl/opengl-sys.h index 4e21894380..7b531cc140 100644 --- a/backends/graphics/opengl/opengl-sys.h +++ b/backends/graphics/opengl/opengl-sys.h @@ -23,35 +23,153 @@ #ifndef BACKENDS_GRAPHICS_OPENGL_OPENGL_SYS_H #define BACKENDS_GRAPHICS_OPENGL_OPENGL_SYS_H -// The purpose of this header is to include the OpenGL headers in an uniform -// fashion. A notable example for a non standard port is the Tizen port. - #include "common/scummsys.h" -#ifdef WIN32 -#if defined(ARRAYSIZE) && !defined(_WINDOWS_) -#undef ARRAYSIZE +#include "backends/graphics/opengl/debug.h" +#ifdef SDL_BACKEND +#include "backends/platform/sdl/sdl-sys.h" #endif -#define WIN32_LEAN_AND_MEAN -#include <windows.h> -#undef ARRAYSIZE + +// On OS X we only support GL contexts. The reason is that Apple's GL interface +// uses "void *" for GLhandleARB which is not type compatible with GLint. This +// kills our aliasing trick for extension functions and thus would force us to +// supply two different Shader class implementations or introduce other +// wrappers. OS X only supports GL contexts right now anyway (at least +// according to SDL2 sources), thus it is not much of an issue. +#if defined(MACOSX) && (!defined(USE_GLES_MODE) || USE_GLES_MODE != 0) +//#warning "Only forced OpenGL mode is supported on Mac OS X. Overriding settings." +#undef USE_GLES_MODE +#define USE_GLES_MODE 0 #endif -// HACK: In case common/util.h has been included already we need to make sure -// to define ARRAYSIZE again in case of Windows. -#if !defined(ARRAYSIZE) && defined(COMMON_UTIL_H) -#define ARRAYSIZE(x) ((int)(sizeof(x) / sizeof(x[0]))) +// We allow to force GL or GLES modes on compile time. +// For this the USE_GLES_MODE define is used. The following values represent +// the given selection choices: +// 0 - Force OpenGL context +// 1 - Force OpenGL ES context +// 2 - Force OpenGL ES 2.0 context +#ifdef USE_GLES_MODE + #define USE_FORCED_GL (USE_GLES_MODE == 0) + #define USE_FORCED_GLES (USE_GLES_MODE == 1) + #define USE_FORCED_GLES2 (USE_GLES_MODE == 2) +#else + #define USE_FORCED_GL 0 + #define USE_FORCED_GLES 0 + #define USE_FORCED_GLES2 0 #endif +// On Tizen we include the toolchain's OpenGL file. This is something we +// actually want to avoid. However, since Tizen uses eglGetProcAddress which +// is not required to return valid function pointers to non OpenGL extension +// functions, we need the system's definitions to resolve all OpenGL +// functions. +// TODO: See if there is an alternative which allows us to avoid including +// Tizen's OpenGL header here. #if defined(TIZEN) -#include <FGraphicsOpengl.h> -using namespace Tizen::Graphics::Opengl; -#elif defined(USE_GLES) -#include <GLES/gl.h> -#elif defined(SDL_BACKEND) -#include <SDL_opengl.h> + #include <FGraphicsOpengl.h> + using namespace Tizen::Graphics::Opengl; + #define USE_BUILTIN_OPENGL #else -#include <GL/gl.h> + #include "backends/graphics/opengl/opengl-defs.h" #endif +#ifdef SDL_BACKEND + // Win32 needs OpenGL functions declared with APIENTRY. + // However, SDL does not define APIENTRY in it's SDL.h file on non-Windows + // targets, thus if it is not available, we just dummy define it. + #ifndef APIENTRY + #define APIENTRY + #endif + #define GL_CALL_CONV APIENTRY +#else + #define GL_CALL_CONV +#endif + +namespace OpenGL { + +enum ContextType { + kContextGL, + kContextGLES, + kContextGLES2 +}; + +class Pipeline; +class Framebuffer; + +/** + * Description structure of the OpenGL (ES) context. + */ +struct Context { + /** The type of the active context. */ + ContextType type; + + /** + * Reset context. + * + * This marks all extensions as unavailable and clears all function + * pointers. + */ + void reset(); + + /** The maximum texture size supported by the context. */ + GLint maxTextureSize; + + /** Whether GL_ARB_texture_non_power_of_two is available or not. */ + bool NPOTSupported; + + /** Whether shader support is available or not. */ + bool shadersSupported; + + /** Whether multi texture support is available or not. */ + bool multitextureSupported; + + /** Whether FBO support is available or not. */ + bool framebufferObjectSupported; + +#define GL_FUNC_DEF(ret, name, param) ret (GL_CALL_CONV *name)param +#include "backends/graphics/opengl/opengl-func.h" +#undef GL_FUNC_DEF + + // + // Wrapper functionality to handle fixed-function pipelines and + // programmable pipelines in the same fashion. + // + +private: + /** Currently active rendering pipeline. */ + Pipeline *activePipeline; + +public: + /** + * Set new pipeline. + * + * Client is responsible for any memory management related to pipelines. + * + * @param pipeline Pipeline to activate. + * @return Formerly active pipeline. + */ + Pipeline *setPipeline(Pipeline *pipeline); + + /** + * Query the currently active rendering pipeline. + */ + Pipeline *getActivePipeline() const { return activePipeline; } +}; + +/** + * The (active) OpenGL context. + */ +extern Context g_context; + +} // End of namespace OpenGL + +#define GL_CALL(x) GL_WRAP_DEBUG(g_context.x, x) +#define GL_CALL_SAFE(func, params) \ + do { \ + if (g_context.func) { \ + GL_CALL(func params); \ + } \ + } while (0) +#define GL_ASSIGN(var, x) GL_WRAP_DEBUG(var = g_context.x, x) + #endif diff --git a/backends/graphics/opengl/pipelines/clut8.cpp b/backends/graphics/opengl/pipelines/clut8.cpp new file mode 100644 index 0000000000..fca40074f0 --- /dev/null +++ b/backends/graphics/opengl/pipelines/clut8.cpp @@ -0,0 +1,46 @@ +/* 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 "backends/graphics/opengl/pipelines/clut8.h" +#include "backends/graphics/opengl/shader.h" +#include "backends/graphics/opengl/framebuffer.h" + +namespace OpenGL { + +#if !USE_FORCED_GLES +CLUT8LookUpPipeline::CLUT8LookUpPipeline() + : ShaderPipeline(ShaderMan.query(ShaderManager::kCLUT8LookUp)), _paletteTexture(nullptr) { +} + +void CLUT8LookUpPipeline::drawTexture(const GLTexture &texture, const GLfloat *coordinates) { + // Set the palette texture. + GL_CALL(glActiveTexture(GL_TEXTURE1)); + if (_paletteTexture) { + _paletteTexture->bind(); + } + + GL_CALL(glActiveTexture(GL_TEXTURE0)); + ShaderPipeline::drawTexture(texture, coordinates); +} +#endif // !USE_FORCED_GLES + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/extensions.cpp b/backends/graphics/opengl/pipelines/clut8.h index 4482ef82b5..16724e4652 100644 --- a/backends/graphics/opengl/extensions.cpp +++ b/backends/graphics/opengl/pipelines/clut8.h @@ -20,29 +20,27 @@ * */ -#include "backends/graphics/opengl/extensions.h" -#include "backends/graphics/opengl/opengl-sys.h" +#ifndef BACKENDS_GRAPHICS_OPENGL_PIPELINES_CLUT8_H +#define BACKENDS_GRAPHICS_OPENGL_PIPELINES_CLUT8_H -#include "common/tokenizer.h" +#include "backends/graphics/opengl/pipelines/shader.h" namespace OpenGL { -bool g_extNPOTSupported = false; +#if !USE_FORCED_GLES +class CLUT8LookUpPipeline : public ShaderPipeline { +public: + CLUT8LookUpPipeline(); -void initializeGLExtensions() { - const char *extString = (const char *)glGetString(GL_EXTENSIONS); + void setPaletteTexture(const GLTexture *paletteTexture) { _paletteTexture = paletteTexture; } - // Initialize default state. - g_extNPOTSupported = false; + virtual void drawTexture(const GLTexture &texture, const GLfloat *coordinates); - Common::StringTokenizer tokenizer(extString, " "); - while (!tokenizer.empty()) { - Common::String token = tokenizer.nextToken(); - - if (token == "GL_ARB_texture_non_power_of_two") { - g_extNPOTSupported = true; - } - } -} +private: + const GLTexture *_paletteTexture; +}; +#endif // !USE_FORCED_GLES } // End of namespace OpenGL + +#endif diff --git a/backends/graphics/opengl/pipelines/fixed.cpp b/backends/graphics/opengl/pipelines/fixed.cpp new file mode 100644 index 0000000000..8e3bd7eaee --- /dev/null +++ b/backends/graphics/opengl/pipelines/fixed.cpp @@ -0,0 +1,70 @@ +/* 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 "backends/graphics/opengl/pipelines/fixed.h" + +namespace OpenGL { + +#if !USE_FORCED_GLES2 +void FixedPipeline::activateInternal() { + GL_CALL(glDisable(GL_LIGHTING)); + GL_CALL(glDisable(GL_FOG)); + GL_CALL(glShadeModel(GL_FLAT)); + GL_CALL(glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST)); + + GL_CALL(glEnableClientState(GL_VERTEX_ARRAY)); + GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY)); + +#if !USE_FORCED_GLES + if (g_context.multitextureSupported) { + GL_CALL(glActiveTexture(GL_TEXTURE0)); + } +#endif + GL_CALL(glEnable(GL_TEXTURE_2D)); +} + +void FixedPipeline::setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { + GL_CALL(glColor4f(r, g, b, a)); +} + +void FixedPipeline::drawTexture(const GLTexture &texture, const GLfloat *coordinates) { + texture.bind(); + + GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texture.getTexCoords())); + GL_CALL(glVertexPointer(2, GL_FLOAT, 0, coordinates)); + GL_CALL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); +} + +void FixedPipeline::setProjectionMatrix(const GLfloat *projectionMatrix) { + if (!isActive()) { + return; + } + + GL_CALL(glMatrixMode(GL_PROJECTION)); + GL_CALL(glLoadMatrixf(projectionMatrix)); + + GL_CALL(glMatrixMode(GL_MODELVIEW)); + GL_CALL(glLoadIdentity()); +} +#endif // !USE_FORCED_GLES2 + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/pipelines/fixed.h b/backends/graphics/opengl/pipelines/fixed.h new file mode 100644 index 0000000000..6bfe140c19 --- /dev/null +++ b/backends/graphics/opengl/pipelines/fixed.h @@ -0,0 +1,46 @@ +/* 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 BACKENDS_GRAPHICS_OPENGL_PIPELINES_FIXED_H +#define BACKENDS_GRAPHICS_OPENGL_PIPELINES_FIXED_H + +#include "backends/graphics/opengl/pipelines/pipeline.h" + +namespace OpenGL { + +#if !USE_FORCED_GLES2 +class FixedPipeline : public Pipeline { +public: + virtual void setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a); + + virtual void drawTexture(const GLTexture &texture, const GLfloat *coordinates); + + virtual void setProjectionMatrix(const GLfloat *projectionMatrix); + +protected: + virtual void activateInternal(); +}; +#endif // !USE_FORCED_GLES2 + +} // End of namespace OpenGL + +#endif diff --git a/backends/graphics/opengl/pipelines/pipeline.cpp b/backends/graphics/opengl/pipelines/pipeline.cpp new file mode 100644 index 0000000000..6a59cd28e7 --- /dev/null +++ b/backends/graphics/opengl/pipelines/pipeline.cpp @@ -0,0 +1,66 @@ +/* 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 "backends/graphics/opengl/pipelines/pipeline.h" +#include "backends/graphics/opengl/framebuffer.h" + +namespace OpenGL { + +Pipeline::Pipeline() + : _activeFramebuffer(nullptr), _isActive(false) { +} + +void Pipeline::activate() { + _isActive = true; + + if (_activeFramebuffer) { + _activeFramebuffer->activate(); + } + + activateInternal(); +} + +void Pipeline::deactivate() { + deactivateInternal(); + + if (_activeFramebuffer) { + _activeFramebuffer->deactivate(); + } + + _isActive = false; +} + +Framebuffer *Pipeline::setFramebuffer(Framebuffer *framebuffer) { + Framebuffer *oldFramebuffer = _activeFramebuffer; + if (_isActive && oldFramebuffer) { + oldFramebuffer->deactivate(); + } + + _activeFramebuffer = framebuffer; + if (_isActive && _activeFramebuffer) { + _activeFramebuffer->activate(); + } + + return oldFramebuffer; +} + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/pipelines/pipeline.h b/backends/graphics/opengl/pipelines/pipeline.h new file mode 100644 index 0000000000..9f32d33b95 --- /dev/null +++ b/backends/graphics/opengl/pipelines/pipeline.h @@ -0,0 +1,126 @@ +/* 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 BACKENDS_GRAPHICS_OPENGL_PIPELINES_PIPELINE_H +#define BACKENDS_GRAPHICS_OPENGL_PIPELINES_PIPELINE_H + +#include "backends/graphics/opengl/opengl-sys.h" +#include "backends/graphics/opengl/texture.h" + +namespace OpenGL { + +class Framebuffer; + +/** + * Interface for OpenGL pipeline functionality. + * + * This encapsulates differences in various rendering pipelines used for + * OpenGL, OpenGL ES 1, and OpenGL ES 2. + */ +class Pipeline { +public: + Pipeline(); + virtual ~Pipeline() {} + + /** + * Activate the pipeline. + * + * This sets the OpenGL state to make use of drawing with the given + * OpenGL pipeline. + */ + void activate(); + + /** + * Deactivate the pipeline. + */ + void deactivate(); + + /** + * Set framebuffer to render to. + * + * Client is responsible for any memory management related to framebuffer. + * + * @param framebuffer Framebuffer to activate. + * @return Formerly active framebuffer. + */ + Framebuffer *setFramebuffer(Framebuffer *framebuffer); + + /** + * Set modulation color. + * + * @param r Red component in [0,1]. + * @param g Green component in [0,1]. + * @param b Blue component in [0,1]. + * @param a Alpha component in [0,1]. + */ + virtual void setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) = 0; + + /** + * Draw a texture rectangle to the currently active framebuffer. + * + * @param texture Texture to use for drawing. + * @param coordinates x1, y1, x2, y2 coordinates where to draw the texture. + */ + virtual void drawTexture(const GLTexture &texture, const GLfloat *coordinates) = 0; + + void drawTexture(const GLTexture &texture, GLfloat x, GLfloat y, GLfloat w, GLfloat h) { + const GLfloat coordinates[4*2] = { + x, y, + x + w, y, + x, y + h, + x + w, y + h + }; + drawTexture(texture, coordinates); + } + + /** + * Set the projection matrix. + * + * This is intended to be only ever be used by Framebuffer subclasses. + */ + virtual void setProjectionMatrix(const GLfloat *projectionMatrix) = 0; + +protected: + /** + * Activate the pipeline. + * + * This sets the OpenGL state to make use of drawing with the given + * OpenGL pipeline. + */ + virtual void activateInternal() = 0; + + /** + * Deactivate the pipeline. + */ + virtual void deactivateInternal() {} + + bool isActive() const { return _isActive; } + + Framebuffer *_activeFramebuffer; + +private: + bool _isActive; +}; + +} // End of namespace OpenGL + +#endif diff --git a/backends/graphics/opengl/pipelines/shader.cpp b/backends/graphics/opengl/pipelines/shader.cpp new file mode 100644 index 0000000000..a2dabb7c22 --- /dev/null +++ b/backends/graphics/opengl/pipelines/shader.cpp @@ -0,0 +1,94 @@ +/* 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 "backends/graphics/opengl/pipelines/shader.h" +#include "backends/graphics/opengl/shader.h" +#include "backends/graphics/opengl/framebuffer.h" + +namespace OpenGL { + +#if !USE_FORCED_GLES +ShaderPipeline::ShaderPipeline(Shader *shader) + : _activeShader(shader), _colorAttributes() { + _vertexAttribLocation = shader->getAttributeLocation("position"); + _texCoordAttribLocation = shader->getAttributeLocation("texCoordIn"); + _colorAttribLocation = shader->getAttributeLocation("blendColorIn"); + + assert(_vertexAttribLocation != -1); + assert(_texCoordAttribLocation != -1); + assert(_colorAttribLocation != -1); + + // One of the attributes needs to be passed through location 0, otherwise + // we get no output for GL contexts due to GL compatibility reasons. Let's + // check whether this ever happens. If this ever gets hit, we need to + // enable location 0 and pass some dummy values through it to fix output. + assert( _vertexAttribLocation == 0 + || _texCoordAttribLocation == 0 + || _colorAttribLocation == 0); +} + +void ShaderPipeline::activateInternal() { + GL_CALL(glEnableVertexAttribArray(_vertexAttribLocation)); + GL_CALL(glEnableVertexAttribArray(_texCoordAttribLocation)); + GL_CALL(glEnableVertexAttribArray(_colorAttribLocation)); + + if (g_context.multitextureSupported) { + GL_CALL(glActiveTexture(GL_TEXTURE0)); + } + + _activeShader->activate(); + + GL_CALL(glVertexAttribPointer(_colorAttribLocation, 4, GL_FLOAT, GL_FALSE, 0, _colorAttributes)); +} + +void ShaderPipeline::deactivateInternal() { + GL_CALL(glDisableVertexAttribArray(_vertexAttribLocation)); + GL_CALL(glDisableVertexAttribArray(_texCoordAttribLocation)); + GL_CALL(glDisableVertexAttribArray(_colorAttribLocation)); + + _activeShader->deactivate(); +} + +void ShaderPipeline::setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { + GLfloat *dst = _colorAttributes; + for (uint i = 0; i < 4; ++i) { + *dst++ = r; + *dst++ = g; + *dst++ = b; + *dst++ = a; + } +} + +void ShaderPipeline::drawTexture(const GLTexture &texture, const GLfloat *coordinates) { + texture.bind(); + + GL_CALL(glVertexAttribPointer(_texCoordAttribLocation, 2, GL_FLOAT, GL_FALSE, 0, texture.getTexCoords())); + GL_CALL(glVertexAttribPointer(_vertexAttribLocation, 2, GL_FLOAT, GL_FALSE, 0, coordinates)); + GL_CALL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); +} + +void ShaderPipeline::setProjectionMatrix(const GLfloat *projectionMatrix) { + _activeShader->setUniform("projection", new ShaderUniformMatrix44(projectionMatrix)); +} +#endif // !USE_FORCED_GLES + +} // End of namespace OpenGL diff --git a/backends/graphics/opengl/pipelines/shader.h b/backends/graphics/opengl/pipelines/shader.h new file mode 100644 index 0000000000..6159607099 --- /dev/null +++ b/backends/graphics/opengl/pipelines/shader.h @@ -0,0 +1,59 @@ +/* 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 BACKENDS_GRAPHICS_OPENGL_PIPELINES_SHADER_H +#define BACKENDS_GRAPHICS_OPENGL_PIPELINES_SHADER_H + +#include "backends/graphics/opengl/pipelines/pipeline.h" + +namespace OpenGL { + +#if !USE_FORCED_GLES +class Shader; + +class ShaderPipeline : public Pipeline { +public: + ShaderPipeline(Shader *shader); + + virtual void setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a); + + virtual void drawTexture(const GLTexture &texture, const GLfloat *coordinates); + + virtual void setProjectionMatrix(const GLfloat *projectionMatrix); + +protected: + virtual void activateInternal(); + virtual void deactivateInternal(); + + GLint _vertexAttribLocation; + GLint _texCoordAttribLocation; + GLint _colorAttribLocation; + + GLfloat _colorAttributes[4*4]; + + Shader *const _activeShader; +}; +#endif // !USE_FORCED_GLES + +} // End of namespace OpenGL + +#endif diff --git a/backends/graphics/opengl/shader.cpp b/backends/graphics/opengl/shader.cpp new file mode 100644 index 0000000000..0b4c677d70 --- /dev/null +++ b/backends/graphics/opengl/shader.cpp @@ -0,0 +1,335 @@ +/* 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 "backends/graphics/opengl/shader.h" + +#if !USE_FORCED_GLES + +#include "common/textconsole.h" +#include "common/util.h" + +namespace Common { +DECLARE_SINGLETON(OpenGL::ShaderManager); +} + +namespace OpenGL { + +namespace { + +#pragma mark - Builtin Shader Sources - + +const char *const g_defaultVertexShader = + "attribute vec4 position;\n" + "attribute vec2 texCoordIn;\n" + "attribute vec4 blendColorIn;\n" + "\n" + "uniform mat4 projection;\n" + "\n" + "varying vec2 texCoord;\n" + "varying vec4 blendColor;\n" + "\n" + "void main(void) {\n" + "\ttexCoord = texCoordIn;\n" + "\tblendColor = blendColorIn;\n" + "\tgl_Position = projection * position;\n" + "}\n"; + +const char *const g_defaultFragmentShader = + "varying vec2 texCoord;\n" + "varying vec4 blendColor;\n" + "\n" + "uniform sampler2D texture;\n" + "\n" + "void main(void) {\n" + "\tgl_FragColor = blendColor * texture2D(texture, texCoord);\n" + "}\n"; + +const char *const g_lookUpFragmentShader = + "varying vec2 texCoord;\n" + "varying vec4 blendColor;\n" + "\n" + "uniform sampler2D texture;\n" + "uniform sampler2D palette;\n" + "\n" + "const float adjustFactor = 255.0 / 256.0 + 1.0 / (2.0 * 256.0);" + "\n" + "void main(void) {\n" + "\tvec4 index = texture2D(texture, texCoord);\n" + "\tgl_FragColor = blendColor * texture2D(palette, vec2(index.a * adjustFactor, 0.0));\n" + "}\n"; + + +// Taken from: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_03#OpenGL_ES_2_portability +const char *const g_precisionDefines = + "#ifdef GL_ES\n" + "\t#if defined(GL_FRAGMENT_PRECISION_HIGH) && GL_FRAGMENT_PRECISION_HIGH == 1\n" + "\t\tprecision highp float;\n" + "\t#else\n" + "\t\tprecision mediump float;\n" + "\t#endif\n" + "#else\n" + "\t#define highp\n" + "\t#define mediump\n" + "\t#define lowp\n" + "#endif\n"; + +} // End of anonymous namespace + +#pragma mark - Uniform Values - + +void ShaderUniformInteger::set(GLint location) const { + GL_CALL(glUniform1i(location, _value)); +} + +void ShaderUniformFloat::set(GLint location) const { + GL_CALL(glUniform1f(location, _value)); +} + +void ShaderUniformMatrix44::set(GLint location) const { + GL_CALL(glUniformMatrix4fv(location, 1, GL_FALSE, _matrix)); +} + +#pragma mark - Shader Implementation - + +Shader::Shader(const Common::String &vertex, const Common::String &fragment) + : _vertex(vertex), _fragment(fragment), _isActive(false), _program(0), _uniforms() { + recreate(); +} + +Shader::~Shader() { + // According to extension specification glDeleteObjectARB silently ignores + // 0. However, with nVidia drivers this can cause GL_INVALID_VALUE, thus + // we do not call it with 0 as parameter to avoid warnings. + if (_program) { + GL_CALL_SAFE(glDeleteProgram, (_program)); + } +} + +void Shader::destroy() { + // According to extension specification glDeleteObjectARB silently ignores + // 0. However, with nVidia drivers this can cause GL_INVALID_VALUE, thus + // we do not call it with 0 as parameter to avoid warnings. + if (_program) { + GL_CALL(glDeleteProgram(_program)); + _program = 0; + } +} + +bool Shader::recreate() { + // Make sure any old programs are destroyed properly. + destroy(); + + GLshader vertexShader = compileShader(_vertex.c_str(), GL_VERTEX_SHADER); + if (!vertexShader) { + return false; + } + + GLshader fragmentShader = compileShader(_fragment.c_str(), GL_FRAGMENT_SHADER); + if (!fragmentShader) { + GL_CALL(glDeleteShader(vertexShader)); + return false; + } + + GL_ASSIGN(_program, glCreateProgram()); + if (!_program) { + GL_CALL(glDeleteShader(vertexShader)); + GL_CALL(glDeleteShader(fragmentShader)); + return false; + } + + GL_CALL(glAttachShader(_program, vertexShader)); + GL_CALL(glAttachShader(_program, fragmentShader)); + + GL_CALL(glLinkProgram(_program)); + + GL_CALL(glDetachShader(_program, fragmentShader)); + GL_CALL(glDeleteShader(fragmentShader)); + + GL_CALL(glDetachShader(_program, vertexShader)); + GL_CALL(glDeleteShader(vertexShader)); + + GLint result; + GL_CALL(glGetProgramiv(_program, GL_LINK_STATUS, &result)); + if (result == GL_FALSE) { + GLint logSize; + GL_CALL(glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &logSize)); + + GLchar *log = new GLchar[logSize]; + GL_CALL(glGetProgramInfoLog(_program, logSize, nullptr, log)); + warning("Could not link shader: \"%s\"", log); + delete[] log; + + destroy(); + return false; + } + + // Set program object in case shader is active during recreation. + if (_isActive) { + GL_CALL(glUseProgram(_program)); + } + + for (UniformMap::iterator i = _uniforms.begin(), end = _uniforms.end(); i != end; ++i) { + i->_value.location = getUniformLocation(i->_key.c_str()); + i->_value.altered = true; + if (_isActive) { + i->_value.set(); + } + } + + return true; +} + +void Shader::activate() { + // Activate program. + GL_CALL(glUseProgram(_program)); + + // Reset changed uniform values. + for (UniformMap::iterator i = _uniforms.begin(), end = _uniforms.end(); i != end; ++i) { + i->_value.set(); + } + + _isActive = true; +} + +void Shader::deactivate() { + _isActive = false; +} + +GLint Shader::getAttributeLocation(const char *name) const { + GLint result = -1; + GL_ASSIGN(result, glGetAttribLocation(_program, name)); + return result; +} + +GLint Shader::getUniformLocation(const char *name) const { + GLint result = -1; + GL_ASSIGN(result, glGetUniformLocation(_program, name)); + return result; +} + +bool Shader::setUniform(const Common::String &name, ShaderUniformValue *value) { + UniformMap::iterator uniformIter = _uniforms.find(name); + Uniform *uniform; + + if (uniformIter == _uniforms.end()) { + const GLint location = getUniformLocation(name.c_str()); + if (location == -1) { + delete value; + return false; + } + + uniform = &_uniforms[name]; + uniform->location = location; + } else { + uniform = &uniformIter->_value; + } + + uniform->value = Common::SharedPtr<ShaderUniformValue>(value); + uniform->altered = true; + if (_isActive) { + uniform->set(); + } + + return true; +} + +GLshader Shader::compileShader(const char *source, GLenum shaderType) { + GLshader handle; + GL_ASSIGN(handle, glCreateShader(shaderType)); + if (!handle) { + return 0; + } + + const char *const sources[2] = { + g_precisionDefines, + source + }; + + GL_CALL(glShaderSource(handle, ARRAYSIZE(sources), sources, nullptr)); + GL_CALL(glCompileShader(handle)); + + GLint result; + GL_CALL(glGetShaderiv(handle, GL_COMPILE_STATUS, &result)); + if (result == GL_FALSE) { + GLint logSize; + GL_CALL(glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &logSize)); + + GLchar *log = new GLchar[logSize]; + GL_CALL(glGetShaderInfoLog(handle, logSize, nullptr, log)); + warning("Could not compile shader \"%s\": \"%s\"", source, log); + delete[] log; + + GL_CALL(glDeleteShader(handle)); + return 0; + } + + return handle; +} + +ShaderManager::ShaderManager() : _initializeShaders(true) { + for (int i = 0; i < ARRAYSIZE(_builtIn); ++i) { + _builtIn[i] = nullptr; + } +} + +ShaderManager::~ShaderManager() { + for (int i = 0; i < ARRAYSIZE(_builtIn); ++i) { + delete _builtIn[i]; + } +} + +void ShaderManager::notifyDestroy() { + for (int i = 0; i < ARRAYSIZE(_builtIn); ++i) { + _builtIn[i]->destroy(); + } +} + +void ShaderManager::notifyCreate() { + if (_initializeShaders) { + _initializeShaders = false; + + _builtIn[kDefault] = new Shader(g_defaultVertexShader, g_defaultFragmentShader); + _builtIn[kCLUT8LookUp] = new Shader(g_defaultVertexShader, g_lookUpFragmentShader); + _builtIn[kCLUT8LookUp]->setUniform1I("palette", 1); + + for (uint i = 0; i < kMaxUsages; ++i) { + _builtIn[i]->setUniform1I("texture", 0); + } + } else { + for (int i = 0; i < ARRAYSIZE(_builtIn); ++i) { + _builtIn[i]->recreate(); + } + } +} + +Shader *ShaderManager::query(ShaderUsage shader) const { + if (shader == kMaxUsages) { + warning("OpenGL: ShaderManager::query used with kMaxUsages"); + return nullptr; + } + + return _builtIn[shader]; +} + +} // End of namespace OpenGL + +#endif // !USE_FORCED_GLES diff --git a/backends/graphics/opengl/shader.h b/backends/graphics/opengl/shader.h new file mode 100644 index 0000000000..ec1e516d14 --- /dev/null +++ b/backends/graphics/opengl/shader.h @@ -0,0 +1,288 @@ +/* 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 BACKENDS_GRAPHICS_OPENGL_SHADER_H +#define BACKENDS_GRAPHICS_OPENGL_SHADER_H + +#include "backends/graphics/opengl/opengl-sys.h" + +#if !USE_FORCED_GLES + +#include "common/singleton.h" +#include "common/hash-str.h" +#include "common/ptr.h" + +namespace OpenGL { + +/** + * A generic uniform value interface for a shader program. + */ +class ShaderUniformValue { +public: + virtual ~ShaderUniformValue() {} + + /** + * Setup the the value to the given location. + * + * @param location Location of the uniform. + */ + virtual void set(GLint location) const = 0; +}; + +/** + * Integer value for a shader uniform. + */ +class ShaderUniformInteger : public ShaderUniformValue { +public: + ShaderUniformInteger(GLint value) : _value(value) {} + + virtual void set(GLint location) const override; + +private: + const GLint _value; +}; + +/** + * Float value for a shader uniform. + */ +class ShaderUniformFloat : public ShaderUniformValue { +public: + ShaderUniformFloat(GLfloat value) : _value(value) {} + + virtual void set(GLint location) const override; + +private: + const GLfloat _value; +}; + +/** + * 4x4 Matrix value for a shader uniform. + */ +class ShaderUniformMatrix44 : public ShaderUniformValue { +public: + ShaderUniformMatrix44(const GLfloat *mat44) { + memcpy(_matrix, mat44, sizeof(_matrix)); + } + + virtual void set(GLint location) const override; + +private: + GLfloat _matrix[4*4]; +}; + +class Shader { +public: + Shader(const Common::String &vertex, const Common::String &fragment); + ~Shader(); + + /** + * Destroy the shader program. + * + * This keeps the vertex and fragment shader sources around and thus + * allows for recreating the shader on context recreation. It also keeps + * the uniform state around. + */ + void destroy(); + + /** + * Recreate shader program. + * + * @return true on success, false on failure. + */ + bool recreate(); + + /** + * Make shader active. + */ + void activate(); + + /** + * Make shader inactive. + */ + void deactivate(); + + /** + * Return location for attribute with given name. + * + * @param name Name of the attribute to look up in the shader. + * @return The loctaion of -1 if attribute was not found. + */ + GLint getAttributeLocation(const char *name) const; + GLint getAttributeLocation(const Common::String &name) const { + return getAttributeLocation(name.c_str()); + } + + /** + * Return location for uniform with given name. + * + * @param name Name of the uniform to look up in the shader. + * @return The location or -1 if uniform was not found. + */ + GLint getUniformLocation(const char *name) const; + GLint getUniformLocation(const Common::String &name) const { + return getUniformLocation(name.c_str()); + } + + /** + * Bind value to uniform. + * + * @param name The name of the uniform to be set. + * @param value The value to be set. + * @return 'false' on error (i.e. uniform unknown or otherwise), + * 'true' otherwise. + */ + bool setUniform(const Common::String &name, ShaderUniformValue *value); + + /** + * Bind integer value to uniform. + * + * @param name The name of the uniform to be set. + * @param value The value to be set. + * @return 'false' on error (i.e. uniform unknown or otherwise), + * 'true' otherwise. + */ + bool setUniform1I(const Common::String &name, GLint value) { + return setUniform(name, new ShaderUniformInteger(value)); + } +protected: + /** + * Vertex shader sources. + */ + const Common::String _vertex; + + /** + * Fragment shader sources. + */ + const Common::String _fragment; + + /** + * Whether the shader is active or not. + */ + bool _isActive; + + /** + * Shader program handle. + */ + GLprogram _program; + + /** + * A uniform descriptor. + * + * This stores the state of a shader uniform. The state is made up of the + * uniform location, whether the state was altered since last set, and the + * value of the uniform. + */ + struct Uniform { + Uniform() : location(-1), altered(false), value() {} + Uniform(GLint loc, ShaderUniformValue *val) + : location(loc), altered(true), value(val) {} + + /** + * Write uniform value into currently active shader. + */ + void set() { + if (altered && value) { + value->set(location); + altered = false; + } + } + + /** + * The location of the uniform or -1 in case it does not exist. + */ + GLint location; + + /** + * Whether the uniform state was aletered since last 'set'. + */ + bool altered; + + /** + * The value of the uniform. + */ + Common::SharedPtr<ShaderUniformValue> value; + }; + + typedef Common::HashMap<Common::String, Uniform> UniformMap; + + /** + * Map from uniform name to associated uniform description. + */ + UniformMap _uniforms; + + /** + * Compile a vertex or fragment shader. + * + * @param source Sources to the shader. + * @param shaderType Type of shader to compile (GL_FRAGMENT_SHADER_ARB or + * GL_VERTEX_SHADER_ARB) + * @return The shader object or 0 on failure. + */ + static GLshader compileShader(const char *source, GLenum shaderType); +}; + +class ShaderManager : public Common::Singleton<ShaderManager> { +public: + enum ShaderUsage { + /** Default shader implementing the GL fixed-function pipeline. */ + kDefault = 0, + + /** CLUT8 look up shader. */ + kCLUT8LookUp, + + /** Number of built-in shaders. Should not be used for query. */ + kMaxUsages + }; + + /** + * Notify shader manager about context destruction. + */ + void notifyDestroy(); + + /** + * Notify shader manager about context creation. + */ + void notifyCreate(); + + /** + * Query a built-in shader. + */ + Shader *query(ShaderUsage shader) const; + +private: + friend class Common::Singleton<SingletonBaseType>; + ShaderManager(); + ~ShaderManager(); + + bool _initializeShaders; + + Shader *_builtIn[kMaxUsages]; +}; + +} // End of namespace OpenGL + +/** Shortcut for accessing the font manager. */ +#define ShaderMan (OpenGL::ShaderManager::instance()) + +#endif // !USE_FORCED_GLES + +#endif diff --git a/backends/graphics/opengl/texture.cpp b/backends/graphics/opengl/texture.cpp index 7b0b22d630..33598b5488 100644 --- a/backends/graphics/opengl/texture.cpp +++ b/backends/graphics/opengl/texture.cpp @@ -21,8 +21,10 @@ */ #include "backends/graphics/opengl/texture.h" -#include "backends/graphics/opengl/extensions.h" -#include "backends/graphics/opengl/debug.h" +#include "backends/graphics/opengl/shader.h" +#include "backends/graphics/opengl/pipelines/pipeline.h" +#include "backends/graphics/opengl/pipelines/clut8.h" +#include "backends/graphics/opengl/framebuffer.h" #include "common/rect.h" #include "common/textconsole.h" @@ -41,94 +43,140 @@ static GLuint nextHigher2(GLuint v) { return ++v; } -GLint Texture::_maxTextureSize = 0; -void Texture::queryTextureInformation() { - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_maxTextureSize); - debug(5, "OpenGL maximum texture size: %d", _maxTextureSize); +GLTexture::GLTexture(GLenum glIntFormat, GLenum glFormat, GLenum glType) + : _glIntFormat(glIntFormat), _glFormat(glFormat), _glType(glType), + _width(0), _height(0), _logicalWidth(0), _logicalHeight(0), + _texCoords(), _glFilter(GL_NEAREST), + _glTexture(0) { + create(); } -Texture::Texture(GLenum glIntFormat, GLenum glFormat, GLenum glType, const Graphics::PixelFormat &format) - : _glIntFormat(glIntFormat), _glFormat(glFormat), _glType(glType), _format(format), _glFilter(GL_NEAREST), - _glTexture(0), _textureData(), _userPixelData(), _allDirty(false) { - recreateInternalTexture(); +GLTexture::~GLTexture() { + GL_CALL_SAFE(glDeleteTextures, (1, &_glTexture)); } -Texture::~Texture() { - releaseInternalTexture(); - _textureData.free(); +void GLTexture::enableLinearFiltering(bool enable) { + if (enable) { + _glFilter = GL_LINEAR; + } else { + _glFilter = GL_NEAREST; + } + + bind(); + + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _glFilter)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _glFilter)); } -void Texture::releaseInternalTexture() { - GLCALL(glDeleteTextures(1, &_glTexture)); +void GLTexture::destroy() { + GL_CALL(glDeleteTextures(1, &_glTexture)); _glTexture = 0; } -void Texture::recreateInternalTexture() { +void GLTexture::create() { // Release old texture name in case it exists. - releaseInternalTexture(); + destroy(); // Get a new texture name. - GLCALL(glGenTextures(1, &_glTexture)); + GL_CALL(glGenTextures(1, &_glTexture)); // Set up all texture parameters. - GLCALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); - GLCALL(glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _glFilter)); - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _glFilter)); - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); - - // In case there is an actual texture setup we reinitialize it. - if (_textureData.getPixels()) { + bind(); + GL_CALL(glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _glFilter)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _glFilter)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + + // If a size is specified, allocate memory for it. + if (_width != 0 && _height != 0) { // Allocate storage for OpenGL texture. - GLCALL(glTexImage2D(GL_TEXTURE_2D, 0, _glIntFormat, _textureData.w, - _textureData.h, 0, _glFormat, _glType, NULL)); - - // Mark dirts such that it will be completely refreshed the next time. - flagDirty(); + GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, _glIntFormat, _width, _height, + 0, _glFormat, _glType, NULL)); } } -void Texture::enableLinearFiltering(bool enable) { - if (enable) { - _glFilter = GL_LINEAR; +void GLTexture::bind() const { + GL_CALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); +} + +void GLTexture::setSize(uint width, uint height) { + const uint oldWidth = _width; + const uint oldHeight = _height; + + if (!g_context.NPOTSupported) { + _width = nextHigher2(width); + _height = nextHigher2(height); } else { - _glFilter = GL_NEAREST; + _width = width; + _height = height; } - GLCALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); + _logicalWidth = width; + _logicalHeight = height; - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _glFilter)); - GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _glFilter)); -} + // If a size is specified, allocate memory for it. + if (width != 0 && height != 0) { + const GLfloat texWidth = (GLfloat)width / _width; + const GLfloat texHeight = (GLfloat)height / _height; -void Texture::allocate(uint width, uint height) { - uint texWidth = width, texHeight = height; - if (!g_extNPOTSupported) { - texWidth = nextHigher2(texWidth); - texHeight = nextHigher2(texHeight); - } + _texCoords[0] = 0; + _texCoords[1] = 0; - // In case the needed texture dimension changed we will reinitialize the - // texture. - if (texWidth != _textureData.w || texHeight != _textureData.h) { - // Create a buffer for the texture data. - _textureData.create(texWidth, texHeight, _format); + _texCoords[2] = texWidth; + _texCoords[3] = 0; - // Set the texture. - GLCALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); + _texCoords[4] = 0; + _texCoords[5] = texHeight; - // Allocate storage for OpenGL texture. - GLCALL(glTexImage2D(GL_TEXTURE_2D, 0, _glIntFormat, _textureData.w, - _textureData.h, 0, _glFormat, _glType, NULL)); + _texCoords[6] = texWidth; + _texCoords[7] = texHeight; + + // Allocate storage for OpenGL texture if necessary. + if (oldWidth != _width || oldHeight != _height) { + bind(); + GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, _glIntFormat, _width, + _height, 0, _glFormat, _glType, NULL)); + } } +} - // Create a sub-buffer for raw access. - _userPixelData = _textureData.getSubArea(Common::Rect(width, height)); +void GLTexture::updateArea(const Common::Rect &area, const Graphics::Surface &src) { + // Set the texture on the active texture unit. + bind(); + + // Update the actual texture. + // Although we have the area of the texture buffer we want to update we + // cannot take advantage of the left/right boundries here because it is + // not possible to specify a pitch to glTexSubImage2D. To be precise, with + // plain OpenGL we could set GL_UNPACK_ROW_LENGTH to achieve this. However, + // OpenGL ES 1.0 does not support GL_UNPACK_ROW_LENGTH. Thus, we are left + // with the following options: + // + // 1) (As we do right now) Simply always update the whole texture lines of + // rect changed. This is simplest to implement. In case performance is + // really an issue we can think of switching to another method. + // + // 2) Copy the dirty rect to a temporary buffer and upload that by using + // glTexSubImage2D. This is what the Android backend does. It is more + // complicated though. + // + // 3) Use glTexSubImage2D per line changed. This is what the old OpenGL + // graphics manager did but it is much slower! Thus, we do not use it. + GL_CALL(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, area.top, src.w, area.height(), + _glFormat, _glType, src.getBasePtr(0, area.top))); +} + +// +// Surface +// + +Surface::Surface() + : _allDirty(false), _dirtyArea() { } -void Texture::copyRectToTexture(uint x, uint y, uint w, uint h, const void *srcPtr, uint srcPitch) { +void Surface::copyRectToTexture(uint x, uint y, uint w, uint h, const void *srcPtr, uint srcPitch) { Graphics::Surface *dstSurf = getSurface(); assert(x + w <= dstSurf->w); assert(y + h <= dstSurf->h); @@ -159,50 +207,74 @@ void Texture::copyRectToTexture(uint x, uint y, uint w, uint h, const void *srcP } } -void Texture::fill(uint32 color) { +void Surface::fill(uint32 color) { Graphics::Surface *dst = getSurface(); dst->fillRect(Common::Rect(dst->w, dst->h), color); flagDirty(); } -void Texture::draw(GLfloat x, GLfloat y, GLfloat w, GLfloat h) { - // Only do any processing when the Texture is initialized. - if (!_textureData.getPixels()) { - return; +Common::Rect Surface::getDirtyArea() const { + if (_allDirty) { + return Common::Rect(getWidth(), getHeight()); + } else { + return _dirtyArea; + } +} + +// +// Surface implementations +// + +Texture::Texture(GLenum glIntFormat, GLenum glFormat, GLenum glType, const Graphics::PixelFormat &format) + : Surface(), _format(format), _glTexture(glIntFormat, glFormat, glType), + _textureData(), _userPixelData() { +} + +Texture::~Texture() { + _textureData.free(); +} + +void Texture::destroy() { + _glTexture.destroy(); +} + +void Texture::recreate() { + _glTexture.create(); + + // In case image date exists assure it will be completely refreshed next + // time. + if (_textureData.getPixels()) { + flagDirty(); + } +} + +void Texture::enableLinearFiltering(bool enable) { + _glTexture.enableLinearFiltering(enable); +} + +void Texture::allocate(uint width, uint height) { + // Assure the texture can contain our user data. + _glTexture.setSize(width, height); + + // In case the needed texture dimension changed we will reinitialize the + // texture data buffer. + if (_glTexture.getWidth() != _textureData.w || _glTexture.getHeight() != _textureData.h) { + // Create a buffer for the texture data. + _textureData.create(_glTexture.getWidth(), _glTexture.getHeight(), _format); } - // First update any potentional changes. - updateTexture(); - - // Set the texture. - GLCALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); - - // Calculate the texture rect that will be drawn. - const GLfloat texWidth = (GLfloat)_userPixelData.w / _textureData.w; - const GLfloat texHeight = (GLfloat)_userPixelData.h / _textureData.h; - const GLfloat texcoords[4*2] = { - 0, 0, - texWidth, 0, - 0, texHeight, - texWidth, texHeight - }; - GLCALL(glTexCoordPointer(2, GL_FLOAT, 0, texcoords)); - - // Calculate the screen rect where the texture will be drawn. - const GLfloat vertices[4*2] = { - x, y, - x + w, y, - x, y + h, - x + w, y + h - }; - GLCALL(glVertexPointer(2, GL_FLOAT, 0, vertices)); - - // Draw the texture to the screen buffer. - GLCALL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); -} - -void Texture::updateTexture() { + // Create a sub-buffer for raw access. + _userPixelData = _textureData.getSubArea(Common::Rect(width, height)); + + // The whole texture is dirty after we changed the size. This fixes + // multiple texture size changes without any actual update in between. + // Without this we might try to write a too big texture into the GL + // texture. + flagDirty(); +} + +void Texture::updateGLTexture() { if (!isDirty()) { return; } @@ -211,7 +283,7 @@ void Texture::updateTexture() { // In case we use linear filtering we might need to duplicate the last // pixel row/column to avoid glitches with filtering. - if (_glFilter == GL_LINEAR) { + if (_glTexture.isLinearFilteringEnabled()) { if (dirtyArea.right == _userPixelData.w && _userPixelData.w != _textureData.w) { uint height = dirtyArea.height(); @@ -238,42 +310,12 @@ void Texture::updateTexture() { } } - // Set the texture. - GLCALL(glBindTexture(GL_TEXTURE_2D, _glTexture)); - - // Update the actual texture. - // Although we keep track of the dirty part of the texture buffer we - // cannot take advantage of the left/right boundries here because it is - // not possible to specify a pitch to glTexSubImage2D. To be precise, with - // plain OpenGL we could set GL_UNPACK_ROW_LENGTH to achieve this. However, - // OpenGL ES 1.0 does not support GL_UNPACK_ROW_LENGTH. Thus, we are left - // with the following options: - // - // 1) (As we do right now) Simply always update the whole texture lines of - // rect changed. This is simplest to implement. In case performance is - // really an issue we can think of switching to another method. - // - // 2) Copy the dirty rect to a temporary buffer and upload that by using - // glTexSubImage2D. This is what the Android backend does. It is more - // complicated though. - // - // 3) Use glTexSubImage2D per line changed. This is what the old OpenGL - // graphics manager did but it is much slower! Thus, we do not use it. - GLCALL(glTexSubImage2D(GL_TEXTURE_2D, 0, 0, dirtyArea.top, _textureData.w, dirtyArea.height(), - _glFormat, _glType, _textureData.getBasePtr(0, dirtyArea.top))); + _glTexture.updateArea(dirtyArea, _textureData); // We should have handled everything, thus not dirty anymore. clearDirty(); } -Common::Rect Texture::getDirtyArea() const { - if (_allDirty) { - return Common::Rect(_userPixelData.w, _userPixelData.h); - } else { - return _dirtyArea; - } -} - TextureCLUT8::TextureCLUT8(GLenum glIntFormat, GLenum glFormat, GLenum glType, const Graphics::PixelFormat &format) : Texture(glIntFormat, glFormat, glType, format), _clut8Data(), _palette(new byte[256 * format.bytesPerPixel]) { memset(_palette, 0, sizeof(byte) * format.bytesPerPixel); @@ -301,6 +343,25 @@ Graphics::PixelFormat TextureCLUT8::getFormat() const { return Graphics::PixelFormat::createFormatCLUT8(); } +void TextureCLUT8::setColorKey(uint colorKey) { + // We remove all alpha bits from the palette entry of the color key. + // This makes sure its properly handled as color key. + const uint32 aMask = (0xFF >> _format.aLoss) << _format.aShift; + + if (_format.bytesPerPixel == 2) { + uint16 *palette = (uint16 *)_palette + colorKey; + *palette &= ~aMask; + } else if (_format.bytesPerPixel == 4) { + uint32 *palette = (uint32 *)_palette + colorKey; + *palette &= ~aMask; + } else { + warning("TextureCLUT8::setColorKey: Unsupported pixel depth %d", _format.bytesPerPixel); + } + + // A palette changes means we need to refresh the whole surface. + flagDirty(); +} + namespace { template<typename ColorType> inline void convertPalette(ColorType *dst, const byte *src, uint colors, const Graphics::PixelFormat &format) { @@ -312,14 +373,12 @@ inline void convertPalette(ColorType *dst, const byte *src, uint colors, const G } // End of anonymous namespace void TextureCLUT8::setPalette(uint start, uint colors, const byte *palData) { - const Graphics::PixelFormat &hardwareFormat = getHardwareFormat(); - - if (hardwareFormat.bytesPerPixel == 2) { - convertPalette<uint16>((uint16 *)_palette + start, palData, colors, hardwareFormat); - } else if (hardwareFormat.bytesPerPixel == 4) { - convertPalette<uint32>((uint32 *)_palette + start, palData, colors, hardwareFormat); + if (_format.bytesPerPixel == 2) { + convertPalette<uint16>((uint16 *)_palette + start, palData, colors, _format); + } else if (_format.bytesPerPixel == 4) { + convertPalette<uint32>((uint32 *)_palette + start, palData, colors, _format); } else { - warning("TextureCLUT8::setPalette: Unsupported pixel depth: %d", hardwareFormat.bytesPerPixel); + warning("TextureCLUT8::setPalette: Unsupported pixel depth: %d", _format.bytesPerPixel); } // A palette changes means we need to refresh the whole surface. @@ -343,7 +402,7 @@ inline void doPaletteLookUp(PixelType *dst, const byte *src, uint width, uint he } } // End of anonymous namespace -void TextureCLUT8::updateTexture() { +void TextureCLUT8::updateGLTexture() { if (!isDirty()) { return; } @@ -368,7 +427,223 @@ void TextureCLUT8::updateTexture() { } // Do generic handling of updating the texture. - Texture::updateTexture(); + Texture::updateGLTexture(); +} + +#if !USE_FORCED_GL +TextureRGB555::TextureRGB555() + : Texture(GL_RGB, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)), + _rgb555Data() { +} + +TextureRGB555::~TextureRGB555() { + _rgb555Data.free(); +} + +void TextureRGB555::allocate(uint width, uint height) { + Texture::allocate(width, height); + + // We only need to reinitialize our RGB555 surface when the output size + // changed. + if (width == _rgb555Data.w && height == _rgb555Data.h) { + return; + } + + _rgb555Data.create(width, height, Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)); +} + +Graphics::PixelFormat TextureRGB555::getFormat() const { + return Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0); +} + +void TextureRGB555::updateTexture() { + if (!isDirty()) { + return; + } + + // Convert color space. + Graphics::Surface *outSurf = Texture::getSurface(); + + const Common::Rect dirtyArea = getDirtyArea(); + + uint16 *dst = (uint16 *)outSurf->getBasePtr(dirtyArea.left, dirtyArea.top); + const uint dstAdd = outSurf->pitch - 2 * dirtyArea.width(); + + const uint16 *src = (const uint16 *)_rgb555Data.getBasePtr(dirtyArea.left, dirtyArea.top); + const uint srcAdd = _rgb555Data.pitch - 2 * dirtyArea.width(); + + for (int height = dirtyArea.height(); height > 0; --height) { + for (int width = dirtyArea.width(); width > 0; --width) { + const uint16 color = *src++; + + *dst++ = ((color & 0x7C00) << 1) // R + | (((color & 0x03E0) << 1) | ((color & 0x0200) >> 4)) // G + | (color & 0x001F); // B + } + + src = (const uint16 *)((const byte *)src + srcAdd); + dst = (uint16 *)((byte *)dst + dstAdd); + } + + // Do generic handling of updating the texture. + Texture::updateGLTexture(); +} +#endif // !USE_FORCED_GL + +#if !USE_FORCED_GLES + +// _clut8Texture needs 8 bits internal precision, otherwise graphics glitches +// can occur. GL_ALPHA does not have any internal precision requirements. +// However, in practice (according to fuzzie) it's 8bit. If we run into +// problems, we need to switch to GL_R8 and GL_RED, but that is only supported +// for ARB_texture_rg and GLES3+ (EXT_rexture_rg does not support GL_R8). +TextureCLUT8GPU::TextureCLUT8GPU() + : _clut8Texture(GL_ALPHA, GL_ALPHA, GL_UNSIGNED_BYTE), + _paletteTexture(GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE), + _target(new TextureTarget()), _clut8Pipeline(new CLUT8LookUpPipeline()), + _clut8Vertices(), _clut8Data(), _userPixelData(), _palette(), + _paletteDirty(false) { + // Allocate space for 256 colors. + _paletteTexture.setSize(256, 1); + + // Setup pipeline. + _clut8Pipeline->setFramebuffer(_target); + _clut8Pipeline->setPaletteTexture(&_paletteTexture); + _clut8Pipeline->setColor(1.0f, 1.0f, 1.0f, 1.0f); +} + +TextureCLUT8GPU::~TextureCLUT8GPU() { + delete _clut8Pipeline; + delete _target; + _clut8Data.free(); +} + +void TextureCLUT8GPU::destroy() { + _clut8Texture.destroy(); + _paletteTexture.destroy(); + _target->destroy(); +} + +void TextureCLUT8GPU::recreate() { + _clut8Texture.create(); + _paletteTexture.create(); + _target->create(); + + // In case image date exists assure it will be completely refreshed next + // time. + if (_clut8Data.getPixels()) { + flagDirty(); + _paletteDirty = true; + } +} + +void TextureCLUT8GPU::enableLinearFiltering(bool enable) { + _target->getTexture()->enableLinearFiltering(enable); +} + +void TextureCLUT8GPU::allocate(uint width, uint height) { + // Assure the texture can contain our user data. + _clut8Texture.setSize(width, height); + _target->setSize(width, height); + + // In case the needed texture dimension changed we will reinitialize the + // texture data buffer. + if (_clut8Texture.getWidth() != _clut8Data.w || _clut8Texture.getHeight() != _clut8Data.h) { + // Create a buffer for the texture data. + _clut8Data.create(_clut8Texture.getWidth(), _clut8Texture.getHeight(), Graphics::PixelFormat::createFormatCLUT8()); + } + + // Create a sub-buffer for raw access. + _userPixelData = _clut8Data.getSubArea(Common::Rect(width, height)); + + // Setup structures for internal rendering to _glTexture. + _clut8Vertices[0] = 0; + _clut8Vertices[1] = 0; + + _clut8Vertices[2] = width; + _clut8Vertices[3] = 0; + + _clut8Vertices[4] = 0; + _clut8Vertices[5] = height; + + _clut8Vertices[6] = width; + _clut8Vertices[7] = height; + + // The whole texture is dirty after we changed the size. This fixes + // multiple texture size changes without any actual update in between. + // Without this we might try to write a too big texture into the GL + // texture. + flagDirty(); +} + +Graphics::PixelFormat TextureCLUT8GPU::getFormat() const { + return Graphics::PixelFormat::createFormatCLUT8(); +} + +void TextureCLUT8GPU::setColorKey(uint colorKey) { + _palette[colorKey * 4 + 3] = 0x00; + + _paletteDirty = true; +} + +void TextureCLUT8GPU::setPalette(uint start, uint colors, const byte *palData) { + byte *dst = _palette + start * 4; + + while (colors-- > 0) { + memcpy(dst, palData, 3); + dst[3] = 0xFF; + + dst += 4; + palData += 3; + } + + _paletteDirty = true; +} + +const GLTexture &TextureCLUT8GPU::getGLTexture() const { + return *_target->getTexture(); +} + +void TextureCLUT8GPU::updateGLTexture() { + const bool needLookUp = Surface::isDirty() || _paletteDirty; + + // Update CLUT8 texture if necessary. + if (Surface::isDirty()) { + _clut8Texture.updateArea(getDirtyArea(), _clut8Data); + clearDirty(); + } + + // Update palette if necessary. + if (_paletteDirty) { + Graphics::Surface palSurface; + palSurface.init(256, 1, 256, _palette, +#ifdef SCUMM_LITTLE_ENDIAN + Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24) // ABGR8888 +#else + Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0) // RGBA8888 +#endif + ); + + _paletteTexture.updateArea(Common::Rect(256, 1), palSurface); + _paletteDirty = false; + } + + // In case any data changed, do color look up and store result in _target. + if (needLookUp) { + lookUpColors(); + } +} + +void TextureCLUT8GPU::lookUpColors() { + // Setup pipeline to do color look up. + Pipeline *oldPipeline = g_context.setPipeline(_clut8Pipeline); + + // Do color look up. + g_context.getActivePipeline()->drawTexture(_clut8Texture, _clut8Vertices); + + // Restore old state. + g_context.setPipeline(oldPipeline); } +#endif // !USE_FORCED_GLES } // End of namespace OpenGL diff --git a/backends/graphics/opengl/texture.h b/backends/graphics/opengl/texture.h index ad70833544..3be09cb9f9 100644 --- a/backends/graphics/opengl/texture.h +++ b/backends/graphics/opengl/texture.h @@ -32,115 +32,267 @@ namespace OpenGL { +class Shader; + /** - * An OpenGL texture wrapper. It automatically takes care of all OpenGL - * texture handling issues and also provides access to the texture data. + * A simple GL texture object abstraction. + * + * This is used for low-level GL texture handling. */ -class Texture { +class GLTexture { public: /** - * Create a new texture with the specific internal format. + * Constrcut a new GL texture object. * * @param glIntFormat The internal format to use. * @param glFormat The input format. * @param glType The input type. - * @param format The format used for the texture input. */ - Texture(GLenum glIntFormat, GLenum glFormat, GLenum glType, const Graphics::PixelFormat &format); - virtual ~Texture(); + GLTexture(GLenum glIntFormat, GLenum glFormat, GLenum glType); + ~GLTexture(); + + /** + * Enable or disable linear texture filtering. + * + * @param enable true to enable and false to disable. + */ + void enableLinearFiltering(bool enable); + + /** + * Test whether linear filtering is enabled. + */ + bool isLinearFilteringEnabled() const { return (_glFilter == GL_LINEAR); } /** * Destroy the OpenGL texture name. */ - void releaseInternalTexture(); + void destroy(); + + /** + * Create the OpenGL texture name. + */ + void create(); + + /** + * Bind the texture to the active texture unit. + */ + void bind() const; + + /** + * Sets the size of the texture in pixels. + * + * The internal OpenGL texture might have a different size. To query the + * actual size use getWidth()/getHeight(). + * + * @param width The desired logical width. + * @param height The desired logical height. + */ + void setSize(uint width, uint height); + + /** + * Copy image data to the texture. + * + * @param area The area to update. + * @param src Surface for the whole texture containing the pixel data + * to upload. Only the area described by area will be + * uploaded. + */ + void updateArea(const Common::Rect &area, const Graphics::Surface &src); /** - * Create the OpenGL texture name and flag the whole texture as dirty. + * Query the GL texture's width. */ - void recreateInternalTexture(); + uint getWidth() const { return _width; } + + /** + * Query the GL texture's height. + */ + uint getHeight() const { return _height; } + + /** + * Query the logical texture's width. + */ + uint getLogicalWidth() const { return _logicalWidth; } + + /** + * Query the logical texture's height. + */ + uint getLogicalHeight() const { return _logicalHeight; } + + /** + * Obtain texture coordinates for rectangular drawing. + */ + const GLfloat *getTexCoords() const { return _texCoords; } + + /** + * Obtain texture name. + * + * Beware that the texture name changes whenever create is used. + * destroy will invalidate the texture name. + */ + GLuint getGLTexture() const { return _glTexture; } +private: + const GLenum _glIntFormat; + const GLenum _glFormat; + const GLenum _glType; + + uint _width, _height; + uint _logicalWidth, _logicalHeight; + GLfloat _texCoords[4*2]; + + GLint _glFilter; + + GLuint _glTexture; +}; + +/** + * Interface for OpenGL implementations of a 2D surface. + */ +class Surface { +public: + Surface(); + virtual ~Surface() {} + + /** + * Destroy OpenGL description of surface. + */ + virtual void destroy() = 0; + + /** + * Recreate OpenGL description of surface. + */ + virtual void recreate() = 0; /** * Enable or disable linear texture filtering. * * @param enable true to enable and false to disable. */ - void enableLinearFiltering(bool enable); + virtual void enableLinearFiltering(bool enable) = 0; /** - * Allocate texture space for the desired dimensions. This wraps any - * handling of requirements for POT textures. + * Allocate storage for surface. * * @param width The desired logical width. * @param height The desired logical height. */ - virtual void allocate(uint width, uint height); + virtual void allocate(uint width, uint height) = 0; + /** + * Copy image data to the surface. + * + * The format of the input data needs to match the format returned by + * getFormat. + * + * @param x X coordinate of upper left corner to copy data to. + * @param y Y coordinate of upper left corner to copy data to. + * @param w Width of the image data to copy. + * @param h Height of the image data to copy. + * @param src Pointer to image data. + * @param srcPitch The number of bytes in a row of the image data. + */ void copyRectToTexture(uint x, uint y, uint w, uint h, const void *src, uint srcPitch); + /** + * Fill the surface with a fixed color. + * + * @param color Color value in format returned by getFormat. + */ void fill(uint32 color); - void draw(GLfloat x, GLfloat y, GLfloat w, GLfloat h); - void flagDirty() { _allDirty = true; } - bool isDirty() const { return _allDirty || !_dirtyArea.isEmpty(); } - - uint getWidth() const { return _userPixelData.w; } - uint getHeight() const { return _userPixelData.h; } + virtual bool isDirty() const { return _allDirty || !_dirtyArea.isEmpty(); } - /** - * @return The hardware format of the texture data. - */ - const Graphics::PixelFormat &getHardwareFormat() const { return _format; } + virtual uint getWidth() const = 0; + virtual uint getHeight() const = 0; /** * @return The logical format of the texture data. */ - virtual Graphics::PixelFormat getFormat() const { return _format; } + virtual Graphics::PixelFormat getFormat() const = 0; - virtual Graphics::Surface *getSurface() { return &_userPixelData; } - virtual const Graphics::Surface *getSurface() const { return &_userPixelData; } + virtual Graphics::Surface *getSurface() = 0; + virtual const Graphics::Surface *getSurface() const = 0; /** - * @return Whether the texture data is using a palette. + * @return Whether the surface is having a palette. */ virtual bool hasPalette() const { return false; } + /** + * Set color key for paletted textures. + * + * This needs to be called after any palette update affecting the color + * key. Calling this multiple times will result in multiple color indices + * to be treated as color keys. + */ + virtual void setColorKey(uint colorKey) {} virtual void setPalette(uint start, uint colors, const byte *palData) {} - virtual void *getPalette() { return 0; } - virtual const void *getPalette() const { return 0; } - /** - * Query texture related OpenGL information from the context. This only - * queries the maximum texture size for now. + * Update underlying OpenGL texture to reflect current state. */ - static void queryTextureInformation(); + virtual void updateGLTexture() = 0; /** - * @return Return the maximum texture dimensions supported. + * Obtain underlying OpenGL texture. */ - static GLint getMaximumTextureSize() { return _maxTextureSize; } + virtual const GLTexture &getGLTexture() const = 0; protected: - virtual void updateTexture(); + void clearDirty() { _allDirty = false; _dirtyArea = Common::Rect(); } Common::Rect getDirtyArea() const; private: - const GLenum _glIntFormat; - const GLenum _glFormat; - const GLenum _glType; + bool _allDirty; + Common::Rect _dirtyArea; +}; + +/** + * An OpenGL texture wrapper. It automatically takes care of all OpenGL + * texture handling issues and also provides access to the texture data. + */ +class Texture : public Surface { +public: + /** + * Create a new texture with the specific internal format. + * + * @param glIntFormat The internal format to use. + * @param glFormat The input format. + * @param glType The input type. + * @param format The format used for the texture input. + */ + Texture(GLenum glIntFormat, GLenum glFormat, GLenum glType, const Graphics::PixelFormat &format); + virtual ~Texture(); + + virtual void destroy(); + + virtual void recreate(); + + virtual void enableLinearFiltering(bool enable); + + virtual void allocate(uint width, uint height); + + virtual uint getWidth() const { return _userPixelData.w; } + virtual uint getHeight() const { return _userPixelData.h; } + + /** + * @return The logical format of the texture data. + */ + virtual Graphics::PixelFormat getFormat() const { return _format; } + + virtual Graphics::Surface *getSurface() { return &_userPixelData; } + virtual const Graphics::Surface *getSurface() const { return &_userPixelData; } + + virtual void updateGLTexture(); + virtual const GLTexture &getGLTexture() const { return _glTexture; } +protected: const Graphics::PixelFormat _format; - GLint _glFilter; - GLuint _glTexture; +private: + GLTexture _glTexture; Graphics::Surface _textureData; Graphics::Surface _userPixelData; - - bool _allDirty; - Common::Rect _dirtyArea; - void clearDirty() { _allDirty = false; _dirtyArea = Common::Rect(); } - - static GLint _maxTextureSize; }; class TextureCLUT8 : public Texture { @@ -154,21 +306,95 @@ public: virtual bool hasPalette() const { return true; } + virtual void setColorKey(uint colorKey); virtual void setPalette(uint start, uint colors, const byte *palData); - virtual void *getPalette() { return _palette; } - virtual const void *getPalette() const { return _palette; } - virtual Graphics::Surface *getSurface() { return &_clut8Data; } virtual const Graphics::Surface *getSurface() const { return &_clut8Data; } -protected: + virtual void updateGLTexture(); +private: + Graphics::Surface _clut8Data; + byte *_palette; +}; + +#if !USE_FORCED_GL +class TextureRGB555 : public Texture { +public: + TextureRGB555(); + virtual ~TextureRGB555(); + + virtual void allocate(uint width, uint height); + + virtual Graphics::PixelFormat getFormat() const; + + virtual Graphics::Surface *getSurface() { return &_rgb555Data; } + virtual const Graphics::Surface *getSurface() const { return &_rgb555Data; } + virtual void updateTexture(); +private: + Graphics::Surface _rgb555Data; +}; +#endif // !USE_FORCED_GL + +#if !USE_FORCED_GLES +class TextureTarget; +class CLUT8LookUpPipeline; + +class TextureCLUT8GPU : public Surface { +public: + TextureCLUT8GPU(); + virtual ~TextureCLUT8GPU(); + + virtual void destroy(); + + virtual void recreate(); + virtual void enableLinearFiltering(bool enable); + + virtual void allocate(uint width, uint height); + + virtual bool isDirty() const { return _paletteDirty || Surface::isDirty(); } + + virtual uint getWidth() const { return _userPixelData.w; } + virtual uint getHeight() const { return _userPixelData.h; } + + virtual Graphics::PixelFormat getFormat() const; + + virtual bool hasPalette() const { return true; } + + virtual void setColorKey(uint colorKey); + virtual void setPalette(uint start, uint colors, const byte *palData); + + virtual Graphics::Surface *getSurface() { return &_userPixelData; } + virtual const Graphics::Surface *getSurface() const { return &_userPixelData; } + + virtual void updateGLTexture(); + virtual const GLTexture &getGLTexture() const; + + static bool isSupportedByContext() { + return g_context.shadersSupported + && g_context.multitextureSupported + && g_context.framebufferObjectSupported; + } private: + void lookUpColors(); + + GLTexture _clut8Texture; + GLTexture _paletteTexture; + + TextureTarget *_target; + CLUT8LookUpPipeline *_clut8Pipeline; + + GLfloat _clut8Vertices[4*2]; + Graphics::Surface _clut8Data; - byte *_palette; + Graphics::Surface _userPixelData; + + byte _palette[4 * 256]; + bool _paletteDirty; }; +#endif // !USE_FORCED_GLES } // End of namespace OpenGL diff --git a/backends/graphics/openglsdl/openglsdl-graphics.cpp b/backends/graphics/openglsdl/openglsdl-graphics.cpp index 0d140ee4d7..860173a3c6 100644 --- a/backends/graphics/openglsdl/openglsdl-graphics.cpp +++ b/backends/graphics/openglsdl/openglsdl-graphics.cpp @@ -45,6 +45,100 @@ OpenGLSdlGraphicsManager::OpenGLSdlGraphicsManager(uint desktopWidth, uint deskt SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + // Setup proper SDL OpenGL context creation. +#if SDL_VERSION_ATLEAST(2, 0, 0) + OpenGL::ContextType glContextType; + + // Context version 1.4 is choosen arbitrarily based on what most shader + // extensions were written against. +#define DEFAULT_GL_MAJOR 1 +#define DEFAULT_GL_MINOR 4 + +#define DEFAULT_GLES_MAJOR 1 +#define DEFAULT_GLES_MINOR 1 + +#define DEFAULT_GLES2_MAJOR 2 +#define DEFAULT_GLES2_MINOR 0 + +#if USE_FORCED_GL + glContextType = OpenGL::kContextGL; + _glContextProfileMask = 0; + _glContextMajor = DEFAULT_GL_MAJOR; + _glContextMinor = DEFAULT_GL_MINOR; +#elif USE_FORCED_GLES + glContextType = OpenGL::kContextGLES; + _glContextProfileMask = SDL_GL_CONTEXT_PROFILE_ES; + _glContextMajor = DEFAULT_GLES_MAJOR; + _glContextMinor = DEFAULT_GLES_MINOR; +#elif USE_FORCED_GLES2 + glContextType = OpenGL::kContextGLES2; + _glContextProfileMask = SDL_GL_CONTEXT_PROFILE_ES; + _glContextMajor = DEFAULT_GLES2_MAJOR; + _glContextMinor = DEFAULT_GLES2_MINOR; +#else + bool noDefaults = false; + + // Obtain the default GL(ES) context SDL2 tries to setup. + // + // Please note this might not actually be SDL2's defaults when multiple + // instances of this object have been created. But that is no issue + // because then we already set up what we want to use. + // + // In case no defaults are given we prefer OpenGL over OpenGL ES. + if (SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &_glContextProfileMask) != 0) { + _glContextProfileMask = 0; + noDefaults = true; + } + + if (SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &_glContextMajor) != 0) { + noDefaults = true; + } + + if (SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &_glContextMinor) != 0) { + noDefaults = true; + } + + if (noDefaults) { + if (_glContextProfileMask == SDL_GL_CONTEXT_PROFILE_ES) { + _glContextMajor = DEFAULT_GLES_MAJOR; + _glContextMinor = DEFAULT_GLES_MINOR; + } else { + _glContextProfileMask = 0; + _glContextMajor = DEFAULT_GL_MAJOR; + _glContextMinor = DEFAULT_GL_MINOR; + } + } + + if (_glContextProfileMask == SDL_GL_CONTEXT_PROFILE_ES) { + if (_glContextMajor >= 2) { + glContextType = OpenGL::kContextGLES2; + } else { + glContextType = OpenGL::kContextGLES; + } + } else if (_glContextProfileMask == SDL_GL_CONTEXT_PROFILE_CORE) { + glContextType = OpenGL::kContextGL; + + // Core profile does not allow legacy functionality, which we use. + // Thus we request a standard OpenGL context. + _glContextProfileMask = 0; + _glContextMajor = DEFAULT_GL_MAJOR; + _glContextMinor = DEFAULT_GL_MINOR; + } else { + glContextType = OpenGL::kContextGL; + } +#undef DEFAULT_GL_MAJOR +#undef DEFAULT_GL_MINOR +#undef DEFAULT_GLES_MAJOR +#undef DEFAULT_GLES_MINOR +#undef DEFAULT_GLES2_MAJOR +#undef DEFAULT_GLES2_MINOR +#endif + + setContextType(glContextType); +#else + setContextType(OpenGL::kContextGL); +#endif + // Retrieve a list of working fullscreen modes #if SDL_VERSION_ATLEAST(2, 0, 0) const int numModes = SDL_GetNumDisplayModes(0); @@ -100,6 +194,10 @@ OpenGLSdlGraphicsManager::OpenGLSdlGraphicsManager(uint desktopWidth, uint deskt } OpenGLSdlGraphicsManager::~OpenGLSdlGraphicsManager() { +#if SDL_VERSION_ATLEAST(2, 0, 0) + notifyContextDestroy(); + SDL_GL_DeleteContext(_glContext); +#endif } void OpenGLSdlGraphicsManager::activateManager() { @@ -210,20 +308,26 @@ Common::List<Graphics::PixelFormat> OpenGLSdlGraphicsManager::getSupportedFormat // RGBA4444 formats.push_back(Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0)); -#ifndef USE_GLES +#if !USE_FORCED_GLES && !USE_FORCED_GLES2 +#if !USE_FORCED_GL + if (!isGLESContext()) { +#endif #ifdef SCUMM_LITTLE_ENDIAN - // RGBA8888 - formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); + // RGBA8888 + formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); #else - // ABGR8888 - formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)); + // ABGR8888 + formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24)); +#endif +#if !USE_FORCED_GL + } +#endif #endif - // ARGB8888, this should not be here, but Sword25 requires it. :-/ - formats.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 16, 8, 0, 24)); // RGB555, this is used by SCUMM HE 16 bit games. + // This is not natively supported by OpenGL ES implementations, we convert + // the pixel format internally. formats.push_back(Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)); -#endif formats.push_back(Graphics::PixelFormat::createFormatCLUT8()); @@ -244,8 +348,19 @@ void OpenGLSdlGraphicsManager::notifyVideoExpose() { void OpenGLSdlGraphicsManager::notifyResize(const uint width, const uint height) { #if SDL_VERSION_ATLEAST(2, 0, 0) + // We sometime get outdated resize events from SDL2. So check that the size we get + // is the actual current window size. If not ignore the resize. + // The issue for example occurs when switching from fullscreen to windowed mode or + // when switching between different fullscreen resolutions because SDL_DestroyWindow + // for a fullscreen window that doesn't have the SDL_WINDOW_FULLSCREEN_DESKTOP flag + // causes a SDL_WINDOWEVENT_RESIZED event with the old resolution to be sent, and this + // event is processed after recreating the window at the new resolution. + int currentWidth, currentHeight; + getWindowDimensions(¤tWidth, ¤tHeight); + if (width != (uint)currentWidth || height != (uint)currentHeight) + return; setActualScreenSize(width, height); - _eventSource->resetKeyboadEmulation(width - 1, height - 1); + _eventSource->resetKeyboardEmulation(width - 1, height - 1); #else if (!_ignoreResizeEvents && _hwScreen && !(_hwScreen->flags & SDL_FULLSCREEN)) { // We save that we handled a resize event here. We need to know this @@ -305,6 +420,10 @@ void OpenGLSdlGraphicsManager::refreshScreen() { #endif } +void *OpenGLSdlGraphicsManager::getProcAddress(const char *name) const { + return SDL_GL_GetProcAddress(name); +} + bool OpenGLSdlGraphicsManager::setupMode(uint width, uint height) { // In case we request a fullscreen mode we will use the mode the user // has chosen last time or the biggest mode available. @@ -378,6 +497,11 @@ bool OpenGLSdlGraphicsManager::setupMode(uint width, uint height) { flags |= SDL_WINDOW_RESIZABLE; } + // Request a OpenGL (ES) context we can use. + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, _glContextProfileMask); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, _glContextMajor); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, _glContextMinor); + if (!_window->createWindow(width, height, flags)) { // We treat fullscreen requests as a "hint" for now. This means in // case it is not available we simply ignore it. @@ -390,13 +514,6 @@ bool OpenGLSdlGraphicsManager::setupMode(uint width, uint height) { } } -#ifdef USE_GLES - // SDL2 will create a GLES2 context by default, so this is needed for GLES1-profile - // functions to work. - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); -#endif _glContext = SDL_GL_CreateContext(_window->getSDLWindow()); if (!_glContext) { return false; @@ -406,7 +523,7 @@ bool OpenGLSdlGraphicsManager::setupMode(uint width, uint height) { int actualWidth, actualHeight; getWindowDimensions(&actualWidth, &actualHeight); setActualScreenSize(actualWidth, actualHeight); - _eventSource->resetKeyboadEmulation(actualWidth - 1, actualHeight - 1); + _eventSource->resetKeyboardEmulation(actualWidth - 1, actualHeight - 1); return true; #else // WORKAROUND: Working around infamous SDL bugs when switching @@ -452,7 +569,7 @@ bool OpenGLSdlGraphicsManager::setupMode(uint width, uint height) { if (_hwScreen) { notifyContextCreate(rgba8888, rgba8888); setActualScreenSize(_hwScreen->w, _hwScreen->h); - _eventSource->resetKeyboadEmulation(_hwScreen->w - 1, _hwScreen->h - 1); + _eventSource->resetKeyboardEmulation(_hwScreen->w - 1, _hwScreen->h - 1); } // Ignore resize events (from SDL) for a few frames, if this isn't @@ -495,9 +612,9 @@ bool OpenGLSdlGraphicsManager::notifyEvent(const Common::Event &event) { #ifdef USE_OSD if (getFeatureState(OSystem::kFeatureFullscreenMode)) { - displayMessageOnOSD("Fullscreen mode"); + displayMessageOnOSD(_("Fullscreen mode")); } else { - displayMessageOnOSD("Windowed mode"); + displayMessageOnOSD(_("Windowed mode")); } #endif return true; @@ -590,7 +707,7 @@ bool OpenGLSdlGraphicsManager::notifyEvent(const Common::Event &event) { #ifdef USE_OSD int windowWidth = 0, windowHeight = 0; getWindowDimensions(&windowWidth, &windowHeight); - const Common::String osdMsg = Common::String::format("Resolution: %dx%d", windowWidth, windowHeight); + const Common::String osdMsg = Common::String::format(_("Resolution: %dx%d"), windowWidth, windowHeight); displayMessageOnOSD(osdMsg.c_str()); #endif @@ -610,45 +727,21 @@ bool OpenGLSdlGraphicsManager::notifyEvent(const Common::Event &event) { assert(!_ignoreLoadVideoMode); #ifdef USE_OSD - Common::String osdMsg = "Aspect ratio correction: "; - osdMsg += getFeatureState(OSystem::kFeatureAspectRatioCorrection) ? "enabled" : "disabled"; - displayMessageOnOSD(osdMsg.c_str()); + if (getFeatureState(OSystem::kFeatureAspectRatioCorrection)) + displayMessageOnOSD(_("Enabled aspect ratio correction")); + else + displayMessageOnOSD(_("Disabled aspect ratio correction")); #endif return true; } else if (event.kbd.keycode == Common::KEYCODE_f) { - // Ctrl+Alt+f toggles the graphics modes. - - // We are crazy we will allow the OpenGL base class to - // introduce new graphics modes like shaders for special - // filtering. If some other OpenGL subclass needs this, - // we can think of refactoring this. - int mode = getGraphicsMode(); - const OSystem::GraphicsMode *supportedModes = getSupportedGraphicsModes(); - const OSystem::GraphicsMode *modeDesc = nullptr; - - // Search the current mode. - for (; supportedModes->name; ++supportedModes) { - if (supportedModes->id == mode) { - modeDesc = supportedModes; - break; - } - } - assert(modeDesc); - - // Try to use the next mode in the list. - ++modeDesc; - if (!modeDesc->name) { - modeDesc = getSupportedGraphicsModes(); - } - - // Never ever try to resize the window when we simply want to - // switch the graphics mode. This assures that the window size - // does not change. + // Never ever try to resize the window when we simply want to enable or disable filtering. + // This assures that the window size does not change. _ignoreLoadVideoMode = true; + // Ctrl+Alt+f toggles filtering on/off beginGFXTransaction(); - setGraphicsMode(modeDesc->id); + setFeatureState(OSystem::kFeatureFilteringMode, !getFeatureState(OSystem::kFeatureFilteringMode)); endGFXTransaction(); // Make sure we do not ignore the next resize. This @@ -656,8 +749,11 @@ bool OpenGLSdlGraphicsManager::notifyEvent(const Common::Event &event) { assert(!_ignoreLoadVideoMode); #ifdef USE_OSD - const Common::String osdMsg = Common::String::format("Graphics mode: %s", _(modeDesc->description)); - displayMessageOnOSD(osdMsg.c_str()); + if (getFeatureState(OSystem::kFeatureFilteringMode)) { + displayMessageOnOSD(_("Filtering enabled")); + } else { + displayMessageOnOSD(_("Filtering disabled")); + } #endif return true; diff --git a/backends/graphics/openglsdl/openglsdl-graphics.h b/backends/graphics/openglsdl/openglsdl-graphics.h index 1552593575..51edcb4363 100644 --- a/backends/graphics/openglsdl/openglsdl-graphics.h +++ b/backends/graphics/openglsdl/openglsdl-graphics.h @@ -67,10 +67,13 @@ protected: virtual bool loadVideoMode(uint requestedWidth, uint requestedHeight, const Graphics::PixelFormat &format); virtual void refreshScreen(); + + virtual void *getProcAddress(const char *name) const; private: bool setupMode(uint width, uint height); #if SDL_VERSION_ATLEAST(2, 0, 0) + int _glContextProfileMask, _glContextMajor, _glContextMinor; SDL_GLContext _glContext; #else uint32 _lastVideoModeLoad; diff --git a/backends/graphics/surfacesdl/surfacesdl-graphics.cpp b/backends/graphics/surfacesdl/surfacesdl-graphics.cpp index 21345515bc..6cd5a47e1c 100644 --- a/backends/graphics/surfacesdl/surfacesdl-graphics.cpp +++ b/backends/graphics/surfacesdl/surfacesdl-graphics.cpp @@ -121,7 +121,8 @@ SurfaceSdlGraphicsManager::SurfaceSdlGraphicsManager(SdlEventSource *sdlEventSou : SdlGraphicsManager(sdlEventSource, window), #ifdef USE_OSD - _osdSurface(0), _osdAlpha(SDL_ALPHA_TRANSPARENT), _osdFadeStartTime(0), + _osdMessageSurface(nullptr), _osdMessageAlpha(SDL_ALPHA_TRANSPARENT), _osdMessageFadeStartTime(0), + _osdIconSurface(nullptr), #endif _hwscreen(0), #if SDL_VERSION_ATLEAST(2, 0, 0) @@ -189,6 +190,10 @@ SurfaceSdlGraphicsManager::SurfaceSdlGraphicsManager(SdlEventSource *sdlEventSou #else _videoMode.fullscreen = true; #endif + +#if SDL_VERSION_ATLEAST(2, 0, 0) + _videoMode.filtering = ConfMan.getBool("filtering"); +#endif } SurfaceSdlGraphicsManager::~SurfaceSdlGraphicsManager() { @@ -226,6 +231,9 @@ bool SurfaceSdlGraphicsManager::hasFeature(OSystem::Feature f) { return (f == OSystem::kFeatureFullscreenMode) || (f == OSystem::kFeatureAspectRatioCorrection) || +#if SDL_VERSION_ATLEAST(2, 0, 0) + (f == OSystem::kFeatureFilteringMode) || +#endif (f == OSystem::kFeatureCursorPalette) || (f == OSystem::kFeatureIconifyWindow); } @@ -238,6 +246,11 @@ void SurfaceSdlGraphicsManager::setFeatureState(OSystem::Feature f, bool enable) case OSystem::kFeatureAspectRatioCorrection: setAspectRatioCorrection(enable); break; +#if SDL_VERSION_ATLEAST(2, 0, 0) + case OSystem::kFeatureFilteringMode: + setFilteringMode(enable); + break; +#endif case OSystem::kFeatureCursorPalette: _cursorPaletteDisabled = !enable; blitCursor(); @@ -262,6 +275,10 @@ bool SurfaceSdlGraphicsManager::getFeatureState(OSystem::Feature f) { return _videoMode.fullscreen; case OSystem::kFeatureAspectRatioCorrection: return _videoMode.aspectRatioCorrection; +#if SDL_VERSION_ATLEAST(2, 0, 0) + case OSystem::kFeatureFilteringMode: + return _videoMode.filtering; +#endif case OSystem::kFeatureCursorPalette: return !_cursorPaletteDisabled; default: @@ -296,6 +313,9 @@ void SurfaceSdlGraphicsManager::beginGFXTransaction() { _transactionDetails.needUpdatescreen = false; _transactionDetails.normal1xScaler = false; +#if SDL_VERSION_ATLEAST(2, 0, 0) + _transactionDetails.needTextureUpdate = false; +#endif #ifdef USE_RGB_COLOR _transactionDetails.formatChanged = false; #endif @@ -322,6 +342,12 @@ OSystem::TransactionError SurfaceSdlGraphicsManager::endGFXTransaction() { _videoMode.mode = _oldVideoMode.mode; _videoMode.scaleFactor = _oldVideoMode.scaleFactor; +#if SDL_VERSION_ATLEAST(2, 0, 0) + } else if (_videoMode.filtering != _oldVideoMode.filtering) { + errors |= OSystem::kTransactionFilteringFailed; + + _videoMode.filtering = _oldVideoMode.filtering; +#endif #ifdef USE_RGB_COLOR } else if (_videoMode.format != _oldVideoMode.format) { errors |= OSystem::kTransactionFormatNotSupported; @@ -341,6 +367,9 @@ OSystem::TransactionError SurfaceSdlGraphicsManager::endGFXTransaction() { if (_videoMode.fullscreen == _oldVideoMode.fullscreen && _videoMode.aspectRatioCorrection == _oldVideoMode.aspectRatioCorrection && _videoMode.mode == _oldVideoMode.mode && +#if SDL_VERSION_ATLEAST(2, 0, 0) + _videoMode.filtering == _oldVideoMode.filtering && +#endif _videoMode.screenWidth == _oldVideoMode.screenWidth && _videoMode.screenHeight == _oldVideoMode.screenHeight) { @@ -394,6 +423,12 @@ OSystem::TransactionError SurfaceSdlGraphicsManager::endGFXTransaction() { if (_transactionDetails.needUpdatescreen) internUpdateScreen(); } +#if SDL_VERSION_ATLEAST(2, 0, 0) + } else if (_transactionDetails.needTextureUpdate) { + setGraphicsModeIntern(); + recreateScreenTexture(); + internUpdateScreen(); +#endif } else if (_transactionDetails.needUpdatescreen) { setGraphicsModeIntern(); internUpdateScreen(); @@ -879,25 +914,11 @@ bool SurfaceSdlGraphicsManager::loadGFXMode() { if (_tmpscreen2 == NULL) error("allocating _tmpscreen2 failed"); -#ifdef USE_OSD - _osdSurface = SDL_CreateRGBSurface(SDL_SWSURFACE | SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, - _hwscreen->w, - _hwscreen->h, - 16, - _hwscreen->format->Rmask, - _hwscreen->format->Gmask, - _hwscreen->format->Bmask, - _hwscreen->format->Amask); - if (_osdSurface == NULL) - error("allocating _osdSurface failed"); - SDL_SetColorKey(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, kOSDColorKey); -#endif - #if !SDL_VERSION_ATLEAST(2, 0, 0) // For SDL2 the output resolution might differ from the requested // resolution. We handle resetting the keyboard emulation properly inside // our SDL_SetVideoMode wrapper for SDL2. - _eventSource->resetKeyboadEmulation( + _eventSource->resetKeyboardEmulation( _videoMode.screenWidth * _videoMode.scaleFactor - 1, effectiveScreenHeight() - 1); #endif @@ -942,9 +963,14 @@ void SurfaceSdlGraphicsManager::unloadGFXMode() { } #ifdef USE_OSD - if (_osdSurface) { - SDL_FreeSurface(_osdSurface); - _osdSurface = NULL; + if (_osdMessageSurface) { + SDL_FreeSurface(_osdMessageSurface); + _osdMessageSurface = NULL; + } + + if (_osdIconSurface) { + SDL_FreeSurface(_osdIconSurface); + _osdIconSurface = NULL; } #endif DestroyScalers(); @@ -975,11 +1001,6 @@ bool SurfaceSdlGraphicsManager::hotswapGFXMode() { SDL_FreeSurface(_tmpscreen); _tmpscreen = NULL; SDL_FreeSurface(_tmpscreen2); _tmpscreen2 = NULL; -#ifdef USE_OSD - // Release the OSD surface - SDL_FreeSurface(_osdSurface); _osdSurface = NULL; -#endif - // Setup the new GFX mode if (!loadGFXMode()) { unloadGFXMode(); @@ -1057,26 +1078,6 @@ void SurfaceSdlGraphicsManager::internUpdateScreen() { _forceFull = true; } -#ifdef USE_OSD - // OSD visible (i.e. non-transparent)? - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - // Updated alpha value - const int diff = SDL_GetTicks() - _osdFadeStartTime; - if (diff > 0) { - if (diff >= kOSDFadeOutDuration) { - // Back to full transparency - _osdAlpha = SDL_ALPHA_TRANSPARENT; - } else { - // Do a linear fade out... - const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; - } - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); - _forceFull = true; - } - } -#endif - if (!_overlayVisible) { origSurf = _screen; srcSurf = _tmpscreen; @@ -1099,6 +1100,10 @@ void SurfaceSdlGraphicsManager::internUpdateScreen() { if (_mouseNeedsRedraw) undrawMouse(); +#ifdef USE_OSD + updateOSD(); +#endif + // Force a full redraw if requested if (_forceFull) { _numDirtyRects = 1; @@ -1179,9 +1184,7 @@ void SurfaceSdlGraphicsManager::internUpdateScreen() { drawMouse(); #ifdef USE_OSD - if (_osdAlpha != SDL_ALPHA_TRANSPARENT) { - SDL_BlitSurface(_osdSurface, 0, _hwscreen, 0); - } + drawOSD(); #endif #ifdef USE_SDL_DEBUG_FOCUSRECT @@ -1298,6 +1301,20 @@ void SurfaceSdlGraphicsManager::setAspectRatioCorrection(bool enable) { } } +#if SDL_VERSION_ATLEAST(2, 0, 0) +void SurfaceSdlGraphicsManager::setFilteringMode(bool enable) { + Common::StackLock lock(_graphicsMutex); + + if (_oldVideoMode.setup && _oldVideoMode.filtering == enable) + return; + + if (_transactionMode == kTransactionActive) { + _videoMode.filtering = enable; + _transactionDetails.needTextureUpdate = true; + } +} +#endif + void SurfaceSdlGraphicsManager::copyRectToScreen(const void *buf, int pitch, int x, int y, int w, int h) { assert(_transactionMode == kTransactionNone); assert(buf); @@ -2047,7 +2064,7 @@ void SurfaceSdlGraphicsManager::undrawMouse() { return; if (_mouseBackup.w != 0 && _mouseBackup.h != 0) - addDirtyRect(x, y - _currentShakePos, _mouseBackup.w, _mouseBackup.h); + addDirtyRect(x, y, _mouseBackup.w, _mouseBackup.h); } void SurfaceSdlGraphicsManager::drawMouse() { @@ -2088,9 +2105,7 @@ void SurfaceSdlGraphicsManager::drawMouse() { // We draw the pre-scaled cursor image, so now we need to adjust for // scaling, shake position and aspect ratio correction manually. - if (!_overlayVisible) { - dst.y += _currentShakePos; - } + dst.y += _currentShakePos; if (_videoMode.aspectRatioCorrection && !_overlayVisible) dst.y = real2Aspect(dst.y); @@ -2123,26 +2138,11 @@ void SurfaceSdlGraphicsManager::displayMessageOnOSD(const char *msg) { Common::StackLock lock(_graphicsMutex); // Lock the mutex until this function ends - uint i; - - // Lock the OSD surface for drawing - if (SDL_LockSurface(_osdSurface)) - error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); - - Graphics::Surface dst; - dst.init(_osdSurface->w, _osdSurface->h, _osdSurface->pitch, _osdSurface->pixels, - Graphics::PixelFormat(_osdSurface->format->BytesPerPixel, - 8 - _osdSurface->format->Rloss, 8 - _osdSurface->format->Gloss, - 8 - _osdSurface->format->Bloss, 8 - _osdSurface->format->Aloss, - _osdSurface->format->Rshift, _osdSurface->format->Gshift, - _osdSurface->format->Bshift, _osdSurface->format->Ashift)); + removeOSDMessage(); // The font we are going to use: const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont); - // Clear everything with the "transparent" color, i.e. the colorkey - SDL_FillRect(_osdSurface, 0, kOSDColorKey); - // Split the message into separate lines. Common::Array<Common::String> lines; const char *ptr; @@ -2161,44 +2161,167 @@ void SurfaceSdlGraphicsManager::displayMessageOnOSD(const char *msg) { const int lineHeight = font->getFontHeight() + 2 * lineSpacing; int width = 0; int height = lineHeight * lines.size() + 2 * vOffset; + uint i; for (i = 0; i < lines.size(); i++) { width = MAX(width, font->getStringWidth(lines[i]) + 14); } // Clip the rect - if (width > dst.w) - width = dst.w; - if (height > dst.h) - height = dst.h; + if (width > _hwscreen->w) + width = _hwscreen->w; + if (height > _hwscreen->h) + height = _hwscreen->h; + + _osdMessageSurface = SDL_CreateRGBSurface( + SDL_SWSURFACE | SDL_RLEACCEL | SDL_SRCALPHA, + width, height, 16, _hwscreen->format->Rmask, _hwscreen->format->Gmask, _hwscreen->format->Bmask, _hwscreen->format->Amask + ); + + // Lock the surface + if (SDL_LockSurface(_osdMessageSurface)) + error("displayMessageOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); // Draw a dark gray rect // TODO: Rounded corners ? Border? - SDL_Rect osdRect; - osdRect.x = (dst.w - width) / 2; - osdRect.y = (dst.h - height) / 2; - osdRect.w = width; - osdRect.h = height; - SDL_FillRect(_osdSurface, &osdRect, SDL_MapRGB(_osdSurface->format, 64, 64, 64)); + SDL_FillRect(_osdMessageSurface, nullptr, SDL_MapRGB(_osdMessageSurface->format, 64, 64, 64)); + + Graphics::Surface dst; + dst.init(_osdMessageSurface->w, _osdMessageSurface->h, _osdMessageSurface->pitch, _osdMessageSurface->pixels, + Graphics::PixelFormat(_osdMessageSurface->format->BytesPerPixel, + 8 - _osdMessageSurface->format->Rloss, 8 - _osdMessageSurface->format->Gloss, + 8 - _osdMessageSurface->format->Bloss, 8 - _osdMessageSurface->format->Aloss, + _osdMessageSurface->format->Rshift, _osdMessageSurface->format->Gshift, + _osdMessageSurface->format->Bshift, _osdMessageSurface->format->Ashift)); // Render the message, centered, and in white for (i = 0; i < lines.size(); i++) { font->drawString(&dst, lines[i], - osdRect.x, osdRect.y + i * lineHeight + vOffset + lineSpacing, osdRect.w, - SDL_MapRGB(_osdSurface->format, 255, 255, 255), - Graphics::kTextAlignCenter); + 0, 0 + i * lineHeight + vOffset + lineSpacing, width, + SDL_MapRGB(_osdMessageSurface->format, 255, 255, 255), + Graphics::kTextAlignCenter); } - // Finished drawing, so unlock the OSD surface again - SDL_UnlockSurface(_osdSurface); + // Finished drawing, so unlock the OSD message surface + SDL_UnlockSurface(_osdMessageSurface); // Init the OSD display parameters, and the fade out - _osdAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; - _osdFadeStartTime = SDL_GetTicks() + kOSDFadeOutDelay; - SDL_SetAlpha(_osdSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdAlpha); + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; + _osdMessageFadeStartTime = SDL_GetTicks() + kOSDFadeOutDelay; +} - // Ensure a full redraw takes place next time the screen is updated - _forceFull = true; +SDL_Rect SurfaceSdlGraphicsManager::getOSDMessageRect() const { + SDL_Rect rect; + rect.x = (_hwscreen->w - _osdMessageSurface->w) / 2; + rect.y = (_hwscreen->h - _osdMessageSurface->h) / 2; + rect.w = _osdMessageSurface->w; + rect.h = _osdMessageSurface->h; + return rect; +} + +void SurfaceSdlGraphicsManager::displayActivityIconOnOSD(const Graphics::Surface *icon) { + assert(_transactionMode == kTransactionNone); + + Common::StackLock lock(_graphicsMutex); // Lock the mutex until this function ends + + if (_osdIconSurface && !icon) { + // Force a redraw to clear the icon on the next update + _forceFull = true; + } + + if (_osdIconSurface) { + SDL_FreeSurface(_osdIconSurface); + _osdIconSurface = nullptr; + } + + if (icon) { + const Graphics::PixelFormat &iconFormat = icon->format; + + _osdIconSurface = SDL_CreateRGBSurface( + SDL_SWSURFACE | SDL_RLEACCEL | SDL_SRCALPHA, + icon->w, icon->h, iconFormat.bytesPerPixel * 8, + ((0xFF >> iconFormat.rLoss) << iconFormat.rShift), + ((0xFF >> iconFormat.gLoss) << iconFormat.gShift), + ((0xFF >> iconFormat.bLoss) << iconFormat.bShift), + ((0xFF >> iconFormat.aLoss) << iconFormat.aShift) + ); + + // Lock the surface + if (SDL_LockSurface(_osdIconSurface)) + error("displayActivityIconOnOSD: SDL_LockSurface failed: %s", SDL_GetError()); + + byte *dst = (byte *) _osdIconSurface->pixels; + const byte *src = (const byte *) icon->getPixels(); + for (uint y = 0; y < icon->h; y++) { + memcpy(dst, src, icon->w * iconFormat.bytesPerPixel); + src += icon->pitch; + dst += _osdIconSurface->pitch; + } + + // Finished drawing, so unlock the OSD icon surface + SDL_UnlockSurface(_osdIconSurface); + } +} + +SDL_Rect SurfaceSdlGraphicsManager::getOSDIconRect() const { + SDL_Rect dstRect; + dstRect.x = _hwscreen->w - _osdIconSurface->w - 10; + dstRect.y = 10; + dstRect.w = _osdIconSurface->w; + dstRect.h = _osdIconSurface->h; + return dstRect; } + +void SurfaceSdlGraphicsManager::removeOSDMessage() { + // Remove the previous message + if (_osdMessageSurface) { + SDL_FreeSurface(_osdMessageSurface); + } + + _osdMessageSurface = NULL; + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT; +} + +void SurfaceSdlGraphicsManager::updateOSD() { + // OSD message visible (i.e. non-transparent)? + if (_osdMessageAlpha != SDL_ALPHA_TRANSPARENT) { + // Updated alpha value + const int diff = SDL_GetTicks() - _osdMessageFadeStartTime; + if (diff > 0) { + if (diff >= kOSDFadeOutDuration) { + // Back to full transparency + _osdMessageAlpha = SDL_ALPHA_TRANSPARENT; + } else { + // Do a linear fade out... + const int startAlpha = SDL_ALPHA_TRANSPARENT + kOSDInitialAlpha * (SDL_ALPHA_OPAQUE - SDL_ALPHA_TRANSPARENT) / 100; + _osdMessageAlpha = startAlpha + diff * (SDL_ALPHA_TRANSPARENT - startAlpha) / kOSDFadeOutDuration; + } + SDL_SetAlpha(_osdMessageSurface, SDL_RLEACCEL | SDL_SRCCOLORKEY | SDL_SRCALPHA, _osdMessageAlpha); + _forceFull = true; + } + + if (_osdMessageAlpha == SDL_ALPHA_TRANSPARENT) { + removeOSDMessage(); + } + } + + if (_osdIconSurface) { + // Redraw the area below the icon for the transparent blit to give correct results. + _forceFull = true; + } +} + +void SurfaceSdlGraphicsManager::drawOSD() { + if (_osdMessageSurface) { + SDL_Rect dstRect = getOSDMessageRect(); + SDL_BlitSurface(_osdMessageSurface, 0, _hwscreen, &dstRect); + } + + if (_osdIconSurface) { + SDL_Rect dstRect = getOSDIconRect(); + SDL_BlitSurface(_osdIconSurface, 0, _hwscreen, &dstRect); + } +} + #endif bool SurfaceSdlGraphicsManager::handleScalerHotkeys(Common::KeyCode key) { @@ -2228,6 +2351,25 @@ bool SurfaceSdlGraphicsManager::handleScalerHotkeys(Common::KeyCode key) { return true; } +#if SDL_VERSION_ATLEAST(2, 0, 0) + // Ctrl-Alt-f toggles filtering + if (key == 'f') { + beginGFXTransaction(); + setFeatureState(OSystem::kFeatureFilteringMode, !_videoMode.filtering); + endGFXTransaction(); +#ifdef USE_OSD + if (getFeatureState(OSystem::kFeatureFilteringMode)) { + displayMessageOnOSD(_("Filtering enabled")); + } else { + displayMessageOnOSD(_("Filtering disabled")); + } +#endif + _forceFull = true; + internUpdateScreen(); + return true; + } +#endif + int newMode = -1; int factor = _videoMode.scaleFactor - 1; SDLKey sdlKey = (SDLKey)key; @@ -2260,26 +2402,24 @@ bool SurfaceSdlGraphicsManager::handleScalerHotkeys(Common::KeyCode key) { setGraphicsMode(newMode); endGFXTransaction(); #ifdef USE_OSD - if (_osdSurface) { - const char *newScalerName = 0; - const OSystem::GraphicsMode *g = getSupportedGraphicsModes(); - while (g->name) { - if (g->id == _videoMode.mode) { - newScalerName = g->description; - break; - } - g++; - } - if (newScalerName) { - char buffer[128]; - sprintf(buffer, "%s %s\n%d x %d -> %d x %d", - _("Active graphics filter:"), - newScalerName, - _videoMode.screenWidth, _videoMode.screenHeight, - _hwscreen->w, _hwscreen->h - ); - displayMessageOnOSD(buffer); + const char *newScalerName = 0; + const OSystem::GraphicsMode *g = getSupportedGraphicsModes(); + while (g->name) { + if (g->id == _videoMode.mode) { + newScalerName = g->description; + break; } + g++; + } + if (newScalerName) { + char buffer[128]; + sprintf(buffer, "%s %s\n%d x %d -> %d x %d", + _("Active graphics filter:"), + newScalerName, + _videoMode.screenWidth, _videoMode.screenHeight, + _hwscreen->w, _hwscreen->h + ); + displayMessageOnOSD(buffer); } #endif internUpdateScreen(); @@ -2302,6 +2442,10 @@ bool SurfaceSdlGraphicsManager::isScalerHotkey(const Common::Event &event) { if (keyValue >= ARRAYSIZE(s_gfxModeSwitchTable)) return false; } +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (event.kbd.keycode == 'f') + return true; +#endif return (isScaleKey || event.kbd.keycode == 'a'); } return false; @@ -2418,7 +2562,7 @@ void SurfaceSdlGraphicsManager::setWindowResolution(int width, int height) { _windowHeight = height; // We expect full screen resolution as inputs coming from the event system. - _eventSource->resetKeyboadEmulation(_windowWidth - 1, _windowHeight - 1); + _eventSource->resetKeyboardEmulation(_windowWidth - 1, _windowHeight - 1); // Calculate the "viewport" for the actual area we draw in. In fullscreen // we can easily get a different resolution than what we requested. In @@ -2446,6 +2590,20 @@ void SurfaceSdlGraphicsManager::setWindowResolution(int width, int height) { _forceFull = true; } +void SurfaceSdlGraphicsManager::recreateScreenTexture() { + if (!_renderer) + return; + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, _videoMode.filtering ? "linear" : "nearest"); + + SDL_Texture *oldTexture = _screenTexture; + _screenTexture = SDL_CreateTexture(_renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, _videoMode.hardwareWidth, _videoMode.hardwareHeight); + if (_screenTexture) + SDL_DestroyTexture(oldTexture); + else + _screenTexture = oldTexture; +} + SDL_Surface *SurfaceSdlGraphicsManager::SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags) { deinitializeRenderer(); @@ -2470,6 +2628,8 @@ SDL_Surface *SurfaceSdlGraphicsManager::SDL_SetVideoMode(int width, int height, SDL_GetWindowSize(_window->getSDLWindow(), &_windowWidth, &_windowHeight); setWindowResolution(_windowWidth, _windowHeight); + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, _videoMode.filtering ? "linear" : "nearest"); + _screenTexture = SDL_CreateTexture(_renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, width, height); if (!_screenTexture) { deinitializeRenderer(); diff --git a/backends/graphics/surfacesdl/surfacesdl-graphics.h b/backends/graphics/surfacesdl/surfacesdl-graphics.h index 25d6ff041c..975cbfe27b 100644 --- a/backends/graphics/surfacesdl/surfacesdl-graphics.h +++ b/backends/graphics/surfacesdl/surfacesdl-graphics.h @@ -145,6 +145,7 @@ public: #ifdef USE_OSD virtual void displayMessageOnOSD(const char *msg); + virtual void displayActivityIconOnOSD(const Graphics::Surface *icon); #endif // Override from Common::EventObserver @@ -161,18 +162,28 @@ public: protected: #ifdef USE_OSD /** Surface containing the OSD message */ - SDL_Surface *_osdSurface; - /** Transparency level of the OSD */ - uint8 _osdAlpha; + SDL_Surface *_osdMessageSurface; + /** Transparency level of the OSD message */ + uint8 _osdMessageAlpha; /** When to start the fade out */ - uint32 _osdFadeStartTime; + uint32 _osdMessageFadeStartTime; /** Enum with OSD options */ enum { kOSDFadeOutDelay = 2 * 1000, /** < Delay before the OSD is faded out (in milliseconds) */ kOSDFadeOutDuration = 500, /** < Duration of the OSD fade out (in milliseconds) */ - kOSDColorKey = 1, /** < Transparent color key */ kOSDInitialAlpha = 80 /** < Initial alpha level, in percent */ }; + /** Screen rectangle where the OSD message is drawn */ + SDL_Rect getOSDMessageRect() const; + /** Clear the currently displayed OSD message if any */ + void removeOSDMessage(); + /** Surface containing the OSD background activity icon */ + SDL_Surface *_osdIconSurface; + /** Screen rectangle where the OSD background activity icon is drawn */ + SDL_Rect getOSDIconRect() const; + + void updateOSD(); + void drawOSD(); #endif /** Hardware screen */ @@ -187,6 +198,7 @@ protected: int _windowWidth, _windowHeight; void deinitializeRenderer(); void setWindowResolution(int width, int height); + void recreateScreenTexture(); SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags); void SDL_UpdateRects(SDL_Surface *screen, int numrects, SDL_Rect *rects); @@ -226,6 +238,9 @@ protected: bool needHotswap; bool needUpdatescreen; bool normal1xScaler; +#if SDL_VERSION_ATLEAST(2, 0, 0) + bool needTextureUpdate; +#endif #ifdef USE_RGB_COLOR bool formatChanged; #endif @@ -239,6 +254,10 @@ protected: bool aspectRatioCorrection; AspectRatio desiredAspectRatio; +#if SDL_VERSION_ATLEAST(2, 0, 0) + bool filtering; +#endif + int mode; int scaleFactor; @@ -372,6 +391,9 @@ protected: virtual void setFullscreenMode(bool enable); virtual void setAspectRatioCorrection(bool enable); +#if SDL_VERSION_ATLEAST(2, 0, 0) + virtual void setFilteringMode(bool enable); +#endif virtual int effectiveScreenHeight() const; diff --git a/backends/graphics/wincesdl/wincesdl-graphics.cpp b/backends/graphics/wincesdl/wincesdl-graphics.cpp index 07f7d47262..44a1214a44 100644 --- a/backends/graphics/wincesdl/wincesdl-graphics.cpp +++ b/backends/graphics/wincesdl/wincesdl-graphics.cpp @@ -934,7 +934,7 @@ bool WINCESdlGraphicsManager::loadGFXMode() { _toolbarHigh = NULL; // keyboard cursor control, some other better place for it? - _eventSource->resetKeyboadEmulation(_videoMode.screenWidth * _scaleFactorXm / _scaleFactorXd - 1, _videoMode.screenHeight * _scaleFactorXm / _scaleFactorXd - 1); + _eventSource->resetKeyboardEmulation(_videoMode.screenWidth * _scaleFactorXm / _scaleFactorXd - 1, _videoMode.screenHeight * _scaleFactorXm / _scaleFactorXd - 1); return true; } diff --git a/backends/midi/timidity.cpp b/backends/midi/timidity.cpp index 497138850a..80dba101ed 100644 --- a/backends/midi/timidity.cpp +++ b/backends/midi/timidity.cpp @@ -52,24 +52,18 @@ #include <sys/types.h> #include <sys/socket.h> #include <sys/param.h> -#include <netdb.h> /* for gethostbyname */ +#include <netdb.h> /* for getaddrinfo */ #include <netinet/in.h> #include <arpa/inet.h> #include <stdarg.h> #include <stdlib.h> #include <errno.h> -// WORKAROUND bug #1870304: Solaris does not provide INADDR_NONE. -#ifndef INADDR_NONE -#define INADDR_NONE 0xffffffff -#endif - // BeOS BONE uses snooze (x/1000) in place of usleep(x) #ifdef __BEOS__ #define usleep(v) snooze(v/1000) #endif - #define SEQ_MIDIPUTC 5 #define TIMIDITY_LOW_DELAY @@ -84,7 +78,7 @@ /* default host & port */ #define DEFAULT_TIMIDITY_HOST "127.0.0.1" -#define DEFAULT_TIMIDITY_PORT 7777 +#define DEFAULT_TIMIDITY_PORT "7777" class MidiDriver_TIMIDITY : public MidiDriver_MPU401 { public: @@ -97,11 +91,8 @@ public: void sysEx(const byte *msg, uint16 length); private: - /* standart routine to extract ip address from a string */ - in_addr_t host_to_addr(const char* address); - /* creates a tcp connection to TiMidity server, returns filedesc (like open()) */ - int connect_to_server(const char* hostname, unsigned short tcp_port); + int connect_to_server(const char* hostname, const char* tcp_port); /* send command to the server; printf-like; returns reply string */ char *timidity_ctl_command(const char *fmt, ...) GCC_PRINTF(2, 3); @@ -150,7 +141,8 @@ MidiDriver_TIMIDITY::MidiDriver_TIMIDITY() { int MidiDriver_TIMIDITY::open() { char *res; char timidity_host[NI_MAXHOST]; - int timidity_port, data_port, i; + char timidity_port[6], data_port[6]; + int num; /* count ourselves open */ if (_isOpen) @@ -166,16 +158,16 @@ int MidiDriver_TIMIDITY::open() { /* extract control port */ if ((res = strrchr(timidity_host, ':')) != NULL) { *res++ = '\0'; - timidity_port = atoi(res); + Common::strlcpy(timidity_port, res, sizeof(timidity_port)); } else { - timidity_port = DEFAULT_TIMIDITY_PORT; + Common::strlcpy(timidity_port, DEFAULT_TIMIDITY_PORT, sizeof(timidity_port)); } /* * create control connection to the server */ if ((_control_fd = connect_to_server(timidity_host, timidity_port)) < 0) { - warning("TiMidity: can't open control connection (host=%s, port=%d)", timidity_host, timidity_port); + warning("TiMidity: can't open control connection (host=%s, port=%s)", timidity_host, timidity_port); return -1; } @@ -183,7 +175,7 @@ int MidiDriver_TIMIDITY::open() { * "220 TiMidity++ v2.13.2 ready)" */ res = timidity_ctl_command(NULL); if (atoi(res) != 220) { - warning("TiMidity: bad response from server (host=%s, port=%d): %s", timidity_host, timidity_port, res); + warning("TiMidity: bad response from server (host=%s, port=%s): %s", timidity_host, timidity_port, res); close_all(); return -1; } @@ -198,13 +190,11 @@ int MidiDriver_TIMIDITY::open() { /* should read something like "200 63017 is ready acceptable", * where 63017 is port for data connection */ - // FIXME: The following looks like a cheap endian test. If this is true, then - // it should be replaced by suitable #ifdef SCUMM_LITTLE_ENDIAN. - i = 1; - if (*(char *)&i == 1) - res = timidity_ctl_command("OPEN lsb"); - else - res = timidity_ctl_command("OPEN msb"); +#ifdef SCUMM_LITTLE_ENDIAN + res = timidity_ctl_command("OPEN lsb"); +#else + res = timidity_ctl_command("OPEN msb"); +#endif if (atoi(res) != 200) { warning("TiMidity: bad reply for OPEN command: %s", res); @@ -215,9 +205,15 @@ int MidiDriver_TIMIDITY::open() { /* * open data connection */ - data_port = atoi(res + 4); + num = atoi(res + 4); + if (num > 65535) { + warning("TiMidity: Invalid port %d given.\n", num); + close_all(); + return -1; + } + snprintf(data_port, sizeof(data_port), "%d", num); if ((_data_fd = connect_to_server(timidity_host, data_port)) < 0) { - warning("TiMidity: can't open data connection (host=%s, port=%d)", timidity_host, data_port); + warning("TiMidity: can't open data connection (host=%s, port=%s)", timidity_host, data_port); close_all(); return -1; } @@ -226,7 +222,7 @@ int MidiDriver_TIMIDITY::open() { * "200 Ready data connection" */ res = timidity_ctl_command(NULL); if (atoi(res) != 200) { - warning("Can't connect timidity: %s\t(host=%s, port=%d)", res, timidity_host, data_port); + warning("Can't connect timidity: %s\t(host=%s, port=%s)", res, timidity_host, data_port); close_all(); return -1; } @@ -277,46 +273,33 @@ void MidiDriver_TIMIDITY::teardown() { close_all(); } -in_addr_t MidiDriver_TIMIDITY::host_to_addr(const char* address) { - in_addr_t addr; - struct hostent *hp; - - /* first check if IP address is given (like 127.0.0.1)*/ - if ((addr = inet_addr(address)) != INADDR_NONE) - return addr; - - /* if not, try to resolve a hostname */ - if ((hp = gethostbyname(address)) == NULL) { - warning("TiMidity: unknown hostname: %s", address); - return INADDR_NONE; - } - - memcpy(&addr, hp->h_addr, (int)sizeof(in_addr_t) <= hp->h_length ? sizeof(in_addr_t) : hp->h_length); - - return addr; -} - -int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, unsigned short tcp_port) { +int MidiDriver_TIMIDITY::connect_to_server(const char* hostname, const char* tcp_port) { int fd; - struct sockaddr_in in; - unsigned int addr; - - /* create socket */ - if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - warning("TiMidity: socket(): %s", strerror(errno)); + struct addrinfo hints; + struct addrinfo *result, *rp; + + /* get all address(es) matching host and port */ + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ + if (getaddrinfo(hostname, tcp_port, &hints, &result) != 0) { + warning("TiMidity: getaddrinfo: %s\n", strerror(errno)); return -1; } - /* connect */ - memset(&in, 0, sizeof(in)); - in.sin_family = AF_INET; - in.sin_port = htons(tcp_port); - addr = host_to_addr(hostname); - memcpy(&in.sin_addr, &addr, 4); - - if (connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0) { - warning("TiMidity: connect(): %s", strerror(errno)); + /* Try all address structures we have got previously */ + for (rp = result; rp != NULL; rp = rp->ai_next) { + if ((fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1) + continue; + if (connect(fd, rp->ai_addr, rp->ai_addrlen) != -1) + break; ::close(fd); + } + + freeaddrinfo(result); + + if (rp == NULL) { + warning("TiMidity: Could not connect\n"); return -1; } diff --git a/backends/midi/windows.cpp b/backends/midi/windows.cpp index e2b327ffa7..52a46200cb 100644 --- a/backends/midi/windows.cpp +++ b/backends/midi/windows.cpp @@ -185,6 +185,9 @@ MusicDevices WindowsMusicPlugin::getDevices() const { deviceNames.push_back(tmp.szPname); } + // Limit us to the number of actually retrieved devices. + numDevs = deviceNames.size(); + // Check for non-unique device names. This may happen if someone has devices with identical // names (e. g. more than one USB device of the exact same hardware type). It seems that this // does happen in reality sometimes. We generate index numbers for these devices. diff --git a/backends/modular-backend.cpp b/backends/modular-backend.cpp index d8be9ca7ed..1fef5c5a48 100644 --- a/backends/modular-backend.cpp +++ b/backends/modular-backend.cpp @@ -241,6 +241,10 @@ void ModularBackend::displayMessageOnOSD(const char *msg) { _graphicsManager->displayMessageOnOSD(msg); } +void ModularBackend::displayActivityIconOnOSD(const Graphics::Surface *icon) { + _graphicsManager->displayActivityIconOnOSD(icon); +} + void ModularBackend::quit() { exit(0); } diff --git a/backends/modular-backend.h b/backends/modular-backend.h index 20e8b7357d..23f0e2d547 100644 --- a/backends/modular-backend.h +++ b/backends/modular-backend.h @@ -127,6 +127,7 @@ public: virtual void quit(); virtual void displayMessageOnOSD(const char *msg); + virtual void displayActivityIconOnOSD(const Graphics::Surface *icon); //@} diff --git a/backends/module.mk b/backends/module.mk index 3d412c031a..158f9a3b7c 100644 --- a/backends/module.mk +++ b/backends/module.mk @@ -3,6 +3,7 @@ MODULE := backends MODULE_OBJS := \ base-backend.o \ modular-backend.o \ + audiocd/audiocd-stream.o \ audiocd/default/default-audiocd.o \ events/default/default-events.o \ fs/abstract-fs.o \ @@ -18,6 +19,72 @@ MODULE_OBJS := \ saves/default/default-saves.o \ timer/default/default-timer.o +ifdef USE_CLOUD + +ifdef USE_LIBCURL +MODULE_OBJS += \ + cloud/cloudicon.o \ + cloud/cloudmanager.o \ + cloud/iso8601.o \ + cloud/storage.o \ + cloud/storagefile.o \ + cloud/downloadrequest.o \ + cloud/folderdownloadrequest.o \ + cloud/savessyncrequest.o \ + cloud/box/boxstorage.o \ + cloud/box/boxlistdirectorybyidrequest.o \ + cloud/box/boxtokenrefresher.o \ + cloud/box/boxuploadrequest.o \ + cloud/dropbox/dropboxstorage.o \ + cloud/dropbox/dropboxcreatedirectoryrequest.o \ + cloud/dropbox/dropboxinforequest.o \ + cloud/dropbox/dropboxlistdirectoryrequest.o \ + cloud/dropbox/dropboxuploadrequest.o \ + cloud/googledrive/googledrivelistdirectorybyidrequest.o \ + cloud/googledrive/googledrivestorage.o \ + cloud/googledrive/googledrivetokenrefresher.o \ + cloud/googledrive/googledriveuploadrequest.o \ + cloud/id/idstorage.o \ + cloud/id/idcreatedirectoryrequest.o \ + cloud/id/iddownloadrequest.o \ + cloud/id/idlistdirectoryrequest.o \ + cloud/id/idresolveidrequest.o \ + cloud/id/idstreamfilerequest.o \ + cloud/onedrive/onedrivestorage.o \ + cloud/onedrive/onedrivecreatedirectoryrequest.o \ + cloud/onedrive/onedrivetokenrefresher.o \ + cloud/onedrive/onedrivelistdirectoryrequest.o \ + cloud/onedrive/onedriveuploadrequest.o +endif +endif + +ifdef USE_LIBCURL +MODULE_OBJS += \ + networking/curl/connectionmanager.o \ + networking/curl/networkreadstream.o \ + networking/curl/curlrequest.o \ + networking/curl/curljsonrequest.o \ + networking/curl/request.o +endif + +ifdef USE_SDL_NET +MODULE_OBJS += \ + networking/sdl_net/client.o \ + networking/sdl_net/getclienthandler.o \ + networking/sdl_net/handlers/createdirectoryhandler.o \ + networking/sdl_net/handlers/downloadfilehandler.o \ + networking/sdl_net/handlers/filesajaxpagehandler.o \ + networking/sdl_net/handlers/filesbasehandler.o \ + networking/sdl_net/handlers/filespagehandler.o \ + networking/sdl_net/handlers/indexpagehandler.o \ + networking/sdl_net/handlers/listajaxhandler.o \ + networking/sdl_net/handlers/resourcehandler.o \ + networking/sdl_net/handlers/uploadfilehandler.o \ + networking/sdl_net/handlerutils.o \ + networking/sdl_net/localwebserver.o \ + networking/sdl_net/reader.o \ + networking/sdl_net/uploadfileclienthandler.o +endif ifdef USE_ELF_LOADER MODULE_OBJS += \ @@ -52,10 +119,16 @@ endif # OpenGL specific source files. ifdef USE_OPENGL MODULE_OBJS += \ + graphics/opengl/context.o \ graphics/opengl/debug.o \ - graphics/opengl/extensions.o \ + graphics/opengl/framebuffer.o \ graphics/opengl/opengl-graphics.o \ - graphics/opengl/texture.o + graphics/opengl/shader.o \ + graphics/opengl/texture.o \ + graphics/opengl/pipelines/clut8.o \ + graphics/opengl/pipelines/fixed.o \ + graphics/opengl/pipelines/pipeline.o \ + graphics/opengl/pipelines/shader.o endif # SDL specific source files. @@ -84,6 +157,15 @@ MODULE_OBJS += \ endif endif +# Connection::isLimited +ifeq ($(BACKEND),android) +MODULE_OBJS += \ + networking/connection/islimited-android.o +else +MODULE_OBJS += \ + networking/connection/islimited-default.o +endif + ifdef POSIX MODULE_OBJS += \ fs/posix/posix-fs.o \ @@ -97,6 +179,7 @@ endif ifdef MACOSX MODULE_OBJS += \ + audiocd/macosx/macosx-audiocd.o \ midi/coreaudio.o \ midi/coremidi.o \ updates/macosx/macosx-updates.o \ @@ -105,14 +188,22 @@ endif ifdef WIN32 MODULE_OBJS += \ + audiocd/win32/win32-audiocd.o \ fs/windows/windows-fs.o \ fs/windows/windows-fs-factory.o \ midi/windows.o \ plugins/win32/win32-provider.o \ saves/windows/windows-saves.o \ + updates/win32/win32-updates.o \ taskbar/win32/win32-taskbar.o endif +ifeq ($(BACKEND),androidsdl) +MODULE_OBJS += \ + events/androidsdl/androidsdl-events.o \ + graphics/androidsdl/androidsdl-graphics.o +endif + ifdef AMIGAOS MODULE_OBJS += \ fs/amigaos4/amigaos4-fs.o \ @@ -128,6 +219,11 @@ MODULE_OBJS += \ events/ps3sdl/ps3sdl-events.o endif +ifdef USE_LINUXCD +MODULE_OBJS += \ + audiocd/linux/linux-audiocd.o +endif + ifeq ($(BACKEND),tizen) MODULE_OBJS += \ timer/tizen/timer.o diff --git a/backends/networking/connection/islimited-android.cpp b/backends/networking/connection/islimited-android.cpp new file mode 100644 index 0000000000..8989f218ec --- /dev/null +++ b/backends/networking/connection/islimited-android.cpp @@ -0,0 +1,35 @@ +/* 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 "backends/networking/connection/islimited.h" +#include "backends/platform/android/jni.h" + +namespace Networking { +namespace Connection { + +bool isLimited() { + return JNI::isConnectionLimited(); +} + +} // End of namespace Connection +} // End of namespace Networking + diff --git a/backends/networking/connection/islimited-default.cpp b/backends/networking/connection/islimited-default.cpp new file mode 100644 index 0000000000..a993077fff --- /dev/null +++ b/backends/networking/connection/islimited-default.cpp @@ -0,0 +1,36 @@ +/* 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 "backends/networking/connection/islimited.h" +#include "common/textconsole.h" + +namespace Networking { +namespace Connection { + +bool isLimited() { + warning("Networking::Connection::isLimited(): not limited by default"); + return false; +} + +} // End of namespace Connection +} // End of namespace Networking + diff --git a/backends/networking/connection/islimited.h b/backends/networking/connection/islimited.h new file mode 100644 index 0000000000..b23d31d157 --- /dev/null +++ b/backends/networking/connection/islimited.h @@ -0,0 +1,39 @@ +/* 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 NETWORKING_CONNECTION_ISLIMITED_H +#define NETWORKING_CONNECTION_ISLIMITED_H + +namespace Networking { +namespace Connection { + +/** +* Returns whether connection's limited (if available on the target system). +* +* Returns true if connection seems limited. +*/ +bool isLimited(); + +} // End of namespace Connection +} // End of namespace Networking + +#endif /*NETWORKING_CONNECTION_ISLIMITED_H*/ diff --git a/backends/networking/curl/connectionmanager.cpp b/backends/networking/curl/connectionmanager.cpp new file mode 100644 index 0000000000..e1761bddc6 --- /dev/null +++ b/backends/networking/curl/connectionmanager.cpp @@ -0,0 +1,199 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/system.h" +#include "common/timer.h" +#include <curl/curl.h> + +namespace Common { + +DECLARE_SINGLETON(Networking::ConnectionManager); + +} + +namespace Networking { + +ConnectionManager::ConnectionManager(): _multi(0), _timerStarted(false), _frame(0) { + curl_global_init(CURL_GLOBAL_ALL); + _multi = curl_multi_init(); +} + +ConnectionManager::~ConnectionManager() { + stopTimer(); + + //terminate all requests + _handleMutex.lock(); + for (Common::Array<RequestWithCallback>::iterator i = _requests.begin(); i != _requests.end(); ++i) { + Request *request = i->request; + RequestCallback callback = i->onDeleteCallback; + if (request) + request->finish(); + delete request; + if (callback) + (*callback)(request); + } + _requests.clear(); + + //cleanup + curl_multi_cleanup(_multi); + curl_global_cleanup(); + _multi = nullptr; + _handleMutex.unlock(); +} + +void ConnectionManager::registerEasyHandle(CURL *easy) const { + curl_multi_add_handle(_multi, easy); +} + +Request *ConnectionManager::addRequest(Request *request, RequestCallback callback) { + _addedRequestsMutex.lock(); + _addedRequests.push_back(RequestWithCallback(request, callback)); + if (!_timerStarted) + startTimer(); + _addedRequestsMutex.unlock(); + return request; +} + +Common::String ConnectionManager::urlEncode(Common::String s) const { + if (!_multi) + return ""; + char *output = curl_easy_escape(_multi, s.c_str(), s.size()); + if (output) { + Common::String result = output; + curl_free(output); + return result; + } + return ""; +} + +uint32 ConnectionManager::getCloudRequestsPeriodInMicroseconds() { + return TIMER_INTERVAL * CLOUD_PERIOD; +} + +//private goes here: + +void connectionsThread(void *ignored) { + ConnMan.handle(); +} + +void ConnectionManager::startTimer(int interval) { + Common::TimerManager *manager = g_system->getTimerManager(); + if (manager->installTimerProc(connectionsThread, interval, 0, "Networking::ConnectionManager's Timer")) { + _timerStarted = true; + } else { + warning("Failed to install Networking::ConnectionManager's timer"); + } +} + +void ConnectionManager::stopTimer() { + debug(9, "timer stopped"); + Common::TimerManager *manager = g_system->getTimerManager(); + manager->removeTimerProc(connectionsThread); + _timerStarted = false; +} + +bool ConnectionManager::hasAddedRequests() { + _addedRequestsMutex.lock(); + bool hasNewRequests = !_addedRequests.empty(); + _addedRequestsMutex.unlock(); + return hasNewRequests; +} + +void ConnectionManager::handle() { + //lock mutex here (in case another handle() would be called before this one ends) + _handleMutex.lock(); + ++_frame; + if (_frame % CLOUD_PERIOD == 0) + interateRequests(); + if (_frame % CURL_PERIOD == 0) + processTransfers(); + + if (_requests.empty() && !hasAddedRequests()) + stopTimer(); + _handleMutex.unlock(); +} + +void ConnectionManager::interateRequests() { + //add new requests + _addedRequestsMutex.lock(); + for (Common::Array<RequestWithCallback>::iterator i = _addedRequests.begin(); i != _addedRequests.end(); ++i) { + _requests.push_back(*i); + } + _addedRequests.clear(); + _addedRequestsMutex.unlock(); + + //call handle() of all running requests (so they can do their work) + debug(9, "handling %d request(s)", _requests.size()); + for (Common::Array<RequestWithCallback>::iterator i = _requests.begin(); i != _requests.end();) { + Request *request = i->request; + if (request) { + if (request->state() == PROCESSING) + request->handle(); + else if (request->state() == RETRY) + request->handleRetry(); + } + + if (!request || request->state() == FINISHED) { + delete (i->request); + if (i->onDeleteCallback) + (*i->onDeleteCallback)(i->request); //that's not a mistake (we're passing an address and that method knows there is no object anymore) + _requests.erase(i); + continue; + } + + ++i; + } +} + +void ConnectionManager::processTransfers() { + if (!_multi) return; + + //check libcurl's transfers and notify requests of messages from queue (transfer completion or failure) + int transfersRunning; + curl_multi_perform(_multi, &transfersRunning); + + int messagesInQueue; + CURLMsg *curlMsg; + while ((curlMsg = curl_multi_info_read(_multi, &messagesInQueue))) { + CURL *easyHandle = curlMsg->easy_handle; + + NetworkReadStream *stream; + curl_easy_getinfo(easyHandle, CURLINFO_PRIVATE, &stream); + if (stream) + stream->finished(); + + if (curlMsg->msg == CURLMSG_DONE) { + debug(9, "ConnectionManager: SUCCESS (%d - %s)", curlMsg->data.result, curl_easy_strerror(curlMsg->data.result)); + } else { + warning("ConnectionManager: FAILURE (CURLMsg (%d))", curlMsg->msg); + } + + curl_multi_remove_handle(_multi, easyHandle); + } +} + +} // End of namespace Cloud diff --git a/backends/networking/curl/connectionmanager.h b/backends/networking/curl/connectionmanager.h new file mode 100644 index 0000000000..f6a9fcb36f --- /dev/null +++ b/backends/networking/curl/connectionmanager.h @@ -0,0 +1,127 @@ +/* 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 BACKENDS_NETWORKING_CURL_CONNECTIONMANAGER_H +#define BACKENDS_NETWORKING_CURL_CONNECTIONMANAGER_H + +#include "backends/networking/curl/request.h" +#include "common/str.h" +#include "common/singleton.h" +#include "common/hashmap.h" +#include "common/mutex.h" + +typedef void CURL; +typedef void CURLM; +struct curl_slist; + +namespace Networking { + +class NetworkReadStream; + +class ConnectionManager : public Common::Singleton<ConnectionManager> { + static const uint32 FRAMES_PER_SECOND = 20; + static const uint32 TIMER_INTERVAL = 1000000 / FRAMES_PER_SECOND; + static const uint32 CLOUD_PERIOD = 20; //every 20th frame + static const uint32 CURL_PERIOD = 1; //every frame + + friend void connectionsThread(void *); //calls handle() + + typedef Common::BaseCallback<Request *> *RequestCallback; + + /** + * RequestWithCallback is used by ConnectionManager to + * storage the Request and a callback which should be + * called on Request delete. + * + * Usually one won't need to pass such callback, but + * in some cases you'd like to know whether Request is + * still running. + * + * For example, Cloud::Storage is keeping track of how + * many Requests are running, and thus it needs to know + * that Request was destroyed to decrease its counter. + * + * onDeleteCallback is called with *invalid* pointer. + * ConnectionManager deletes Request first and then passes + * the pointer to the callback. One may use the address + * to find it in own HashMap or Array and remove it. + * So, again, this pointer is for information only. One + * cannot use it. + */ + struct RequestWithCallback { + Request *request; + RequestCallback onDeleteCallback; + + RequestWithCallback(Request *rq = nullptr, RequestCallback cb = nullptr): request(rq), onDeleteCallback(cb) {} + }; + + CURLM *_multi; + bool _timerStarted; + Common::Array<RequestWithCallback> _requests, _addedRequests; + Common::Mutex _handleMutex, _addedRequestsMutex; + uint32 _frame; + + void startTimer(int interval = TIMER_INTERVAL); + void stopTimer(); + void handle(); + void interateRequests(); + void processTransfers(); + bool hasAddedRequests(); + +public: + ConnectionManager(); + virtual ~ConnectionManager(); + + /** + * All libcurl transfers are going through this ConnectionManager. + * So, if you want to start any libcurl transfer, you must create + * an easy handle and register it using this method. + */ + void registerEasyHandle(CURL *easy) const; + + /** + * Use this method to add new Request into manager's queue. + * Manager will periodically call handle() method of these + * Requests until they set their state to FINISHED. + * + * If Request's state is RETRY, handleRetry() is called instead. + * + * The passed callback would be called after Request is deleted. + * + * @note This method starts the timer if it's not started yet. + * + * @return the same Request pointer, just as a shortcut + */ + Request *addRequest(Request *request, RequestCallback callback = nullptr); + + /** Return URL-encoded version of given string. */ + Common::String urlEncode(Common::String s) const; + + static uint32 getCloudRequestsPeriodInMicroseconds(); +}; + +/** Shortcut for accessing the connection manager. */ +#define ConnMan Networking::ConnectionManager::instance() + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/curljsonrequest.cpp b/backends/networking/curl/curljsonrequest.cpp new file mode 100644 index 0000000000..7764e11950 --- /dev/null +++ b/backends/networking/curl/curljsonrequest.cpp @@ -0,0 +1,215 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/curl/curljsonrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/debug.h" +#include "common/json.h" +#include <curl/curl.h> + +namespace Networking { + +CurlJsonRequest::CurlJsonRequest(JsonCallback cb, ErrorCallback ecb, Common::String url) : + CurlRequest(nullptr, ecb, url), _jsonCallback(cb), _contentsStream(DisposeAfterUse::YES), + _buffer(new byte[CURL_JSON_REQUEST_BUFFER_SIZE]) {} + +CurlJsonRequest::~CurlJsonRequest() { + delete _jsonCallback; + delete[] _buffer; +} + +char *CurlJsonRequest::getPreparedContents() { + //write one more byte in the end + byte zero[1] = {0}; + _contentsStream.write(zero, 1); + + //replace all "bad" bytes with '.' character + byte *result = _contentsStream.getData(); + uint32 size = _contentsStream.size(); + for (uint32 i = 0; i < size; ++i) { + if (result[i] == '\n') + result[i] = ' '; //yeah, kinda stupid + else if (result[i] < 0x20 || result[i] > 0x7f) + result[i] = '.'; + } + + //make it zero-terminated string + result[size - 1] = '\0'; + + return (char *)result; +} + +void CurlJsonRequest::handle() { + if (!_stream) _stream = makeStream(); + + if (_stream) { + uint32 readBytes = _stream->read(_buffer, CURL_JSON_REQUEST_BUFFER_SIZE); + if (readBytes != 0) + if (_contentsStream.write(_buffer, readBytes) != readBytes) + warning("CurlJsonRequest: unable to write all the bytes into MemoryWriteStreamDynamic"); + + if (_stream->eos()) { + char *contents = getPreparedContents(); + Common::JSONValue *json = Common::JSON::parse(contents); + if (json) { + finishJson(json); //it's JSON even if's not 200 OK? That's fine!.. + } else { + if (_stream->httpResponseCode() == 200) //no JSON, but 200 OK? That's fine!.. + finishJson(nullptr); + else + finishError(ErrorResponse(this, false, true, contents, _stream->httpResponseCode())); + } + } + } +} + +void CurlJsonRequest::restart() { + if (_stream) + delete _stream; + _stream = nullptr; + _contentsStream = Common::MemoryWriteStreamDynamic(DisposeAfterUse::YES); + //with no stream available next handle() will create another one +} + +void CurlJsonRequest::finishJson(Common::JSONValue *json) { + Request::finishSuccess(); + if (_jsonCallback) + (*_jsonCallback)(JsonResponse(this, json)); //potential memory leak, free it in your callbacks! + else + delete json; +} + +bool CurlJsonRequest::jsonIsObject(Common::JSONValue *item, const char *warningPrefix) { + if (item == nullptr) { + warning("%s: passed item is NULL", warningPrefix); + return false; + } + + if (item->isObject()) return true; + + warning("%s: passed item is not an object", warningPrefix); + debug(9, "%s", item->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsObject(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isObject()) return true; + + warning("%s: passed item's \"%s\" attribute is not an object", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsString(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isString()) return true; + + warning("%s: passed item's \"%s\" attribute is not a string", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isIntegerNumber()) return true; + + warning("%s: passed item's \"%s\" attribute is not an integer", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsArray(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isArray()) return true; + + warning("%s: passed item's \"%s\" attribute is not an array", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsStringOrIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + if (item.getVal(key)->isString() || item.getVal(key)->isIntegerNumber()) return true; + + warning("%s: passed item's \"%s\" attribute is neither a string or an integer", warningPrefix, key); + debug(9, "%s", item.getVal(key)->stringify(true).c_str()); + return false; +} + +bool CurlJsonRequest::jsonContainsAttribute(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional) { + if (!item.contains(key)) { + if (isOptional) { + return true; + } + + warning("%s: passed item misses the \"%s\" attribute", warningPrefix, key); + return false; + } + + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/curl/curljsonrequest.h b/backends/networking/curl/curljsonrequest.h new file mode 100644 index 0000000000..11c6bc81ea --- /dev/null +++ b/backends/networking/curl/curljsonrequest.h @@ -0,0 +1,67 @@ +/* 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 BACKENDS_NETWORKING_CURL_CURLJSONREQUEST_H +#define BACKENDS_NETWORKING_CURL_CURLJSONREQUEST_H + +#include "backends/networking/curl/curlrequest.h" +#include "common/memstream.h" +#include "common/json.h" + +namespace Networking { + +typedef Response<Common::JSONValue *> JsonResponse; +typedef Common::BaseCallback<JsonResponse> *JsonCallback; + +#define CURL_JSON_REQUEST_BUFFER_SIZE 512 * 1024 + +class CurlJsonRequest: public CurlRequest { +protected: + JsonCallback _jsonCallback; + Common::MemoryWriteStreamDynamic _contentsStream; + byte *_buffer; + + /** Prepares raw bytes from _contentsStream to be parsed with Common::JSON::parse(). */ + char *getPreparedContents(); + + /** Sets FINISHED state and passes the JSONValue * into user's callback in JsonResponse. */ + virtual void finishJson(Common::JSONValue *json); + +public: + CurlJsonRequest(JsonCallback cb, ErrorCallback ecb, Common::String url); + virtual ~CurlJsonRequest(); + + virtual void handle(); + virtual void restart(); + + static bool jsonIsObject(Common::JSONValue *item, const char *warningPrefix); + static bool jsonContainsObject(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsString(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsArray(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsStringOrIntegerNumber(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); + static bool jsonContainsAttribute(Common::JSONObject &item, const char *key, const char *warningPrefix, bool isOptional = false); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/curlrequest.cpp b/backends/networking/curl/curlrequest.cpp new file mode 100644 index 0000000000..7ee8d66c66 --- /dev/null +++ b/backends/networking/curl/curlrequest.cpp @@ -0,0 +1,162 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/curl/curlrequest.h" +#include "backends/networking/curl/connectionmanager.h" +#include "backends/networking/curl/networkreadstream.h" +#include "common/textconsole.h" +#include <curl/curl.h> + +namespace Networking { + +CurlRequest::CurlRequest(DataCallback cb, ErrorCallback ecb, Common::String url): + Request(cb, ecb), _url(url), _stream(nullptr), _headersList(nullptr), _bytesBuffer(nullptr), + _bytesBufferSize(0), _uploading(false), _usingPatch(false) {} + +CurlRequest::~CurlRequest() { + delete _stream; + delete _bytesBuffer; +} + +NetworkReadStream *CurlRequest::makeStream() { + if (_bytesBuffer) + return new NetworkReadStream(_url.c_str(), _headersList, _bytesBuffer, _bytesBufferSize, _uploading, _usingPatch, true); + if (!_formFields.empty() || !_formFiles.empty()) + return new NetworkReadStream(_url.c_str(), _headersList, _formFields, _formFiles); + return new NetworkReadStream(_url.c_str(), _headersList, _postFields, _uploading, _usingPatch); +} + +void CurlRequest::handle() { + if (!_stream) _stream = makeStream(); + + if (_stream && _stream->eos()) { + if (_stream->httpResponseCode() != 200) { + warning("CurlRequest: HTTP response code is not 200 OK (it's %ld)", _stream->httpResponseCode()); + ErrorResponse error(this, false, true, "", _stream->httpResponseCode()); + finishError(error); + return; + } + + finishSuccess(); //note that this Request doesn't call its callback on success (that's because it has nothing to return) + } +} + +void CurlRequest::restart() { + if (_stream) + delete _stream; + _stream = nullptr; + //with no stream available next handle() will create another one +} + +Common::String CurlRequest::date() const { + if (_stream) { + Common::String headers = _stream->responseHeaders(); + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "Date: "); + + if (position) { + Common::String result = ""; + char c; + for (const char *i = position + 6; c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + return result; + } + } + return ""; +} + +void CurlRequest::setHeaders(Common::Array<Common::String> &headers) { + curl_slist_free_all(_headersList); + _headersList = nullptr; + for (uint32 i = 0; i < headers.size(); ++i) + addHeader(headers[i]); +} + +void CurlRequest::addHeader(Common::String header) { + _headersList = curl_slist_append(_headersList, header.c_str()); +} + +void CurlRequest::addPostField(Common::String keyValuePair) { + if (_bytesBuffer) + warning("CurlRequest: added POST fields would be ignored, because there is buffer present"); + + if (!_formFields.empty() || !_formFiles.empty()) + warning("CurlRequest: added POST fields would be ignored, because there are form fields/files present"); + + if (_postFields == "") + _postFields = keyValuePair; + else + _postFields += "&" + keyValuePair; +} + +void CurlRequest::addFormField(Common::String name, Common::String value) { + if (_bytesBuffer) + warning("CurlRequest: added POST form fields would be ignored, because there is buffer present"); + + if (_formFields.contains(name)) + warning("CurlRequest: form field '%s' already had a value", name.c_str()); + + _formFields[name] = value; +} + +void CurlRequest::addFormFile(Common::String name, Common::String filename) { + if (_bytesBuffer) + warning("CurlRequest: added POST form files would be ignored, because there is buffer present"); + + if (_formFields.contains(name)) + warning("CurlRequest: form file field '%s' already had a value", name.c_str()); + + _formFiles[name] = filename; +} + +void CurlRequest::setBuffer(byte *buffer, uint32 size) { + if (_postFields != "") + warning("CurlRequest: added POST fields would be ignored, because buffer added"); + + if (_bytesBuffer) + delete _bytesBuffer; + + _bytesBuffer = buffer; + _bytesBufferSize = size; +} + +void CurlRequest::usePut() { _uploading = true; } + +void CurlRequest::usePatch() { _usingPatch = true; } + +NetworkReadStreamResponse CurlRequest::execute() { + if (!_stream) { + _stream = makeStream(); + ConnMan.addRequest(this); + } + + return NetworkReadStreamResponse(this, _stream); +} + +const NetworkReadStream *CurlRequest::getNetworkReadStream() const { return _stream; } + +} // End of namespace Networking diff --git a/backends/networking/curl/curlrequest.h b/backends/networking/curl/curlrequest.h new file mode 100644 index 0000000000..75fb787f86 --- /dev/null +++ b/backends/networking/curl/curlrequest.h @@ -0,0 +1,100 @@ +/* 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 BACKENDS_NETWORKING_CURL_CURLREQUEST_H +#define BACKENDS_NETWORKING_CURL_CURLREQUEST_H + +#include "backends/networking/curl/request.h" +#include "common/str.h" +#include "common/array.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +struct curl_slist; + +namespace Networking { + +class NetworkReadStream; + +typedef Response<NetworkReadStream *> NetworkReadStreamResponse; +typedef Common::BaseCallback<NetworkReadStreamResponse> *NetworkReadStreamCallback; + +class CurlRequest: public Request { +protected: + Common::String _url; + NetworkReadStream *_stream; + curl_slist *_headersList; + Common::String _postFields; + Common::HashMap<Common::String, Common::String> _formFields; + Common::HashMap<Common::String, Common::String> _formFiles; + byte *_bytesBuffer; + uint32 _bytesBufferSize; + bool _uploading; //using PUT method + bool _usingPatch; //using PATCH method + + virtual NetworkReadStream *makeStream(); + +public: + CurlRequest(DataCallback cb, ErrorCallback ecb, Common::String url); + virtual ~CurlRequest(); + + virtual void handle(); + virtual void restart(); + virtual Common::String date() const; + + /** Replaces all headers with the passed array of headers. */ + virtual void setHeaders(Common::Array<Common::String> &headers); + + /** Adds a header into headers list. */ + virtual void addHeader(Common::String header); + + /** Adds a post field (key=value pair). */ + virtual void addPostField(Common::String field); + + /** Adds a form/multipart field (name, value). */ + virtual void addFormField(Common::String name, Common::String value); + + /** Adds a form/multipart file (field name, file name). */ + virtual void addFormFile(Common::String name, Common::String filename); + + /** Sets bytes buffer. */ + virtual void setBuffer(byte *buffer, uint32 size); + + /** Remembers to use PUT method when it would create NetworkReadStream. */ + virtual void usePut(); + + /** Remembers to use PATCH method when it would create NetworkReadStream. */ + virtual void usePatch(); + + /** + * Starts this Request with ConnMan. + * @return its NetworkReadStream in NetworkReadStreamResponse. + */ + virtual NetworkReadStreamResponse execute(); + + /** Returns Request's NetworkReadStream. */ + const NetworkReadStream *getNetworkReadStream() const; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/networkreadstream.cpp b/backends/networking/curl/networkreadstream.cpp new file mode 100644 index 0000000000..b4516701a7 --- /dev/null +++ b/backends/networking/curl/networkreadstream.cpp @@ -0,0 +1,257 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/curl/networkreadstream.h" +#include "backends/networking/curl/connectionmanager.h" +#include "base/version.h" +#include <curl/curl.h> + +namespace Networking { + +static size_t curlDataCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->write(d, n * l); + return 0; +} + +static size_t curlReadDataCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->fillWithSendingContents(d, n * l); + return 0; +} + +static size_t curlHeadersCallback(char *d, size_t n, size_t l, void *p) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + return stream->addResponseHeaders(d, n * l); + return 0; +} + +static int curlProgressCallback(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + NetworkReadStream *stream = (NetworkReadStream *)p; + if (stream) + stream->setProgress(dlnow, dltotal); + return 0; +} + +static int curlProgressCallbackOlder(void *p, double dltotal, double dlnow, double ultotal, double ulnow) { + // for libcurl older than 7.32.0 (CURLOPT_PROGRESSFUNCTION) + return curlProgressCallback(p, (curl_off_t)dltotal, (curl_off_t)dlnow, (curl_off_t)ultotal, (curl_off_t)ulnow); +} + +void NetworkReadStream::init(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post) { + _eos = _requestComplete = false; + _sendingContentsBuffer = nullptr; + _sendingContentsSize = _sendingContentsPos = 0; + _progressDownloaded = _progressTotal = 0; + + _easy = curl_easy_init(); + curl_easy_setopt(_easy, CURLOPT_WRITEFUNCTION, curlDataCallback); + curl_easy_setopt(_easy, CURLOPT_WRITEDATA, this); //so callback can call us + curl_easy_setopt(_easy, CURLOPT_PRIVATE, this); //so ConnectionManager can call us when request is complete + curl_easy_setopt(_easy, CURLOPT_HEADER, 0L); + curl_easy_setopt(_easy, CURLOPT_HEADERDATA, this); + curl_easy_setopt(_easy, CURLOPT_HEADERFUNCTION, curlHeadersCallback); + curl_easy_setopt(_easy, CURLOPT_URL, url); + curl_easy_setopt(_easy, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(_easy, CURLOPT_FOLLOWLOCATION, 1L); //probably it's OK to have it always on + curl_easy_setopt(_easy, CURLOPT_HTTPHEADER, headersList); + curl_easy_setopt(_easy, CURLOPT_USERAGENT, gScummVMFullVersion); + curl_easy_setopt(_easy, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(_easy, CURLOPT_PROGRESSFUNCTION, curlProgressCallbackOlder); + curl_easy_setopt(_easy, CURLOPT_PROGRESSDATA, this); +#if LIBCURL_VERSION_NUM >= 0x072000 + // CURLOPT_XFERINFOFUNCTION introduced in libcurl 7.32.0 + // CURLOPT_PROGRESSFUNCTION is used as a backup plan in case older version is used + curl_easy_setopt(_easy, CURLOPT_XFERINFOFUNCTION, curlProgressCallback); + curl_easy_setopt(_easy, CURLOPT_XFERINFODATA, this); +#endif + if (uploading) { + curl_easy_setopt(_easy, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(_easy, CURLOPT_READDATA, this); + curl_easy_setopt(_easy, CURLOPT_READFUNCTION, curlReadDataCallback); + _sendingContentsBuffer = buffer; + _sendingContentsSize = bufferSize; + } else if (usingPatch) { + curl_easy_setopt(_easy, CURLOPT_CUSTOMREQUEST, "PATCH"); + } else { + if (post || bufferSize != 0) { + curl_easy_setopt(_easy, CURLOPT_POSTFIELDSIZE, bufferSize); + curl_easy_setopt(_easy, CURLOPT_COPYPOSTFIELDS, buffer); + } + } + ConnMan.registerEasyHandle(_easy); +} + +void NetworkReadStream::init(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles) { + _eos = _requestComplete = false; + _sendingContentsBuffer = nullptr; + _sendingContentsSize = _sendingContentsPos = 0; + _progressDownloaded = _progressTotal = 0; + + _easy = curl_easy_init(); + curl_easy_setopt(_easy, CURLOPT_WRITEFUNCTION, curlDataCallback); + curl_easy_setopt(_easy, CURLOPT_WRITEDATA, this); //so callback can call us + curl_easy_setopt(_easy, CURLOPT_PRIVATE, this); //so ConnectionManager can call us when request is complete + curl_easy_setopt(_easy, CURLOPT_HEADER, 0L); + curl_easy_setopt(_easy, CURLOPT_HEADERDATA, this); + curl_easy_setopt(_easy, CURLOPT_HEADERFUNCTION, curlHeadersCallback); + curl_easy_setopt(_easy, CURLOPT_URL, url); + curl_easy_setopt(_easy, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(_easy, CURLOPT_FOLLOWLOCATION, 1L); //probably it's OK to have it always on + curl_easy_setopt(_easy, CURLOPT_HTTPHEADER, headersList); + curl_easy_setopt(_easy, CURLOPT_USERAGENT, gScummVMFullVersion); + curl_easy_setopt(_easy, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(_easy, CURLOPT_PROGRESSFUNCTION, curlProgressCallbackOlder); + curl_easy_setopt(_easy, CURLOPT_PROGRESSDATA, this); +#if LIBCURL_VERSION_NUM >= 0x072000 + // CURLOPT_XFERINFOFUNCTION introduced in libcurl 7.32.0 + // CURLOPT_PROGRESSFUNCTION is used as a backup plan in case older version is used + curl_easy_setopt(_easy, CURLOPT_XFERINFOFUNCTION, curlProgressCallback); + curl_easy_setopt(_easy, CURLOPT_XFERINFODATA, this); +#endif + + // set POST multipart upload form fields/files + struct curl_httppost *formpost = nullptr; + struct curl_httppost *lastptr = nullptr; + + for (Common::HashMap<Common::String, Common::String>::iterator i = formFields.begin(); i != formFields.end(); ++i) { + CURLFORMcode code = curl_formadd( + &formpost, + &lastptr, + CURLFORM_COPYNAME, i->_key.c_str(), + CURLFORM_COPYCONTENTS, i->_value.c_str(), + CURLFORM_END + ); + + if (code != CURL_FORMADD_OK) + warning("NetworkReadStream: field curl_formadd('%s') failed", i->_key.c_str()); + } + + for (Common::HashMap<Common::String, Common::String>::iterator i = formFiles.begin(); i != formFiles.end(); ++i) { + CURLFORMcode code = curl_formadd( + &formpost, + &lastptr, + CURLFORM_COPYNAME, i->_key.c_str(), + CURLFORM_FILE, i->_value.c_str(), + CURLFORM_END + ); + + if (code != CURL_FORMADD_OK) + warning("NetworkReadStream: file curl_formadd('%s') failed", i->_key.c_str()); + } + + curl_easy_setopt(_easy, CURLOPT_HTTPPOST, formpost); + + ConnMan.registerEasyHandle(_easy); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, Common::String postFields, bool uploading, bool usingPatch) { + init(url, headersList, (const byte *)postFields.c_str(), postFields.size(), uploading, usingPatch, false); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles) { + init(url, headersList, formFields, formFiles); +} + +NetworkReadStream::NetworkReadStream(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post) { + init(url, headersList, buffer, bufferSize, uploading, usingPatch, post); +} + +NetworkReadStream::~NetworkReadStream() { + if (_easy) + curl_easy_cleanup(_easy); +} + +bool NetworkReadStream::eos() const { + return _eos; +} + +uint32 NetworkReadStream::read(void *dataPtr, uint32 dataSize) { + uint32 actuallyRead = MemoryReadWriteStream::read(dataPtr, dataSize); + + if (actuallyRead == 0) { + if (_requestComplete) + _eos = true; + return 0; + } + + return actuallyRead; +} + +void NetworkReadStream::finished() { + _requestComplete = true; +} + +long NetworkReadStream::httpResponseCode() const { + long responseCode = -1; + if (_easy) + curl_easy_getinfo(_easy, CURLINFO_RESPONSE_CODE, &responseCode); + return responseCode; +} + +Common::String NetworkReadStream::currentLocation() const { + Common::String result = ""; + if (_easy) { + char *pointer; + curl_easy_getinfo(_easy, CURLINFO_EFFECTIVE_URL, &pointer); + result = Common::String(pointer); + } + return result; +} + +Common::String NetworkReadStream::responseHeaders() const { + return _responseHeaders; +} + +uint32 NetworkReadStream::fillWithSendingContents(char *bufferToFill, uint32 maxSize) { + uint32 size = _sendingContentsSize - _sendingContentsPos; + if (size > maxSize) + size = maxSize; + for (uint32 i = 0; i < size; ++i) { + bufferToFill[i] = _sendingContentsBuffer[_sendingContentsPos + i]; + } + _sendingContentsPos += size; + return size; +} + +uint32 NetworkReadStream::addResponseHeaders(char *buffer, uint32 size) { + _responseHeaders += Common::String(buffer, size); + return size; +} + +double NetworkReadStream::getProgress() const { + if (_progressTotal < 1) + return 0; + return (double)_progressDownloaded / (double)_progressTotal; +} + +void NetworkReadStream::setProgress(uint64 downloaded, uint64 total) { + _progressDownloaded = downloaded; + _progressTotal = total; +} + +} // End of namespace Cloud diff --git a/backends/networking/curl/networkreadstream.h b/backends/networking/curl/networkreadstream.h new file mode 100644 index 0000000000..62a3e41d9b --- /dev/null +++ b/backends/networking/curl/networkreadstream.h @@ -0,0 +1,142 @@ +/* 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 BACKENDS_NETWORKING_CURL_NETWORKREADSTREAM_H +#define BACKENDS_NETWORKING_CURL_NETWORKREADSTREAM_H + +#include "common/memstream.h" +#include "common/stream.h" +#include "common/str.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +typedef void CURL; +struct curl_slist; + +namespace Networking { + +class NetworkReadStream: public Common::MemoryReadWriteStream { + CURL *_easy; + bool _eos, _requestComplete; + const byte *_sendingContentsBuffer; + uint32 _sendingContentsSize; + uint32 _sendingContentsPos; + Common::String _responseHeaders; + uint64 _progressDownloaded, _progressTotal; + void init(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post); + void init(const char *url, curl_slist *headersList, Common::HashMap<Common::String, Common::String> formFields, Common::HashMap<Common::String, Common::String> formFiles); + +public: + /** Send <postFields>, using POST by default. */ + NetworkReadStream(const char *url, curl_slist *headersList, Common::String postFields, bool uploading = false, bool usingPatch = false); + /** Send <formFields>, <formFiles>, using POST multipart/form. */ + NetworkReadStream( + const char *url, curl_slist *headersList, + Common::HashMap<Common::String, Common::String> formFields, + Common::HashMap<Common::String, Common::String> formFiles); + /** Send <buffer, using POST by default. */ + NetworkReadStream(const char *url, curl_slist *headersList, const byte *buffer, uint32 bufferSize, bool uploading = false, bool usingPatch = false, bool post = true); + virtual ~NetworkReadStream(); + + /** + * Returns true if a read failed because the stream end has been reached. + * This flag is cleared by clearErr(). + * For a SeekableReadStream, it is also cleared by a successful seek. + * + * @note The semantics of any implementation of this method are + * supposed to match those of ISO C feof(). In particular, in a stream + * with N bytes, reading exactly N bytes from the start should *not* + * set eos; only reading *beyond* the available data should set it. + */ + virtual bool eos() const; + + /** + * Read data from the stream. Subclasses must implement this + * method; all other read methods are implemented using it. + * + * @note The semantics of any implementation of this method are + * supposed to match those of ISO C fread(), in particular where + * it concerns setting error and end of file/stream flags. + * + * @param dataPtr pointer to a buffer into which the data is read + * @param dataSize number of bytes to be read + * @return the number of bytes which were actually read. + */ + virtual uint32 read(void *dataPtr, uint32 dataSize); + + /** + * This method is called by ConnectionManager to indicate + * that transfer is finished. + * + * @note It's called on failure too. + */ + void finished(); + + /** + * Returns HTTP response code from inner CURL handle. + * It returns -1 to indicate there is no inner handle. + * + * @note This method should be called when eos() == true. + */ + long httpResponseCode() const; + + /** + * Return current location URL from inner CURL handle. + * "" is returned to indicate there is no inner handle. + * + * @note This method should be called when eos() == true. + */ + Common::String currentLocation() const; + + /** + * Return response headers. + * + * @note This method should be called when eos() == true. + */ + Common::String responseHeaders() const; + + /** + * Fills the passed buffer with _sendingContentsBuffer contents. + * It works similarly to read(), expect it's not for reading + * Stream's contents, but for sending our own data to the server. + * + * @returns how many bytes were actually read (filled in) + */ + uint32 fillWithSendingContents(char *bufferToFill, uint32 maxSize); + + /** + * Remembers headers returned to CURL in server's response. + * + * @returns how many bytes were actually read + */ + uint32 addResponseHeaders(char *buffer, uint32 size); + + /** Returns a number in range [0, 1], where 1 is "complete". */ + double getProgress() const; + + /** Used in curl progress callback to pass current downloaded/total values. */ + void setProgress(uint64 downloaded, uint64 total); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/curl/request.cpp b/backends/networking/curl/request.cpp new file mode 100644 index 0000000000..398af04f4b --- /dev/null +++ b/backends/networking/curl/request.cpp @@ -0,0 +1,74 @@ +/* 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 "backends/networking/curl/request.h" + +namespace Networking { + +ErrorResponse::ErrorResponse(Request *rq): + request(rq), interrupted(false), failed(true), response(""), httpResponseCode(-1) {} + +ErrorResponse::ErrorResponse(Request *rq, bool interrupt, bool failure, Common::String resp, long httpCode): + request(rq), interrupted(interrupt), failed(failure), response(resp), httpResponseCode(httpCode) {} + +Request::Request(DataCallback cb, ErrorCallback ecb): + _callback(cb), _errorCallback(ecb), _state(PROCESSING), _retryInSeconds(0) {} + +Request::~Request() { + delete _callback; + delete _errorCallback; +} + +void Request::handleRetry() { + if (_retryInSeconds > 0) { + --_retryInSeconds; + } else { + _state = PROCESSING; + restart(); + } +} + +void Request::pause() { _state = PAUSED; } + +void Request::finish() { + ErrorResponse error(this, true, false, "", -1); + finishError(error); +} + +void Request::retry(uint32 seconds) { + _state = RETRY; + _retryInSeconds = seconds; +} + +RequestState Request::state() const { return _state; } + +Common::String Request::date() const { return ""; } + +void Request::finishError(ErrorResponse error) { + _state = FINISHED; + if (_errorCallback) + (*_errorCallback)(error); +} + +void Request::finishSuccess() { _state = FINISHED; } + +} // End of namespace Networking diff --git a/backends/networking/curl/request.h b/backends/networking/curl/request.h new file mode 100644 index 0000000000..b1261f23ee --- /dev/null +++ b/backends/networking/curl/request.h @@ -0,0 +1,203 @@ +/* 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 BACKENDS_NETWORKING_CURL_REQUEST_H +#define BACKENDS_NETWORKING_CURL_REQUEST_H + +#include "common/callback.h" +#include "common/scummsys.h" +#include "common/str.h" + +namespace Networking { + +class Request; + +/** + * Response<T> is a struct to be returned from Request + * to user's callbacks. It's a type safe way to indicate + * which "return value" Request has and user awaits. + * + * It just keeps a Request pointer together with + * some T value (which might be a pointer, a reference + * or a plain type (copied by value)). + * + * To make it more convenient, typedefs are used. + * For example, Response<void *> is called DataResponse + * and corresponding callback pointer is DataCallback. + */ + +template<typename T> struct Response { + Request *request; + T value; + + Response(Request *rq, T v) : request(rq), value(v) {} +}; + +/** + * ErrorResponse is a struct to be returned from Request + * to user's failure callbacks. + * + * It keeps a Request pointer together with some useful + * information fields, which would explain why failure + * callback was called. + * + * <interrupted> flag is set when Request was interrupted, + * i.e. finished by user with finish() call. + * + * <failed> flag is set when Request has failed because of + * some error (bad server response, for example). + * + * <response> contains server's original response. + * + * <httpResponseCode> contains server's HTTP response code. + */ + +struct ErrorResponse { + Request *request; + bool interrupted; + bool failed; + Common::String response; + long httpResponseCode; + + ErrorResponse(Request *rq); + ErrorResponse(Request *rq, bool interrupt, bool failure, Common::String resp, long httpCode); +}; + +typedef Response<void *> DataReponse; +typedef Common::BaseCallback<DataReponse> *DataCallback; +typedef Common::BaseCallback<ErrorResponse> *ErrorCallback; + +/** + * RequestState is used to indicate current Request state. + * ConnectionManager uses it to decide what to do with the Request. + * + * PROCESSING state indicates that Request is working. + * ConnectionManager calls handle() method of Requests in that state. + * + * PAUSED state indicates that Request is not working. + * ConnectionManager keeps Requests in that state and doesn't call any methods of those. + * + * RETRY state indicates that Request must restart after a few seconds. + * ConnectionManager calls handleRetry() method of Requests in that state. + * Default handleRetry() implementation decreases _retryInSeconds value + * until it reaches zero. When it does, Request's restart() method is called. + * + * FINISHED state indicates that Request did the work and might be deleted. + * ConnectionManager deletes Requests in that state. + * After this state is set, but before ConnectionManager deletes the Request, + * Request calls user's callback. User can ask Request to change its state + * by calling retry() or pause() methods and Request won't be deleted. + * + * Request get a success and failure callbacks. Request must call one + * (and only one!) of these callbacks when it sets FINISHED state. + */ +enum RequestState { + PROCESSING, + PAUSED, + RETRY, + FINISHED +}; + +class Request { +protected: + /** + * Callback, which should be called when Request is finished. + * That's the way Requests pass the result to the code which asked to create this request. + * + * @note some Requests use their own callbacks to return something but void *. + * @note callback must be called in finish() or similar method. + */ + DataCallback _callback; + + /** + * Callback, which should be called when Request is failed/interrupted. + * That's the way Requests pass error information to the code which asked to create this request. + * @note callback must be called in finish() or similar method. + */ + ErrorCallback _errorCallback; + + /** + * Request state, which is used by ConnectionManager to determine + * whether request might be deleted or it's still working. + * + * State might be changed from outside with finish(), pause() or + * retry() methods. Override these if you want to react to these + * changes correctly. + */ + RequestState _state; + + /** In RETRY state this indicates whether it's time to call restart(). */ + uint32 _retryInSeconds; + + /** Sets FINISHED state and calls the _errorCallback with given error. */ + virtual void finishError(ErrorResponse error); + + /** Sets FINISHED state. Implementations might extend it if needed. */ + virtual void finishSuccess(); + +public: + Request(DataCallback cb, ErrorCallback ecb); + virtual ~Request(); + + /** Method, which does actual work. Depends on what this Request is doing. */ + virtual void handle() = 0; + + /** Method, which is called by ConnectionManager when Request's state is RETRY. */ + virtual void handleRetry(); + + /** Method, which is used to restart the Request. */ + virtual void restart() = 0; + + /** Method, which is called to pause the Request. */ + virtual void pause(); + + /** + * Method, which is called to *interrupt* the Request. + * When it's called, Request must stop its work and + * call the failure callback to notify user. + */ + virtual void finish(); + + /** Method, which is called to retry the Request. */ + virtual void retry(uint32 seconds); + + /** Returns Request's current state. */ + RequestState state() const; + + /** + * Return date this Request received from server. + * It could be extracted from "Date" header, + * which is kept in NetworkReadStream. + * + * @note not all Requests do that, so "" is returned + * to indicate the date is unknown. That's also true + * if no server response available or no "Date" header + * was passed. + * + * @returns date from "Date" response header. + */ + virtual Common::String date() const; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/client.cpp b/backends/networking/sdl_net/client.cpp new file mode 100644 index 0000000000..fbb5fadf35 --- /dev/null +++ b/backends/networking/sdl_net/client.cpp @@ -0,0 +1,190 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/sdl_net/client.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/memstream.h" +#include <SDL_net.h> + +namespace Networking { + +Client::Client(): + _state(INVALID), _set(nullptr), _socket(nullptr), _handler(nullptr), + _previousHandler(nullptr), _stream(nullptr), _buffer(new byte[CLIENT_BUFFER_SIZE]) {} + +Client::Client(SDLNet_SocketSet set, TCPsocket socket): + _state(INVALID), _set(nullptr), _socket(nullptr), _handler(nullptr), + _previousHandler(nullptr), _stream(nullptr), _buffer(new byte[CLIENT_BUFFER_SIZE]) { + open(set, socket); +} + +Client::~Client() { + close(); + delete[] _buffer; +} + +void Client::open(SDLNet_SocketSet set, TCPsocket socket) { + if (_state != INVALID) + close(); + _state = READING_HEADERS; + _socket = socket; + _set = set; + Reader cleanReader; + _reader = cleanReader; + if (_handler) + delete _handler; + _handler = nullptr; + if (_previousHandler) + delete _previousHandler; + _previousHandler = nullptr; + _stream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + if (set) { + int numused = SDLNet_TCP_AddSocket(set, socket); + if (numused == -1) { + error("Client: SDLNet_AddSocket: %s\n", SDLNet_GetError()); + } + } +} + +bool Client::readMoreIfNeeded() { + if (_stream == nullptr) + return false; //nothing to read into + if (_stream->size() - _stream->pos() > 0) + return true; //not needed, some data left in the stream + if (!_socket) + return false; + if (!SDLNet_SocketReady(_socket)) + return false; + + int bytes = SDLNet_TCP_Recv(_socket, _buffer, CLIENT_BUFFER_SIZE); + if (bytes <= 0) { + warning("Client::readMoreIfNeeded: recv fail"); + close(); + return false; + } + + if (_stream->write(_buffer, bytes) != bytes) { + warning("Client::readMoreIfNeeded: failed to write() into MemoryReadWriteStream"); + close(); + return false; + } + + return true; +} + +void Client::readHeaders() { + if (!readMoreIfNeeded()) + return; + _reader.setContent(_stream); + if (_reader.readFirstHeaders()) + _state = (_reader.badRequest() ? BAD_REQUEST : READ_HEADERS); +} + +bool Client::readContent(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readFirstContent(stream); +} + +bool Client::readBlockHeaders(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readBlockHeaders(stream); +} + +bool Client::readBlockContent(Common::WriteStream *stream) { + if (!readMoreIfNeeded()) + return false; + _reader.setContent(_stream); + return _reader.readBlockContent(stream); +} + +void Client::setHandler(ClientHandler *handler) { + if (_handler) { + if (_previousHandler) + delete _previousHandler; + _previousHandler = _handler; //can't just delete it, as setHandler() could've been called by handler itself + } + _state = BEING_HANDLED; + _handler = handler; +} + +void Client::handle() { + if (_state != BEING_HANDLED) + warning("handle() called in a wrong Client's state"); + if (!_handler) + warning("Client doesn't have handler to be handled by"); + if (_handler) + _handler->handle(this); +} + +void Client::close() { + if (_set) { + if (_socket) { + int numused = SDLNet_TCP_DelSocket(_set, _socket); + if (numused == -1) + error("Client: SDLNet_DelSocket: %s\n", SDLNet_GetError()); + } + _set = nullptr; + } + + if (_socket) { + SDLNet_TCP_Close(_socket); + _socket = nullptr; + } + + if (_stream) { + delete _stream; + _stream = nullptr; + } + + _state = INVALID; +} + + +ClientState Client::state() const { return _state; } + +Common::String Client::headers() const { return _reader.headers(); } + +Common::String Client::method() const { return _reader.method(); } + +Common::String Client::path() const { return _reader.path(); } + +Common::String Client::query() const { return _reader.query(); } + +Common::String Client::queryParameter(Common::String name) const { return _reader.queryParameter(name); } + +Common::String Client::anchor() const { return _reader.anchor(); } + +bool Client::noMoreContent() const { return _reader.noMoreContent(); } + +bool Client::socketIsReady() { return SDLNet_SocketReady(_socket); } + +int Client::recv(void *data, int maxlen) { return SDLNet_TCP_Recv(_socket, data, maxlen); } + +int Client::send(void *data, int len) { return SDLNet_TCP_Send(_socket, data, len); } + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/client.h b/backends/networking/sdl_net/client.h new file mode 100644 index 0000000000..254ace584a --- /dev/null +++ b/backends/networking/sdl_net/client.h @@ -0,0 +1,125 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_CLIENT_H +#define BACKENDS_NETWORKING_SDL_NET_CLIENT_H + +#include "backends/networking/sdl_net/reader.h" +#include "common/str.h" + +namespace Common { +class MemoryReadWriteStream; +} + +typedef struct _SDLNet_SocketSet *SDLNet_SocketSet; +typedef struct _TCPsocket *TCPsocket; + +namespace Networking { + +enum ClientState { + INVALID, + READING_HEADERS, + READ_HEADERS, + BAD_REQUEST, + BEING_HANDLED +}; + +class Client; + +#define CLIENT_BUFFER_SIZE 1 * 1024 * 1024 + +class ClientHandler { +public: + virtual ~ClientHandler() {}; + virtual void handle(Client *client) = 0; +}; + +/** + * Client class represents one client's HTTP request + * to the LocalWebserver. + * + * While in READING_HEADERS state, it's kept in LocalWebserver. + * Client must read the headers and decide whether it's + * READ_HEADERS (could be handled) or BAD_REQUEST (failed). + * + * If it's READ_HEADERS, LocalWebserver searches for a corresponding + * BaseHandler. These classes use the information from headers - + * like method, path, GET parameters - to build the response + * for this client's request. When they do, they call setHandler() + * and pass a special ClientHandler. Client becomes BEING_HANDLED. + * + * While in that state, LocalWebserver calls Client's handle() and + * it's passed to ClientHandler. The latter does the job: it commands + * Client to read or write bytes with its socket or calls + * readContent() methods, so Client reads the request through Reader. + */ + +class Client { + ClientState _state; + SDLNet_SocketSet _set; + TCPsocket _socket; + Reader _reader; + ClientHandler *_handler, *_previousHandler; + Common::MemoryReadWriteStream *_stream; + byte *_buffer; + + bool readMoreIfNeeded(); + +public: + Client(); + Client(SDLNet_SocketSet set, TCPsocket socket); + virtual ~Client(); + + void open(SDLNet_SocketSet set, TCPsocket socket); + void readHeaders(); + bool readContent(Common::WriteStream *stream); + bool readBlockHeaders(Common::WriteStream *stream); + bool readBlockContent(Common::WriteStream *stream); + void setHandler(ClientHandler *handler); + void handle(); + void close(); + + ClientState state() const; + Common::String headers() const; + Common::String method() const; + Common::String path() const; + Common::String query() const; + Common::String queryParameter(Common::String name) const; + Common::String anchor() const; + + bool noMoreContent() const; + + /** + * Return SDLNet_SocketReady(_socket). + * + * It's "ready" when it has something + * to read (recv()). You can send() + * when this is false. + */ + bool socketIsReady(); + int recv(void *data, int maxlen); + int send(void *data, int len); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/getclienthandler.cpp b/backends/networking/sdl_net/getclienthandler.cpp new file mode 100644 index 0000000000..b22c80cebf --- /dev/null +++ b/backends/networking/sdl_net/getclienthandler.cpp @@ -0,0 +1,162 @@ +/* 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 "backends/networking/sdl_net/getclienthandler.h" +#include "common/textconsole.h" + +namespace Networking { + +GetClientHandler::GetClientHandler(Common::SeekableReadStream *stream): + _responseCode(200), _headersPrepared(false), + _stream(stream), _buffer(new byte[CLIENT_HANDLER_BUFFER_SIZE]) {} + +GetClientHandler::~GetClientHandler() { + delete _stream; + delete[] _buffer; +} + +const char *GetClientHandler::responseMessage(long responseCode) { + switch (responseCode) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 226: return "IM Used"; + + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Moved Temporarily"; //case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 306: return "RESERVED"; + case 307: return "Temporary Redirect"; + + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Large"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 425: return "Unordered Collection"; + case 426: return "Upgrade Required"; + case 428: return "Precondition Required"; + case 429: return "Too Many Requests"; + case 431: return "Request Header Fields Too Large"; + case 434: return "Requested Host Unavailable"; + case 449: return "Retry With"; + case 451: return "Unavailable For Legal Reasons"; + + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 506: return "Variant Also Negotiates"; + case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 509: return "Bandwidth Limit Exceeded"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; + } + return "Unknown"; +} + +void GetClientHandler::prepareHeaders() { + if (!_specialHeaders.contains("Content-Type")) + setHeader("Content-Type", "text/html"); + + if (!_specialHeaders.contains("Content-Length") && _stream) + setHeader("Content-Length", Common::String::format("%u", _stream->size())); + + _headers = Common::String::format("HTTP/1.1 %ld %s\r\n", _responseCode, responseMessage(_responseCode)); + for (Common::HashMap<Common::String, Common::String>::iterator i = _specialHeaders.begin(); i != _specialHeaders.end(); ++i) + _headers += i->_key + ": " + i->_value + "\r\n"; + _headers += "\r\n"; + + _headersPrepared = true; +} + +void GetClientHandler::handle(Client *client) { + if (!client) + return; + if (!_headersPrepared) + prepareHeaders(); + + uint32 readBytes; + + // send headers first + if (_headers.size() > 0) { + readBytes = _headers.size(); + if (readBytes > CLIENT_HANDLER_BUFFER_SIZE) + readBytes = CLIENT_HANDLER_BUFFER_SIZE; + memcpy(_buffer, _headers.c_str(), readBytes); + _headers.erase(0, readBytes); + } else { + if (!_stream) { + client->close(); + return; + } + + readBytes = _stream->read(_buffer, CLIENT_HANDLER_BUFFER_SIZE); + } + + if (readBytes != 0) + if (client->send(_buffer, readBytes) != readBytes) { + warning("GetClientHandler: unable to send all bytes to the client"); + client->close(); + return; + } + + // we're done here! + if (_stream->eos()) + client->close(); +} + +void GetClientHandler::setHeader(Common::String name, Common::String value) { _specialHeaders[name] = value; } +void GetClientHandler::setResponseCode(long code) { _responseCode = code; } + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/getclienthandler.h b/backends/networking/sdl_net/getclienthandler.h new file mode 100644 index 0000000000..fa34b8555b --- /dev/null +++ b/backends/networking/sdl_net/getclienthandler.h @@ -0,0 +1,57 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_GETCLIENTHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_GETCLIENTHANDLER_H + +#include "backends/networking/sdl_net/client.h" +#include "common/hashmap.h" +#include "common/stream.h" +#include "common/hash-str.h" + +namespace Networking { + +#define CLIENT_HANDLER_BUFFER_SIZE 1 * 1024 * 1024 + +class GetClientHandler: public ClientHandler { + Common::HashMap<Common::String, Common::String> _specialHeaders; + long _responseCode; + bool _headersPrepared; + Common::String _headers; + Common::SeekableReadStream *_stream; + byte *_buffer; + + static const char *responseMessage(long responseCode); + void prepareHeaders(); + +public: + GetClientHandler(Common::SeekableReadStream *stream); + virtual ~GetClientHandler(); + + virtual void handle(Client *client); + void setHeader(Common::String name, Common::String value); + void setResponseCode(long code); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/basehandler.h b/backends/networking/sdl_net/handlers/basehandler.h new file mode 100644 index 0000000000..060fa70b14 --- /dev/null +++ b/backends/networking/sdl_net/handlers/basehandler.h @@ -0,0 +1,41 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_BASEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_BASEHANDLER_H + +#include "backends/networking/sdl_net/client.h" + +namespace Networking { + +class BaseHandler { +public: + BaseHandler() {} + virtual ~BaseHandler() {} + + virtual void handle(Client &) = 0; + virtual bool minimalModeSupported() { return false; } +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp b/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp new file mode 100644 index 0000000000..d315ad2ee3 --- /dev/null +++ b/backends/networking/sdl_net/handlers/createdirectoryhandler.cpp @@ -0,0 +1,129 @@ +/* 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 "backends/networking/sdl_net/handlers/createdirectoryhandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" +#include <common/callback.h> + +namespace Networking { + +CreateDirectoryHandler::CreateDirectoryHandler() {} + +CreateDirectoryHandler::~CreateDirectoryHandler() {} + +void CreateDirectoryHandler::handleError(Client &client, Common::String message) const { + if (client.queryParameter("answer_json") == "true") + setJsonResponseHandler(client, "error", message); + else + HandlerUtils::setFilesManagerErrorMessageHandler(client, message); +} + +void CreateDirectoryHandler::setJsonResponseHandler(Client &client, Common::String type, Common::String message) const { + Common::JSONObject response; + response.setVal("type", new Common::JSONValue(type)); + response.setVal("message", new Common::JSONValue(message)); + + Common::JSONValue json = response; + LocalWebserver::setClientGetHandler(client, json.stringify(true)); +} + +/// public + +void CreateDirectoryHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + Common::String name = client.queryParameter("directory_name"); + + // check that <path> is not an absolute root + if (path == "" || path == "/") { + handleError(client, _("Can't create directory here!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + handleError(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd) || path.empty()) { + handleError(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + handleError(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + handleError(client, _("Parent directory doesn't exists!")); + return; + } + if (!node->isDirectory()) { + handleError(client, _("Can't create a directory within a file!")); + return; + } + + // check that <directory_name> doesn't exist or is directory + if (path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + node = g_system->getFilesystemFactory()->makeFileNodePath(path + name); + if (node->exists()) { + if (!node->isDirectory()) { + handleError(client, _("There is a file with that name in the parent directory!")); + return; + } + } else { + // create the <directory_name> in <path> + if (!node->create(true)) { + handleError(client, _("Failed to create the directory!")); + return; + } + } + + // if json requested, respond with it + if (client.queryParameter("answer_json") == "true") { + setJsonResponseHandler(client, "success", _("Directory created successfully!")); + return; + } + + // set redirect on success + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files?path=%s\">%s</a>", + _("Directory created successfully!"), + client.queryParameter("path").c_str(), + _("Back to parent directory") + ), + (client.queryParameter("ajax") == "true" ? "/filesAJAX?path=" : "/files?path=") + + LocalWebserver::urlEncodeQueryParameterValue(client.queryParameter("path")) + ); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/createdirectoryhandler.h b/backends/networking/sdl_net/handlers/createdirectoryhandler.h new file mode 100644 index 0000000000..3991686fdd --- /dev/null +++ b/backends/networking/sdl_net/handlers/createdirectoryhandler.h @@ -0,0 +1,42 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_CREATEDIRECTORYHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_CREATEDIRECTORYHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class CreateDirectoryHandler: public FilesBaseHandler { + void handleError(Client &client, Common::String message) const; + void setJsonResponseHandler(Client &client, Common::String type, Common::String message) const; +public: + CreateDirectoryHandler(); + virtual ~CreateDirectoryHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/downloadfilehandler.cpp b/backends/networking/sdl_net/handlers/downloadfilehandler.cpp new file mode 100644 index 0000000000..281be2d27f --- /dev/null +++ b/backends/networking/sdl_net/handlers/downloadfilehandler.cpp @@ -0,0 +1,88 @@ +/* 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 "backends/networking/sdl_net/handlers/downloadfilehandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/getclienthandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" + +namespace Networking { + +DownloadFileHandler::DownloadFileHandler() {} + +DownloadFileHandler::~DownloadFileHandler() {} + +/// public + +void DownloadFileHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + + // check that <path> is not an absolute root + if (path == "" || path == "/") { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd, false) || path.empty()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The file doesn't exist!")); + return; + } + if (node->isDirectory()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Can't download a directory!")); + return; + } + Common::SeekableReadStream *stream = node->createReadStream(); + if (stream == nullptr) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Failed to read the file!")); + return; + } + + GetClientHandler *handler = new GetClientHandler(stream); + handler->setResponseCode(200); + handler->setHeader("Content-Type", "application/force-download"); + handler->setHeader("Content-Disposition", "attachment; filename=\"" + node->getDisplayName() + "\""); + handler->setHeader("Content-Transfer-Encoding", "binary"); + client.setHandler(handler); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/downloadfilehandler.h b/backends/networking/sdl_net/handlers/downloadfilehandler.h new file mode 100644 index 0000000000..80ee779d08 --- /dev/null +++ b/backends/networking/sdl_net/handlers/downloadfilehandler.h @@ -0,0 +1,40 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_DOWNLOADFILEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_DOWNLOADFILEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class DownloadFileHandler: public FilesBaseHandler { +public: + DownloadFileHandler(); + virtual ~DownloadFileHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp b/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp new file mode 100644 index 0000000000..55b7a62dd1 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesajaxpagehandler.cpp @@ -0,0 +1,81 @@ +/* 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 "backends/networking/sdl_net/handlers/filesajaxpagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" + +namespace Networking { + +#define FILES_PAGE_NAME ".filesAJAX.html" + +FilesAjaxPageHandler::FilesAjaxPageHandler() {} + +FilesAjaxPageHandler::~FilesAjaxPageHandler() {} + +namespace { + +Common::String encodeDoubleQuotesAndSlashes(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '"') { + result += "\\\""; + } else if (s[i] == '\\') { + result += "\\\\"; + } else { + result += s[i]; + } + return result; +} + +} + +/// public + +void FilesAjaxPageHandler::handle(Client &client) { + // load stylish response page from the archive + Common::SeekableReadStream *const stream = HandlerUtils::getArchiveFile(FILES_PAGE_NAME); + if (stream == nullptr) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The page is not available without the resources.")); + return; + } + + Common::String response = HandlerUtils::readEverythingFromStream(stream); + Common::String path = client.queryParameter("path"); + + //these occur twice: + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{upload_files_button}", _("Upload files")); //tab + replace(response, "{upload_file_button}", _("Upload files")); //button in the tab + replace(response, "{create_directory_desc}", _("Type new directory name:")); + replace(response, "{upload_file_desc}", _("Select a file to upload:")); + replace(response, "{or_upload_directory_desc}", _("Or select a directory (works in Chrome only):")); + replace(response, "{index_of}", _("Index of ")); + replace(response, "{loading}", _("Loading...")); + replace(response, "{error}", _("Error occurred")); + replace(response, "{start_path}", encodeDoubleQuotesAndSlashes(path)); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filesajaxpagehandler.h b/backends/networking/sdl_net/handlers/filesajaxpagehandler.h new file mode 100644 index 0000000000..142376b3ff --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesajaxpagehandler.h @@ -0,0 +1,40 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_FILESAJAXPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESAJAXPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class FilesAjaxPageHandler: public FilesBaseHandler { +public: + FilesAjaxPageHandler(); + virtual ~FilesAjaxPageHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filesbasehandler.cpp b/backends/networking/sdl_net/handlers/filesbasehandler.cpp new file mode 100644 index 0000000000..f12f88a308 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesbasehandler.cpp @@ -0,0 +1,91 @@ +/* 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 "backends/networking/sdl_net/handlers/filesbasehandler.h" +#include "backends/saves/default/default-saves.h" +#include "common/config-manager.h" +#include "common/system.h" + +namespace Networking { + +FilesBaseHandler::FilesBaseHandler() {} + +FilesBaseHandler::~FilesBaseHandler() {} + +Common::String FilesBaseHandler::parentPath(Common::String path) { + if (path.size() && (path.lastChar() == '/' || path.lastChar() == '\\')) path.deleteLastChar(); + if (!path.empty()) { + for (int i = path.size() - 1; i >= 0; --i) + if (i == 0 || path[i] == '/' || path[i] == '\\') { + path.erase(i); + break; + } + } + if (path.size() && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + return path; +} + +bool FilesBaseHandler::transformPath(Common::String &path, Common::String &prefixToRemove, Common::String &prefixToAdd, bool isDirectory) { + // <path> is not empty, but could lack the trailing slash + if (isDirectory && path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + + if (path.hasPrefix("/root") && ConfMan.hasKey("rootpath", "cloud")) { + prefixToAdd = "/root/"; + prefixToRemove = ConfMan.get("rootpath", "cloud"); + if (prefixToRemove.size() && prefixToRemove.lastChar() != '/' && prefixToRemove.lastChar() != '\\') + prefixToRemove += '/'; + if (prefixToRemove == "/") prefixToRemove = ""; + path.erase(0, 5); + if (path.size() && (path[0] == '/' || path[0] == '\\')) + path.deleteChar(0); // if that was "/root/ab/c", it becomes "/ab/c", but we need "ab/c" + path = prefixToRemove + path; + if (path == "") + path = "/"; // absolute root is '/' + return true; + } + + if (path.hasPrefix("/saves")) { + prefixToAdd = "/saves/"; + + // determine savepath (prefix to remove) +#ifdef USE_LIBCURL + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + prefixToRemove = (manager ? manager->concatWithSavesPath("") : ConfMan.get("savepath")); +#else + prefixToRemove = ConfMan.get("savepath"); +#endif + if (prefixToRemove.size() && prefixToRemove.lastChar() != '/' && prefixToRemove.lastChar() != '\\') + prefixToRemove += '/'; + + path.erase(0, 6); + if (path.size() && (path[0] == '/' || path[0] == '\\')) + path.deleteChar(0); + path = prefixToRemove + path; + return true; + } + + return false; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filesbasehandler.h b/backends/networking/sdl_net/handlers/filesbasehandler.h new file mode 100644 index 0000000000..c30d91d341 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filesbasehandler.h @@ -0,0 +1,52 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_FILESBASEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESBASEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" + +namespace Networking { + +class FilesBaseHandler: public BaseHandler { +protected: + Common::String parentPath(Common::String path); + + /** + * Transforms virtual <path> into actual file system path. + * + * Fills prefixes with actual file system prefix ("to remove") + * and virtual path prefix ("to add"). + * + * Returns true on success. + */ + bool transformPath(Common::String &path, Common::String &prefixToRemove, Common::String &prefixToAdd, bool isDirectory = true); +public: + FilesBaseHandler(); + virtual ~FilesBaseHandler(); + + virtual void handle(Client &client) = 0; +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/filespagehandler.cpp b/backends/networking/sdl_net/handlers/filespagehandler.cpp new file mode 100644 index 0000000000..01b40c3bcb --- /dev/null +++ b/backends/networking/sdl_net/handlers/filespagehandler.cpp @@ -0,0 +1,237 @@ +/* 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 "backends/networking/sdl_net/handlers/filespagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/config-manager.h" +#include "common/translation.h" + +namespace Networking { + +#define INDEX_PAGE_NAME ".index.html" +#define FILES_PAGE_NAME ".files.html" + +FilesPageHandler::FilesPageHandler() {} + +FilesPageHandler::~FilesPageHandler() {} + +namespace { +Common::String encodeDoubleQuotes(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '"') { + result += "\\\""; + } else { + result += s[i]; + } + return result; +} + +Common::String encodeHtmlEntities(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '<') + result += "<"; + else if (s[i] == '>') + result += ">"; + else if (s[i] == '&') + result += "&"; + else if (s[i] > 0x7F) + result += Common::String::format("&#%d;", (int)s[i]); + else result += s[i]; + return result; +} + +Common::String getDisplayPath(Common::String s) { + Common::String result = ""; + for (uint32 i = 0; i < s.size(); ++i) + if (s[i] == '\\') + result += '/'; + else + result += s[i]; + if (result == "") + return "/"; + return result; +} +} + +bool FilesPageHandler::listDirectory(Common::String path, Common::String &content, const Common::String &itemTemplate) { + if (path == "" || path == "/") { + if (ConfMan.hasKey("rootpath", "cloud")) + addItem(content, itemTemplate, IT_DIRECTORY, "/root/", _("File system root")); + addItem(content, itemTemplate, IT_DIRECTORY, "/saves/", _("Saved games")); + return true; + } + + if (HandlerUtils::hasForbiddenCombinations(path)) + return false; + + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd)) + return false; + + Common::FSNode node = Common::FSNode(path); + if (path == "/") + node = node.getParent(); // absolute root + + if (!HandlerUtils::permittedPath(node.getPath())) + return false; + + if (!node.isDirectory()) + return false; + + // list directory + Common::FSList _nodeContent; + if (!node.getChildren(_nodeContent, Common::FSNode::kListAll, false)) // do not show hidden files + _nodeContent.clear(); + else + Common::sort(_nodeContent.begin(), _nodeContent.end()); + + // add parent directory link + { + Common::String filePath = path; + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + if (filePath == "" || filePath == "/" || filePath == "\\") + filePath = "/"; + else + filePath = parentPath(prefixToAdd + filePath); + addItem(content, itemTemplate, IT_PARENT_DIRECTORY, filePath, _("Parent directory")); + } + + // fill the content + for (Common::FSList::iterator i = _nodeContent.begin(); i != _nodeContent.end(); ++i) { + Common::String name = i->getDisplayName(); + if (i->isDirectory()) + name += "/"; + + Common::String filePath = i->getPath(); + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + filePath = prefixToAdd + filePath; + + addItem(content, itemTemplate, detectType(i->isDirectory(), name), filePath, name); + } + + return true; +} + +FilesPageHandler::ItemType FilesPageHandler::detectType(bool isDirectory, const Common::String &name) { + if (isDirectory) + return IT_DIRECTORY; + if (name.hasSuffix(".txt")) + return IT_TXT; + if (name.hasSuffix(".zip")) + return IT_ZIP; + if (name.hasSuffix(".7z")) + return IT_7Z; + return IT_UNKNOWN; +} + +void FilesPageHandler::addItem(Common::String &content, const Common::String &itemTemplate, ItemType itemType, Common::String path, Common::String name, Common::String size) const { + Common::String item = itemTemplate, icon; + bool isDirectory = (itemType == IT_DIRECTORY || itemType == IT_PARENT_DIRECTORY); + switch (itemType) { + case IT_DIRECTORY: + icon = "dir.png"; + break; + case IT_PARENT_DIRECTORY: + icon = "up.png"; + break; + case IT_TXT: + icon = "txt.png"; + break; + case IT_ZIP: + icon = "zip.png"; + break; + case IT_7Z: + icon = "7z.png"; + break; + default: + icon = "unk.png"; + } + replace(item, "{icon}", icon); + replace(item, "{link}", (isDirectory ? "files?path=" : "download?path=") + LocalWebserver::urlEncodeQueryParameterValue(path)); + replace(item, "{name}", encodeHtmlEntities(name)); + replace(item, "{size}", size); + content += item; +} + +/// public + +void FilesPageHandler::handle(Client &client) { + Common::String response = + "<html>" \ + "<head><title>ScummVM</title></head>" \ + "<body>" \ + "<p>{create_directory_desc}</p>" \ + "<form action=\"create\">" \ + "<input type=\"hidden\" name=\"path\" value=\"{path}\"/>" \ + "<input type=\"text\" name=\"directory_name\" value=\"\"/>" \ + "<input type=\"submit\" value=\"{create_directory_button}\"/>" \ + "</form>" \ + "<hr/>" \ + "<p>{upload_file_desc}</p>" \ + "<form action=\"upload?path={path}\" method=\"post\" enctype=\"multipart/form-data\">" \ + "<input type=\"file\" name=\"upload_file-f\" allowdirs multiple/>" \ + "<span>{or_upload_directory_desc}</span>" \ + "<input type=\"file\" name=\"upload_file-d\" directory webkitdirectory multiple/>" \ + "<input type=\"submit\" value=\"{upload_file_button}\"/>" \ + "</form>" + "<hr/>" \ + "<h1>{index_of_directory}</h1>" \ + "<table>{content}</table>" \ + "</body>" \ + "</html>"; + Common::String itemTemplate = "<tr><td><img src=\"icons/{icon}\"/></td><td><a href=\"{link}\">{name}</a></td><td>{size}</td></tr>\n"; //TODO: load this template too? + + // load stylish response page from the archive + Common::SeekableReadStream *const stream = HandlerUtils::getArchiveFile(FILES_PAGE_NAME); + if (stream) + response = HandlerUtils::readEverythingFromStream(stream); + + Common::String path = client.queryParameter("path"); + Common::String content = ""; + + // show an error message if failed to list directory + if (!listDirectory(path, content, itemTemplate)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("ScummVM couldn't list the directory you specified.")); + return; + } + + //these occur twice: + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{create_directory_button}", _("Create directory")); + replace(response, "{path}", encodeDoubleQuotes(client.queryParameter("path"))); + replace(response, "{path}", encodeDoubleQuotes(client.queryParameter("path"))); + replace(response, "{upload_files_button}", _("Upload files")); //tab + replace(response, "{upload_file_button}", _("Upload files")); //button in the tab + replace(response, "{create_directory_desc}", _("Type new directory name:")); + replace(response, "{upload_file_desc}", _("Select a file to upload:")); + replace(response, "{or_upload_directory_desc}", _("Or select a directory (works in Chrome only):")); + replace(response, "{index_of_directory}", Common::String::format(_("Index of %s"), encodeHtmlEntities(getDisplayPath(client.queryParameter("path"))).c_str())); + replace(response, "{content}", content); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/filespagehandler.h b/backends/networking/sdl_net/handlers/filespagehandler.h new file mode 100644 index 0000000000..8841ab8657 --- /dev/null +++ b/backends/networking/sdl_net/handlers/filespagehandler.h @@ -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. + * + */ + +#ifndef BACKENDS_NETWORKING_SDL_NET_FILESPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_FILESPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class FilesPageHandler: public FilesBaseHandler { + enum ItemType { + IT_DIRECTORY, + IT_PARENT_DIRECTORY, + IT_TXT, + IT_ZIP, + IT_7Z, + IT_UNKNOWN + }; + + /** + * Lists the directory <path>. + * + * Returns true on success. + */ + bool listDirectory(Common::String path, Common::String &content, const Common::String &itemTemplate); + + /** Helper method for detecting items' type. */ + static ItemType detectType(bool isDirectory, const Common::String &name); + + /** Helper method for adding items into the files list. */ + void addItem(Common::String &content, const Common::String &itemTemplate, ItemType itemType, Common::String path, Common::String name, Common::String size = "") const; + +public: + FilesPageHandler(); + virtual ~FilesPageHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.cpp b/backends/networking/sdl_net/handlers/indexpagehandler.cpp new file mode 100644 index 0000000000..04a3f45eff --- /dev/null +++ b/backends/networking/sdl_net/handlers/indexpagehandler.cpp @@ -0,0 +1,65 @@ +/* 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 "backends/networking/sdl_net/handlers/indexpagehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/translation.h" +#include "gui/storagewizarddialog.h" + +namespace Networking { + +IndexPageHandler::IndexPageHandler(): CommandSender(nullptr) {} + +IndexPageHandler::~IndexPageHandler() {} + +/// public + +Common::String IndexPageHandler::code() const { return _code; } + +void IndexPageHandler::handle(Client &client) { + Common::String code = client.queryParameter("code"); + + if (code == "") { + // redirect to "/filesAJAX" + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files\">%s</a>", + _("This is a local webserver index page."), + _("Open Files manager") + ), + "/filesAJAX" + ); + return; + } + + _code = code; + sendCommand(GUI::kStorageCodePassedCmd, 0); + HandlerUtils::setMessageHandler(client, _("ScummVM got the code and already connects to your cloud storage!")); +} + +bool IndexPageHandler::minimalModeSupported() { + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/indexpagehandler.h b/backends/networking/sdl_net/handlers/indexpagehandler.h new file mode 100644 index 0000000000..0d8e616395 --- /dev/null +++ b/backends/networking/sdl_net/handlers/indexpagehandler.h @@ -0,0 +1,45 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_INDEXPAGEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_INDEXPAGEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" +#include "gui/object.h" + +namespace Networking { +class LocalWebserver; + +class IndexPageHandler: public BaseHandler, public GUI::CommandSender { + Common::String _code; +public: + IndexPageHandler(); + virtual ~IndexPageHandler(); + + Common::String code() const; + virtual void handle(Client &client); + virtual bool minimalModeSupported(); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/listajaxhandler.cpp b/backends/networking/sdl_net/handlers/listajaxhandler.cpp new file mode 100644 index 0000000000..7c2801539d --- /dev/null +++ b/backends/networking/sdl_net/handlers/listajaxhandler.cpp @@ -0,0 +1,157 @@ +/* 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 "backends/networking/sdl_net/handlers/listajaxhandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/config-manager.h" +#include "common/json.h" +#include "common/translation.h" + +namespace Networking { + +ListAjaxHandler::ListAjaxHandler() {} + +ListAjaxHandler::~ListAjaxHandler() {} + +Common::JSONObject ListAjaxHandler::listDirectory(Common::String path) { + Common::JSONArray itemsList; + Common::JSONObject errorResult; + Common::JSONObject successResult; + successResult.setVal("type", new Common::JSONValue("success")); + errorResult.setVal("type", new Common::JSONValue("error")); + + if (path == "" || path == "/") { + if (ConfMan.hasKey("rootpath", "cloud")) + addItem(itemsList, IT_DIRECTORY, "/root/", _("File system root")); + addItem(itemsList, IT_DIRECTORY, "/saves/", _("Saved games")); + successResult.setVal("items", new Common::JSONValue(itemsList)); + return successResult; + } + + if (HandlerUtils::hasForbiddenCombinations(path)) + return errorResult; + + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd)) + return errorResult; + + Common::FSNode node = Common::FSNode(path); + if (path == "/") + node = node.getParent(); // absolute root + + if (!HandlerUtils::permittedPath(node.getPath())) + return errorResult; + + if (!node.isDirectory()) + return errorResult; + + // list directory + Common::FSList _nodeContent; + if (!node.getChildren(_nodeContent, Common::FSNode::kListAll, false)) // do not show hidden files + _nodeContent.clear(); + else + Common::sort(_nodeContent.begin(), _nodeContent.end()); + + // add parent directory link + { + Common::String filePath = path; + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + if (filePath == "" || filePath == "/" || filePath == "\\") + filePath = "/"; + else + filePath = parentPath(prefixToAdd + filePath); + addItem(itemsList, IT_PARENT_DIRECTORY, filePath, _("Parent directory")); + } + + // fill the content + for (Common::FSList::iterator i = _nodeContent.begin(); i != _nodeContent.end(); ++i) { + Common::String name = i->getDisplayName(); + if (i->isDirectory()) name += "/"; + + Common::String filePath = i->getPath(); + if (filePath.hasPrefix(prefixToRemove)) + filePath.erase(0, prefixToRemove.size()); + filePath = prefixToAdd + filePath; + + addItem(itemsList, detectType(i->isDirectory(), name), filePath, name); + } + + successResult.setVal("items", new Common::JSONValue(itemsList)); + return successResult; +} + +ListAjaxHandler::ItemType ListAjaxHandler::detectType(bool isDirectory, const Common::String &name) { + if (isDirectory) + return IT_DIRECTORY; + if (name.hasSuffix(".txt")) + return IT_TXT; + if (name.hasSuffix(".zip")) + return IT_ZIP; + if (name.hasSuffix(".7z")) + return IT_7Z; + return IT_UNKNOWN; +} + +void ListAjaxHandler::addItem(Common::JSONArray &responseItemsList, ItemType itemType, Common::String path, Common::String name, Common::String size) { + Common::String icon; + bool isDirectory = (itemType == IT_DIRECTORY || itemType == IT_PARENT_DIRECTORY); + switch (itemType) { + case IT_DIRECTORY: + icon = "dir.png"; + break; + case IT_PARENT_DIRECTORY: + icon = "up.png"; + break; + case IT_TXT: + icon = "txt.png"; + break; + case IT_ZIP: + icon = "zip.png"; + break; + case IT_7Z: + icon = "7z.png"; + break; + default: + icon = "unk.png"; + } + + Common::JSONObject item; + item.setVal("name", new Common::JSONValue(name)); + item.setVal("path", new Common::JSONValue(path)); + item.setVal("isDirectory", new Common::JSONValue(isDirectory)); + item.setVal("size", new Common::JSONValue(size)); + item.setVal("icon", new Common::JSONValue(icon)); + responseItemsList.push_back(new Common::JSONValue(item)); +} + +/// public + +void ListAjaxHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + Common::JSONValue jsonResponse = listDirectory(path); + Common::String response = jsonResponse.stringify(true); + LocalWebserver::setClientGetHandler(client, response); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/listajaxhandler.h b/backends/networking/sdl_net/handlers/listajaxhandler.h new file mode 100644 index 0000000000..27929aeb4a --- /dev/null +++ b/backends/networking/sdl_net/handlers/listajaxhandler.h @@ -0,0 +1,63 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_LISTAJAXHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_LISTAJAXHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" +#include "common/json.h" + +namespace Networking { + +class ListAjaxHandler: public FilesBaseHandler { + enum ItemType { + IT_DIRECTORY, + IT_PARENT_DIRECTORY, + IT_TXT, + IT_ZIP, + IT_7Z, + IT_UNKNOWN + }; + + /** + * Lists the directory <path>. + * + * Returns JSON with either listed directory or error response. + */ + Common::JSONObject listDirectory(Common::String path); + + /** Helper method for detecting items' type. */ + static ItemType detectType(bool isDirectory, const Common::String &name); + + /** Helper method for adding items into the files list. */ + static void addItem(Common::JSONArray &responseItemsList, ItemType itemType, Common::String path, Common::String name, Common::String size = ""); + +public: + ListAjaxHandler(); + virtual ~ListAjaxHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/resourcehandler.cpp b/backends/networking/sdl_net/handlers/resourcehandler.cpp new file mode 100644 index 0000000000..ecd8eb296a --- /dev/null +++ b/backends/networking/sdl_net/handlers/resourcehandler.cpp @@ -0,0 +1,75 @@ +/* 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 "backends/networking/sdl_net/handlers/resourcehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" + +namespace Networking { + +ResourceHandler::ResourceHandler() {} + +ResourceHandler::~ResourceHandler() {} + +const char *ResourceHandler::determineMimeType(Common::String &filename) { + // text + if (filename.hasSuffix(".html")) return "text/html"; + if (filename.hasSuffix(".css")) return "text/css"; + if (filename.hasSuffix(".txt")) return "text/plain"; + if (filename.hasSuffix(".js")) return "application/javascript"; + + // images + if (filename.hasSuffix(".jpeg") || filename.hasSuffix(".jpg") || filename.hasSuffix(".jpe")) return "image/jpeg"; + if (filename.hasSuffix(".gif")) return "image/gif"; + if (filename.hasSuffix(".png")) return "image/png"; + if (filename.hasSuffix(".svg")) return "image/svg+xml"; + if (filename.hasSuffix(".tiff")) return "image/tiff"; + if (filename.hasSuffix(".ico")) return "image/vnd.microsoft.icon"; + if (filename.hasSuffix(".wbmp")) return "image/vnd.wap.wbmp"; + + if (filename.hasSuffix(".zip")) return "application/zip"; + return "application/octet-stream"; +} + +/// public + +void ResourceHandler::handle(Client &client) { + Common::String filename = client.path(); + filename.deleteChar(0); + + // if archive hidden file is requested, ignore + if (filename.size() && filename[0] == '.') + return; + + // if file not found, don't set handler either + Common::SeekableReadStream *file = HandlerUtils::getArchiveFile(filename); + if (file == nullptr) + return; + + LocalWebserver::setClientGetHandler(client, file, 200, determineMimeType(filename)); +} + +bool ResourceHandler::minimalModeSupported() { + return true; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/resourcehandler.h b/backends/networking/sdl_net/handlers/resourcehandler.h new file mode 100644 index 0000000000..47ecfcbacd --- /dev/null +++ b/backends/networking/sdl_net/handlers/resourcehandler.h @@ -0,0 +1,42 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_RESOURCEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_RESOURCEHANDLER_H + +#include "backends/networking/sdl_net/handlers/basehandler.h" + +namespace Networking { + +class ResourceHandler: public BaseHandler { + static const char *determineMimeType(Common::String &filename); +public: + ResourceHandler(); + virtual ~ResourceHandler(); + + virtual void handle(Client &client); + virtual bool minimalModeSupported(); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlers/uploadfilehandler.cpp b/backends/networking/sdl_net/handlers/uploadfilehandler.cpp new file mode 100644 index 0000000000..6b8be15f1f --- /dev/null +++ b/backends/networking/sdl_net/handlers/uploadfilehandler.cpp @@ -0,0 +1,79 @@ +/* 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 "backends/networking/sdl_net/handlers/uploadfilehandler.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/uploadfileclienthandler.h" +#include "backends/fs/fs-factory.h" +#include "common/system.h" +#include "common/translation.h" + +namespace Networking { + +UploadFileHandler::UploadFileHandler() {} + +UploadFileHandler::~UploadFileHandler() {} + +/// public + +void UploadFileHandler::handle(Client &client) { + Common::String path = client.queryParameter("path"); + + // check that <path> is not an absolute root + if (path == "" || path == "/" || path == "\\") { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> contains no '../' + if (HandlerUtils::hasForbiddenCombinations(path)) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // transform virtual path to actual file system one + Common::String prefixToRemove = "", prefixToAdd = ""; + if (!transformPath(path, prefixToRemove, prefixToAdd, false) || path.empty()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + + // check that <path> exists, is directory and isn't forbidden + AbstractFSNode *node = g_system->getFilesystemFactory()->makeFileNodePath(path); + if (!HandlerUtils::permittedPath(node->getPath())) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Invalid path!")); + return; + } + if (!node->exists()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("The parent directory doesn't exist!")); + return; + } + if (!node->isDirectory()) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, _("Can't upload into a file!")); + return; + } + + // if all OK, set special handler + client.setHandler(new UploadFileClientHandler(path)); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlers/uploadfilehandler.h b/backends/networking/sdl_net/handlers/uploadfilehandler.h new file mode 100644 index 0000000000..8ea7cdc7e1 --- /dev/null +++ b/backends/networking/sdl_net/handlers/uploadfilehandler.h @@ -0,0 +1,40 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_UPLOADFILEHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_UPLOADFILEHANDLER_H + +#include "backends/networking/sdl_net/handlers/filesbasehandler.h" + +namespace Networking { + +class UploadFileHandler: public FilesBaseHandler { +public: + UploadFileHandler(); + virtual ~UploadFileHandler(); + + virtual void handle(Client &client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/handlerutils.cpp b/backends/networking/sdl_net/handlerutils.cpp new file mode 100644 index 0000000000..73ddf9e83f --- /dev/null +++ b/backends/networking/sdl_net/handlerutils.cpp @@ -0,0 +1,203 @@ +/* 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 "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/saves/default/default-saves.h" +#include "common/archive.h" +#include "common/config-manager.h" +#include "common/file.h" +#include "common/translation.h" +#include "common/unzip.h" + +namespace Networking { + +#define ARCHIVE_NAME "wwwroot.zip" + +#define INDEX_PAGE_NAME ".index.html" + +Common::Archive *HandlerUtils::getZipArchive() { + // first search in themepath + if (ConfMan.hasKey("themepath")) { + const Common::FSNode &node = Common::FSNode(ConfMan.get("themepath")); + if (node.exists() && node.isReadable() && node.isDirectory()) { + Common::FSNode fileNode = node.getChild(ARCHIVE_NAME); + if (fileNode.exists() && fileNode.isReadable() && !fileNode.isDirectory()) { + Common::SeekableReadStream *const stream = fileNode.createReadStream(); + Common::Archive *zipArchive = Common::makeZipArchive(stream); + if (zipArchive) + return zipArchive; + } + } + } + + // then use SearchMan to find it + Common::ArchiveMemberList fileList; + SearchMan.listMatchingMembers(fileList, ARCHIVE_NAME); + for (Common::ArchiveMemberList::iterator it = fileList.begin(); it != fileList.end(); ++it) { + Common::ArchiveMember const &m = **it; + Common::SeekableReadStream *const stream = m.createReadStream(); + Common::Archive *zipArchive = Common::makeZipArchive(stream); + if (zipArchive) + return zipArchive; + } + + return nullptr; +} + +Common::ArchiveMemberList HandlerUtils::listArchive() { + Common::ArchiveMemberList resultList; + Common::Archive *zipArchive = getZipArchive(); + if (zipArchive) { + zipArchive->listMembers(resultList); + delete zipArchive; + } + return resultList; +} + +Common::SeekableReadStream *HandlerUtils::getArchiveFile(Common::String name) { + Common::SeekableReadStream *result = nullptr; + Common::Archive *zipArchive = getZipArchive(); + if (zipArchive) { + const Common::ArchiveMemberPtr ptr = zipArchive->getMember(name); + if (ptr.get() == nullptr) + return nullptr; + result = ptr->createReadStream(); + delete zipArchive; + } + return result; +} + +Common::String HandlerUtils::readEverythingFromStream(Common::SeekableReadStream *const stream) { + Common::String result; + char buf[1024]; + uint32 readBytes; + while (!stream->eos()) { + readBytes = stream->read(buf, 1024); + result += Common::String(buf, readBytes); + } + return result; +} + +Common::String HandlerUtils::normalizePath(const Common::String &path) { + Common::String normalized; + bool slash = false; + for (uint32 i = 0; i < path.size(); ++i) { + char c = path[i]; + if (c == '\\' || c == '/') { + slash = true; + continue; + } + + if (slash) { + normalized += '/'; + slash = false; + } + + if ('A' <= c && c <= 'Z') { + normalized += c - 'A' + 'a'; + } else { + normalized += c; + } + } + if (slash) normalized += '/'; + return normalized; +} + +bool HandlerUtils::hasForbiddenCombinations(const Common::String &path) { + return (path.contains("/../") || path.contains("\\..\\") || path.contains("\\../") || path.contains("/..\\")); +} + +bool HandlerUtils::isBlacklisted(const Common::String &path) { + const char *blacklist[] = { + "/etc", + "/bin", + "c:/windows" // just saying: I know guys who install windows on another drives + }; + + // normalize path + Common::String normalized = normalizePath(path); + + uint32 size = sizeof(blacklist) / sizeof(const char *); + for (uint32 i = 0; i < size; ++i) + if (normalized.hasPrefix(blacklist[i])) + return true; + + return false; +} + +bool HandlerUtils::hasPermittedPrefix(const Common::String &path) { + // normalize path + Common::String normalized = normalizePath(path); + + // prefix for /root/ + Common::String prefix; + if (ConfMan.hasKey("rootpath", "cloud")) { + prefix = normalizePath(ConfMan.get("rootpath", "cloud")); + if (prefix == "/" || normalized.hasPrefix(prefix)) + return true; + } + + // prefix for /saves/ +#ifdef USE_LIBCURL + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + prefix = (manager ? manager->concatWithSavesPath("") : ConfMan.get("savepath")); +#else + prefix = ConfMan.get("savepath"); +#endif + return (normalized.hasPrefix(normalizePath(prefix))); +} + +bool HandlerUtils::permittedPath(const Common::String path) { + return hasPermittedPrefix(path) && !isBlacklisted(path); +} + +void HandlerUtils::setMessageHandler(Client &client, Common::String message, Common::String redirectTo) { + Common::String response = "<html><head><title>ScummVM</title></head><body>{message}</body></html>"; + + // load stylish response page from the archive + Common::SeekableReadStream *const stream = getArchiveFile(INDEX_PAGE_NAME); + if (stream) + response = readEverythingFromStream(stream); + + replace(response, "{message}", message); + if (redirectTo.empty()) + LocalWebserver::setClientGetHandler(client, response); + else + LocalWebserver::setClientRedirectHandler(client, response, redirectTo); +} + +void HandlerUtils::setFilesManagerErrorMessageHandler(Client &client, Common::String message, Common::String redirectTo) { + setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files%s?path=%s\">%s</a>", + message.c_str(), + client.queryParameter("ajax") == "true" ? "AJAX" : "", + "%2F", //that's encoded "/" + _("Back to the files manager") + ), + redirectTo + ); +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/handlerutils.h b/backends/networking/sdl_net/handlerutils.h new file mode 100644 index 0000000000..0958294595 --- /dev/null +++ b/backends/networking/sdl_net/handlerutils.h @@ -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 BACKENDS_NETWORKING_SDL_NET_HANDLERUTILS_H +#define BACKENDS_NETWORKING_SDL_NET_HANDLERUTILS_H + +#include "backends/networking/sdl_net/client.h" +#include "common/archive.h" + +namespace Networking { + +class HandlerUtils { +public: + static Common::Archive *getZipArchive(); + static Common::ArchiveMemberList listArchive(); + static Common::SeekableReadStream *getArchiveFile(Common::String name); + static Common::String readEverythingFromStream(Common::SeekableReadStream *const stream); + + static Common::String normalizePath(const Common::String &path); + static bool hasForbiddenCombinations(const Common::String &path); + static bool isBlacklisted(const Common::String &path); + static bool hasPermittedPrefix(const Common::String &path); + static bool permittedPath(const Common::String path); + + static void setMessageHandler(Client &client, Common::String message, Common::String redirectTo = ""); + static void setFilesManagerErrorMessageHandler(Client &client, Common::String message, Common::String redirectTo = ""); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/localwebserver.cpp b/backends/networking/sdl_net/localwebserver.cpp new file mode 100644 index 0000000000..fdc89b51e4 --- /dev/null +++ b/backends/networking/sdl_net/localwebserver.cpp @@ -0,0 +1,446 @@ +/* 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_ALLOW_ALL + +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/networking/sdl_net/getclienthandler.h" +#include "common/memstream.h" +#include "common/str.h" +#include "common/system.h" +#include "common/timer.h" +#include "common/translation.h" +#include <SDL_net.h> +#include <common/config-manager.h> + +#ifdef POSIX +#include <sys/types.h> +#include <ifaddrs.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#endif + +namespace Common { +class MemoryReadWriteStream; + +DECLARE_SINGLETON(Networking::LocalWebserver); + +} + +namespace Networking { + +LocalWebserver::LocalWebserver(): _set(nullptr), _serverSocket(nullptr), _timerStarted(false), + _stopOnIdle(false), _minimalMode(false), _clients(0), _idlingFrames(0), _serverPort(DEFAULT_SERVER_PORT) { + addPathHandler("/", &_indexPageHandler); + addPathHandler("/files", &_filesPageHandler); + addPathHandler("/create", &_createDirectoryHandler); + addPathHandler("/download", &_downloadFileHandler); + addPathHandler("/upload", &_uploadFileHandler); + addPathHandler("/list", &_listAjaxHandler); + addPathHandler("/filesAJAX", &_filesAjaxPageHandler); + _defaultHandler = &_resourceHandler; +} + +LocalWebserver::~LocalWebserver() { + stop(); +} + +void localWebserverTimer(void *ignored) { + LocalServer.handle(); +} + +void LocalWebserver::startTimer(int interval) { + Common::TimerManager *manager = g_system->getTimerManager(); + if (manager->installTimerProc(localWebserverTimer, interval, 0, "Networking::LocalWebserver's Timer")) { + _timerStarted = true; + } else { + warning("Failed to install Networking::LocalWebserver's timer"); + } +} + +void LocalWebserver::stopTimer() { + Common::TimerManager *manager = g_system->getTimerManager(); + manager->removeTimerProc(localWebserverTimer); + _timerStarted = false; +} + +void LocalWebserver::start(bool useMinimalMode) { + _handleMutex.lock(); + _serverPort = getPort(); + _stopOnIdle = false; + if (_timerStarted) { + _handleMutex.unlock(); + return; + } + _minimalMode = useMinimalMode; + startTimer(); + + // Create a listening TCP socket + IPaddress ip; + if (SDLNet_ResolveHost(&ip, NULL, _serverPort) == -1) { + error("LocalWebserver: SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + } + + resolveAddress(&ip); + + _serverSocket = SDLNet_TCP_Open(&ip); + if (!_serverSocket) { + warning("LocalWebserver: SDLNet_TCP_Open: %s", SDLNet_GetError()); + stopTimer(); + g_system->displayMessageOnOSD(_("Failed to start local webserver.\nCheck whether selected port is not used by another application and try again.")); + _handleMutex.unlock(); + return; + } + + // Create a socket set + _set = SDLNet_AllocSocketSet(MAX_CONNECTIONS + 1); //one more for our server socket + if (!_set) { + error("LocalWebserver: SDLNet_AllocSocketSet: %s\n", SDLNet_GetError()); + } + + int numused = SDLNet_TCP_AddSocket(_set, _serverSocket); + if (numused == -1) { + error("LocalWebserver: SDLNet_AddSocket: %s\n", SDLNet_GetError()); + } + _handleMutex.unlock(); +} + +void LocalWebserver::stop() { + _handleMutex.lock(); + if (_timerStarted) + stopTimer(); + + if (_serverSocket) { + SDLNet_TCP_Close(_serverSocket); + _serverSocket = nullptr; + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + _client[i].close(); + + _clients = 0; + + if (_set) { + SDLNet_FreeSocketSet(_set); + _set = nullptr; + } + _handleMutex.unlock(); +} + +void LocalWebserver::stopOnIdle() { _stopOnIdle = true; } + +void LocalWebserver::addPathHandler(Common::String path, BaseHandler *handler) { + if (_pathHandlers.contains(path)) + warning("LocalWebserver::addPathHandler: path already had a handler"); + _pathHandlers[path] = handler; +} + +Common::String LocalWebserver::getAddress() { return _address; } + +IndexPageHandler &LocalWebserver::indexPageHandler() { return _indexPageHandler; } + +bool LocalWebserver::isRunning() { + bool result = false; + _handleMutex.lock(); + result = _timerStarted; + _handleMutex.unlock(); + return result; +} + +uint32 LocalWebserver::getPort() { +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + if (ConfMan.hasKey("local_server_port")) + return ConfMan.getInt("local_server_port"); +#endif + return DEFAULT_SERVER_PORT; +} + +void LocalWebserver::handle() { + _handleMutex.lock(); + int numready = SDLNet_CheckSockets(_set, 0); + if (numready == -1) { + error("LocalWebserver: SDLNet_CheckSockets: %s\n", SDLNet_GetError()); + } else if (numready) { + acceptClient(); + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + handleClient(i); + + _clients = 0; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() != INVALID) + ++_clients; + + if (_clients == 0) + ++_idlingFrames; + else + _idlingFrames = 0; + + if (_idlingFrames > FRAMES_PER_SECOND && _stopOnIdle) { + _handleMutex.unlock(); + stop(); + return; + } + + _handleMutex.unlock(); +} + +void LocalWebserver::handleClient(uint32 i) { + switch (_client[i].state()) { + case INVALID: + return; + case READING_HEADERS: + _client[i].readHeaders(); + break; + case READ_HEADERS: { + // decide what to do next with that client + // check whether we know a handler for such URL + BaseHandler *handler = nullptr; + if (_pathHandlers.contains(_client[i].path())) { + handler = _pathHandlers[_client[i].path()]; + } else { + // try default handler + handler = _defaultHandler; + } + + // if server's in "minimal mode", only handlers which support it are used + if (handler && (!_minimalMode || handler->minimalModeSupported())) + handler->handle(_client[i]); + + if (_client[i].state() == BEING_HANDLED || _client[i].state() == INVALID) + break; + + // if no handler, answer with default BAD REQUEST + // fallthrough + } + + case BAD_REQUEST: + setClientGetHandler(_client[i], "<html><head><title>ScummVM - Bad Request</title></head><body>BAD REQUEST</body></html>", 400); + break; + case BEING_HANDLED: + _client[i].handle(); + break; + } +} + +void LocalWebserver::acceptClient() { + if (!SDLNet_SocketReady(_serverSocket)) + return; + + TCPsocket client = SDLNet_TCP_Accept(_serverSocket); + if (!client) + return; + + if (_clients == MAX_CONNECTIONS) { //drop the connection + SDLNet_TCP_Close(client); + return; + } + + ++_clients; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() == INVALID) { + _client[i].open(_set, client); + break; + } +} + +void LocalWebserver::resolveAddress(void *ipAddress) { + IPaddress *ip = (IPaddress *)ipAddress; + + // not resolved + _address = Common::String::format("http://127.0.0.1:%u/ (unresolved)", _serverPort); + + // default way (might work everywhere, surely works on Windows) + const char *name = SDLNet_ResolveIP(ip); + if (name == NULL) { + warning("LocalWebserver: SDLNet_ResolveIP: %s", SDLNet_GetError()); + } else { + IPaddress localIp; + if (SDLNet_ResolveHost(&localIp, name, _serverPort) == -1) { + warning("LocalWebserver: SDLNet_ResolveHost: %s", SDLNet_GetError()); + } else { + _address = Common::String::format( + "http://%u.%u.%u.%u:%u/", + localIp.host & 0xFF, (localIp.host >> 8) & 0xFF, (localIp.host >> 16) & 0xFF, (localIp.host >> 24) & 0xFF, + _serverPort + ); + } + } + + // check that our trick worked + if (_address.contains("/127.0.0.1:") || _address.contains("localhost") || _address.contains("/0.0.0.0:")) + warning("LocalWebserver: Failed to resolve IP with the default way"); + else + return; + + // if not - try platform-specific +#ifdef POSIX + struct ifaddrs *ifAddrStruct = NULL; + void *tmpAddrPtr = NULL; + + getifaddrs(&ifAddrStruct); + + for (struct ifaddrs *i = ifAddrStruct; i != NULL; i = i->ifa_next) { + if (!i->ifa_addr) { + continue; + } + + Common::String addr; + + // IPv4 + if (i->ifa_addr->sa_family == AF_INET) { + tmpAddrPtr = &((struct sockaddr_in *)i->ifa_addr)->sin_addr; + char addressBuffer[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + + // IPv6 + /* + if (i->ifa_addr->sa_family == AF_INET6) { + tmpAddrPtr = &((struct sockaddr_in6 *)i->ifa_addr)->sin6_addr; + char addressBuffer[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + */ + + if (addr.empty()) + continue; + + // ignored IPv4 addresses + if (addr.equals("127.0.0.1") || addr.equals("0.0.0.0") || addr.equals("localhost")) + continue; + + // ignored IPv6 addresses + /* + if (addr.equals("::1")) + continue; + */ + + // use the address found + _address = "http://" + addr + Common::String::format(":%u/", _serverPort); + } + + if (ifAddrStruct != NULL) freeifaddrs(ifAddrStruct); +#endif +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::String response, long code, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientGetHandler(client, stream, code, mimeType); +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::SeekableReadStream *responseStream, long code, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(code); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::String response, Common::String location, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientRedirectHandler(client, stream, location, mimeType); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::SeekableReadStream *responseStream, Common::String location, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(302); //redirect + handler->setHeader("Location", location); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +namespace { +int hexDigit(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + return -1; +} +} + +Common::String LocalWebserver::urlDecode(Common::String value) { + Common::String result = ""; + uint32 size = value.size(); + for (uint32 i = 0; i < size; ++i) { + if (value[i] == '+') { + result += ' '; + continue; + } + + if (value[i] == '%' && i + 2 < size) { + int d1 = hexDigit(value[i + 1]); + int d2 = hexDigit(value[i + 2]); + if (0 <= d1 && d1 < 16 && 0 <= d2 && d2 < 16) { + result += (char)(d1 * 16 + d2); + i = i + 2; + continue; + } + } + + result += value[i]; + } + return result; +} + +namespace { +bool isQueryUnreserved(char c) { + return ( + ('0' <= c && c <= '9') || + ('A' <= c && c <= 'Z') || + ('a' <= c && c <= 'z') || + c == '-' || c == '_' || c == '.' || c == '!' || + c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' + ); +} +} + +Common::String LocalWebserver::urlEncodeQueryParameterValue(Common::String value) { + //OK chars = alphanum | "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + //reserved for query are ";", "/", "?", ":", "@", "&", "=", "+", "," + //that means these must be encoded too or otherwise they could malform the query + Common::String result = ""; + char hexChar[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + for (uint32 i = 0; i < value.size(); ++i) { + char c = value[i]; + if (isQueryUnreserved(c)) + result += c; + else { + result += '%'; + result += hexChar[(c >> 4) & 0xF]; + result += hexChar[c & 0xF]; + } + } + return result; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/localwebserver.h b/backends/networking/sdl_net/localwebserver.h new file mode 100644 index 0000000000..5c94d6d091 --- /dev/null +++ b/backends/networking/sdl_net/localwebserver.h @@ -0,0 +1,115 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_LOCALWEBSERVER_H +#define BACKENDS_NETWORKING_SDL_NET_LOCALWEBSERVER_H + +#include "backends/networking/sdl_net/client.h" +#include "backends/networking/sdl_net/handlers/basehandler.h" +#include "backends/networking/sdl_net/handlers/createdirectoryhandler.h" +#include "backends/networking/sdl_net/handlers/downloadfilehandler.h" +#include "backends/networking/sdl_net/handlers/filesajaxpagehandler.h" +#include "backends/networking/sdl_net/handlers/filespagehandler.h" +#include "backends/networking/sdl_net/handlers/indexpagehandler.h" +#include "backends/networking/sdl_net/handlers/listajaxhandler.h" +#include "backends/networking/sdl_net/handlers/resourcehandler.h" +#include "backends/networking/sdl_net/handlers/uploadfilehandler.h" +#include "common/hash-str.h" +#include "common/mutex.h" +#include "common/singleton.h" +#include "common/scummsys.h" + +namespace Common { +class SeekableReadStream; +} + +typedef struct _SDLNet_SocketSet *SDLNet_SocketSet; +typedef struct _TCPsocket *TCPsocket; + +namespace Networking { + +#define NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + +class LocalWebserver : public Common::Singleton<LocalWebserver> { + static const uint32 FRAMES_PER_SECOND = 20; + static const uint32 TIMER_INTERVAL = 1000000 / FRAMES_PER_SECOND; + static const uint32 MAX_CONNECTIONS = 10; + + friend void localWebserverTimer(void *); //calls handle() + + SDLNet_SocketSet _set; + TCPsocket _serverSocket; + Client _client[MAX_CONNECTIONS]; + int _clients; + bool _timerStarted, _stopOnIdle, _minimalMode; + Common::HashMap<Common::String, BaseHandler*> _pathHandlers; + BaseHandler *_defaultHandler; + IndexPageHandler _indexPageHandler; + FilesPageHandler _filesPageHandler; + CreateDirectoryHandler _createDirectoryHandler; + DownloadFileHandler _downloadFileHandler; + UploadFileHandler _uploadFileHandler; + ListAjaxHandler _listAjaxHandler; + FilesAjaxPageHandler _filesAjaxPageHandler; + ResourceHandler _resourceHandler; + uint32 _idlingFrames; + Common::Mutex _handleMutex; + Common::String _address; + uint32 _serverPort; + + void startTimer(int interval = TIMER_INTERVAL); + void stopTimer(); + void handle(); + void handleClient(uint32 i); + void acceptClient(); + void resolveAddress(void *ipAddress); + void addPathHandler(Common::String path, BaseHandler *handler); + +public: + static const uint32 DEFAULT_SERVER_PORT = 12345; + + LocalWebserver(); + virtual ~LocalWebserver(); + + void start(bool useMinimalMode = false); + void stop(); + void stopOnIdle(); + + Common::String getAddress(); + IndexPageHandler &indexPageHandler(); + bool isRunning(); + static uint32 getPort(); + + static void setClientGetHandler(Client &client, Common::String response, long code = 200, const char *mimeType = nullptr); + static void setClientGetHandler(Client &client, Common::SeekableReadStream *responseStream, long code = 200, const char *mimeType = nullptr); + static void setClientRedirectHandler(Client &client, Common::String response, Common::String location, const char *mimeType = nullptr); + static void setClientRedirectHandler(Client &client, Common::SeekableReadStream *responseStream, Common::String location, const char *mimeType = nullptr); + static Common::String urlDecode(Common::String value); + static Common::String urlEncodeQueryParameterValue(Common::String value); +}; + +/** Shortcut for accessing the local webserver. */ +#define LocalServer Networking::LocalWebserver::instance() + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/reader.cpp b/backends/networking/sdl_net/reader.cpp new file mode 100644 index 0000000000..aa9f4746dd --- /dev/null +++ b/backends/networking/sdl_net/reader.cpp @@ -0,0 +1,462 @@ +/* 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 "backends/networking/sdl_net/reader.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "common/memstream.h" +#include "common/stream.h" + +namespace Networking { + +Reader::Reader() { + _state = RS_NONE; + _content = nullptr; + _bytesLeft = 0; + + _window = nullptr; + _windowUsed = 0; + _windowSize = 0; + + _headersStream = nullptr; + _firstBlock = true; + + _contentLength = 0; + _availableBytes = 0; + _isBadRequest = false; + _allContentRead = false; +} + +Reader::~Reader() { + cleanup(); +} + +Reader &Reader::operator=(Reader &r) { + if (this == &r) + return *this; + cleanup(); + + _state = r._state; + _content = r._content; + _bytesLeft = r._bytesLeft; + r._state = RS_NONE; + + _window = r._window; + _windowUsed = r._windowUsed; + _windowSize = r._windowSize; + r._window = nullptr; + + _headersStream = r._headersStream; + r._headersStream = nullptr; + + _headers = r._headers; + _method = r._method; + _path = r._path; + _query = r._query; + _anchor = r._anchor; + _queryParameters = r._queryParameters; + _contentLength = r._contentLength; + _boundary = r._boundary; + _availableBytes = r._availableBytes; + _firstBlock = r._firstBlock; + _isBadRequest = r._isBadRequest; + _allContentRead = r._allContentRead; + + return *this; +} + +void Reader::cleanup() { + //_content is not to be freed, it's not owned by Reader + + if (_headersStream != nullptr) + delete _headersStream; + + if (_window != nullptr) + freeWindow(); +} + +bool Reader::readAndHandleFirstHeaders() { + Common::String boundary = "\r\n\r\n"; + if (_window == nullptr) { + makeWindow(boundary.size()); + } + if (_headersStream == nullptr) { + _headersStream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + } + + while (readOneByteInStream(_headersStream, boundary)) { + if (_headersStream->size() > SUSPICIOUS_HEADERS_SIZE) { + _isBadRequest = true; + return true; + } + if (!bytesLeft()) + return false; + } + handleFirstHeaders(_headersStream); + + freeWindow(); + _state = RS_READING_CONTENT; + return true; +} + +bool Reader::readBlockHeadersIntoStream(Common::WriteStream *stream) { + Common::String boundary = "\r\n\r\n"; + if (_window == nullptr) makeWindow(boundary.size()); + + while (readOneByteInStream(stream, boundary)) { + if (!bytesLeft()) + return false; + } + if (stream) stream->flush(); + + freeWindow(); + _state = RS_READING_CONTENT; + return true; +} + +namespace { +void readFromThatUntilLineEnd(const char *cstr, Common::String needle, Common::String &result) { + const char *position = strstr(cstr, needle.c_str()); + + if (position) { + char c; + for (const char *i = position + needle.size(); c = *i, c != 0; ++i) { + if (c == '\n' || c == '\r') + break; + result += c; + } + } +} +} + +void Reader::handleFirstHeaders(Common::MemoryReadWriteStream *headersStream) { + if (!_boundary.empty()) { + warning("Reader: handleFirstHeaders() called when first headers were already handled"); + return; + } + + //parse method, path, query, fragment + _headers = readEverythingFromMemoryStream(headersStream); + parseFirstLine(_headers); + + //find boundary + _boundary = ""; + readFromThatUntilLineEnd(_headers.c_str(), "boundary=", _boundary); + + //find content length + Common::String contentLength = ""; + readFromThatUntilLineEnd(_headers.c_str(), "Content-Length: ", contentLength); + _contentLength = contentLength.asUint64(); + _availableBytes = _contentLength; +} + +void Reader::parseFirstLine(const Common::String &headers) { + uint32 headersSize = headers.size(); + bool bad = false; + + if (headersSize > 0) { + const char *cstr = headers.c_str(); + const char *position = strstr(cstr, "\r\n"); + if (position) { //we have at least one line - and we want the first one + //"<METHOD> <path> HTTP/<VERSION>\r\n" + Common::String method, path, http, buf; + uint32 length = position - cstr; + if (headersSize > length) + headersSize = length; + for (uint32 i = 0; i < headersSize; ++i) { + if (headers[i] != ' ') + buf += headers[i]; + if (headers[i] == ' ' || i == headersSize - 1) { + if (method == "") { + method = buf; + } else if (path == "") { + path = buf; + } else if (http == "") { + http = buf; + } else { + bad = true; + break; + } + buf = ""; + } + } + + //check that method is supported + if (method != "GET" && method != "PUT" && method != "POST") + bad = true; + + //check that HTTP/<VERSION> is OK + if (!http.hasPrefix("HTTP/")) + bad = true; + + _method = method; + parsePathQueryAndAnchor(path); + } + } + + if (bad) _isBadRequest = true; +} + +void Reader::parsePathQueryAndAnchor(Common::String path) { + //<path>[?query][#anchor] + bool readingPath = true; + bool readingQuery = false; + _path = ""; + _query = ""; + _anchor = ""; + for (uint32 i = 0; i < path.size(); ++i) { + if (readingPath) { + if (path[i] == '?') { + readingPath = false; + readingQuery = true; + } else { + _path += path[i]; + } + } else if (readingQuery) { + if (path[i] == '#') { + readingQuery = false; + } else { + _query += path[i]; + } + } else { + _anchor += path[i]; + } + } + + parseQueryParameters(); +} + +void Reader::parseQueryParameters() { + Common::String key = ""; + Common::String value = ""; + bool readingKey = true; + for (uint32 i = 0; i < _query.size(); ++i) { + if (readingKey) { + if (_query[i] == '=') { + readingKey = false; + value = ""; + } else { + key += _query[i]; + } + } else { + if (_query[i] == '&') { + if (_queryParameters.contains(key)) + warning("Reader: query parameter \"%s\" is already set!", key.c_str()); + else + _queryParameters[key] = LocalWebserver::urlDecode(value); + readingKey = true; + key = ""; + } else { + value += _query[i]; + } + } + } + + if (!key.empty()) { + if (_queryParameters.contains(key)) + warning("Reader: query parameter \"%s\" is already set!", key.c_str()); + else + _queryParameters[key] = LocalWebserver::urlDecode(value); + } +} + +bool Reader::readContentIntoStream(Common::WriteStream *stream) { + Common::String boundary = "--" + _boundary; + if (!_firstBlock) + boundary = "\r\n" + boundary; + if (_boundary.empty()) + boundary = "\r\n"; + if (_window == nullptr) + makeWindow(boundary.size()); + + while (readOneByteInStream(stream, boundary)) { + if (!bytesLeft()) + return false; + } + + _firstBlock = false; + if (stream) + stream->flush(); + + freeWindow(); + _state = RS_READING_HEADERS; + return true; +} + +void Reader::makeWindow(uint32 size) { + freeWindow(); + + _window = new byte[size]; + _windowUsed = 0; + _windowSize = size; +} + +void Reader::freeWindow() { + delete[] _window; + _window = nullptr; + _windowUsed = _windowSize = 0; +} + +namespace { +bool windowEqualsString(const byte *window, uint32 windowSize, const Common::String &boundary) { + if (boundary.size() != windowSize) + return false; + + for (uint32 i = 0; i < windowSize; ++i) { + if (window[i] != boundary[i]) + return false; + } + + return true; +} +} + +bool Reader::readOneByteInStream(Common::WriteStream *stream, const Common::String &boundary) { + byte b = readOne(); + _window[_windowUsed++] = b; + if (_windowUsed < _windowSize) + return true; + + //when window is filled, check whether that's the boundary + if (windowEqualsString(_window, _windowSize, boundary)) + return false; + + //if not, add the first byte of the window to the string + if (stream) + stream->writeByte(_window[0]); + for (uint32 i = 1; i < _windowSize; ++i) + _window[i - 1] = _window[i]; + --_windowUsed; + return true; +} + +byte Reader::readOne() { + byte b; + _content->read(&b, 1); + --_availableBytes; + --_bytesLeft; + return b; +} + +/// public + +bool Reader::readFirstHeaders() { + if (_state == RS_NONE) + _state = RS_READING_HEADERS; + + if (!bytesLeft()) + return false; + + if (_state == RS_READING_HEADERS) + return readAndHandleFirstHeaders(); + + warning("Reader::readFirstHeaders(): bad state"); + return false; +} + +bool Reader::readFirstContent(Common::WriteStream *stream) { + if (_state != RS_READING_CONTENT) { + warning("Reader::readFirstContent(): bad state"); + return false; + } + + // no difference, actually + return readBlockContent(stream); +} + +bool Reader::readBlockHeaders(Common::WriteStream *stream) { + if (_state != RS_READING_HEADERS) { + warning("Reader::readBlockHeaders(): bad state"); + return false; + } + + if (!bytesLeft()) + return false; + + return readBlockHeadersIntoStream(stream); +} + +bool Reader::readBlockContent(Common::WriteStream *stream) { + if (_state != RS_READING_CONTENT) { + warning("Reader::readBlockContent(): bad state"); + return false; + } + + if (!bytesLeft()) + return false; + + if (!readContentIntoStream(stream)) + return false; + + if (_availableBytes >= 2) { + Common::String bts; + bts += readOne(); + bts += readOne(); + if (bts == "--") + _allContentRead = true; + else if (bts != "\r\n") + warning("Reader: strange bytes: \"%s\"", bts.c_str()); + } else { + warning("Reader: strange ending"); + _allContentRead = true; + } + + return true; +} + +uint32 Reader::bytesLeft() const { return _bytesLeft; } + +void Reader::setContent(Common::MemoryReadWriteStream *stream) { + _content = stream; + _bytesLeft = stream->size() - stream->pos(); +} + +bool Reader::badRequest() const { return _isBadRequest; } + +bool Reader::noMoreContent() const { return _allContentRead; } + +Common::String Reader::headers() const { return _headers; } + +Common::String Reader::method() const { return _method; } + +Common::String Reader::path() const { return _path; } + +Common::String Reader::query() const { return _query; } + +Common::String Reader::queryParameter(Common::String name) const { return _queryParameters[name]; } + +Common::String Reader::anchor() const { return _anchor; } + +Common::String Reader::readEverythingFromMemoryStream(Common::MemoryReadWriteStream *stream) { + Common::String result; + char buf[1024]; + uint32 readBytes; + while (true) { + readBytes = stream->read(buf, 1024); + if (readBytes == 0) + break; + result += Common::String(buf, readBytes); + } + return result; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/reader.h b/backends/networking/sdl_net/reader.h new file mode 100644 index 0000000000..1a60e7a58d --- /dev/null +++ b/backends/networking/sdl_net/reader.h @@ -0,0 +1,143 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_READER_H +#define BACKENDS_NETWORKING_SDL_NET_READER_H + +#include "common/str.h" +#include "common/hashmap.h" +#include "common/hash-str.h" + +namespace Common { +class MemoryReadWriteStream; +class WriteStream; +} + +namespace Networking { + +enum ReaderState { + RS_NONE, + RS_READING_HEADERS, + RS_READING_CONTENT +}; + +/** + * This is a helper class for Client. + * + * It parses HTTP request and finds headers + * and content. It also supports POST form/multipart. + * + * One might pass the request even byte by byte, + * Reader will always be able to continue from the + * state it stopped on. + * + * Main headers/content must be read with + * readFirstHeaders() and readFirstContent() methods. + * Further headers/content blocks (POST form/multipart) + * must be read with readBlockHeaders() and readBlockContent(). + * + * Main headers and parsed URL components could be accessed + * with special methods after reading. + * + * To use the object, call setContent() and then one of those + * reading methods. It would return whether reading is over + * or not. If reading is over, content stream still could + * contain bytes to read with other methods. + * + * If reading is not over, Reader awaits you to call the + * same reading method when you'd get more content. + * + * If it's over, you should check whether Reader awaits + * more content with noMoreContent() and call the other + * reading method, if it is. When headers are read, one + * must read contents, and vice versa. + */ + +class Reader { + ReaderState _state; + Common::MemoryReadWriteStream *_content; + uint32 _bytesLeft; + + byte *_window; + uint32 _windowUsed, _windowSize; + + Common::MemoryReadWriteStream *_headersStream; + + Common::String _headers; + Common::String _method, _path, _query, _anchor; + Common::HashMap<Common::String, Common::String> _queryParameters; + uint32 _contentLength; + Common::String _boundary; + uint32 _availableBytes; + bool _firstBlock; + bool _isBadRequest; + bool _allContentRead; + + void cleanup(); + + bool readAndHandleFirstHeaders(); //true when ended reading + bool readBlockHeadersIntoStream(Common::WriteStream *stream); //true when ended reading + bool readContentIntoStream(Common::WriteStream *stream); //true when ended reading + + void handleFirstHeaders(Common::MemoryReadWriteStream *headers); + void parseFirstLine(const Common::String &headers); + void parsePathQueryAndAnchor(Common::String path); + void parseQueryParameters(); + + void makeWindow(uint32 size); + void freeWindow(); + bool readOneByteInStream(Common::WriteStream *stream, const Common::String &boundary); + + byte readOne(); + uint32 bytesLeft() const; + +public: + static const uint32 SUSPICIOUS_HEADERS_SIZE = 1024 * 1024; // 1 MB is really a lot + + Reader(); + ~Reader(); + + Reader &operator=(Reader &r); + + bool readFirstHeaders(); //true when ended reading + bool readFirstContent(Common::WriteStream *stream); //true when ended reading + bool readBlockHeaders(Common::WriteStream *stream); //true when ended reading + bool readBlockContent(Common::WriteStream *stream); //true when ended reading + + void setContent(Common::MemoryReadWriteStream *stream); + + bool badRequest() const; + bool noMoreContent() const; + + Common::String headers() const; + Common::String method() const; + Common::String path() const; + Common::String query() const; + Common::String queryParameter(Common::String name) const; + Common::String anchor() const; + + static Common::String readEverythingFromMemoryStream(Common::MemoryReadWriteStream *stream); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/networking/sdl_net/uploadfileclienthandler.cpp b/backends/networking/sdl_net/uploadfileclienthandler.cpp new file mode 100644 index 0000000000..8486b6e92e --- /dev/null +++ b/backends/networking/sdl_net/uploadfileclienthandler.cpp @@ -0,0 +1,212 @@ +/* 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 "backends/networking/sdl_net/uploadfileclienthandler.h" +#include "backends/fs/fs-factory.h" +#include "backends/networking/sdl_net/handlerutils.h" +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/networking/sdl_net/reader.h" +#include "common/file.h" +#include "common/memstream.h" +#include "common/translation.h" + +namespace Networking { + +UploadFileClientHandler::UploadFileClientHandler(Common::String parentDirectoryPath): + _state(UFH_READING_CONTENT), _headersStream(nullptr), _contentStream(nullptr), + _parentDirectoryPath(parentDirectoryPath), _uploadedFiles(0) {} + +UploadFileClientHandler::~UploadFileClientHandler() { + delete _headersStream; + delete _contentStream; +} + +void UploadFileClientHandler::handle(Client *client) { + if (client == nullptr) { + warning("UploadFileClientHandler::handle(): empty client pointer"); + return; + } + + while (true) { + switch (_state) { + case UFH_READING_CONTENT: + if (client->readContent(nullptr)) { + _state = UFH_READING_BLOCK_HEADERS; + continue; + } + break; + + case UFH_READING_BLOCK_HEADERS: + if (_headersStream == nullptr) + _headersStream = new Common::MemoryReadWriteStream(DisposeAfterUse::YES); + + if (client->readBlockHeaders(_headersStream)) { + handleBlockHeaders(client); + continue; + } + + // fail on suspicious headers + if (_headersStream->size() > Reader::SUSPICIOUS_HEADERS_SIZE) { + setErrorMessageHandler(*client, _("Invalid request: headers are too long!")); + } + break; + + case UFH_READING_BLOCK_CONTENT: + // _contentStream is created by handleBlockHeaders() if needed + + if (client->readBlockContent(_contentStream)) { + handleBlockContent(client); + continue; + } + break; + + case UFH_ERROR: + case UFH_STOP: + return; + } + + break; + } +} + +namespace { +void readFromThatUntilDoubleQuote(const char *cstr, Common::String needle, Common::String &result) { + const char *position = strstr(cstr, needle.c_str()); + + if (position) { + char c; + for (const char *i = position + needle.size(); c = *i, c != 0; ++i) { + if (c == '"') + break; + result += c; + } + } +} +} + +void UploadFileClientHandler::handleBlockHeaders(Client *client) { + _state = UFH_READING_BLOCK_CONTENT; + + // fail on suspicious headers + if (_headersStream->size() > Reader::SUSPICIOUS_HEADERS_SIZE) { + setErrorMessageHandler(*client, _("Invalid request: headers are too long!")); + } + + // search for "upload_file" field + Common::String headers = Reader::readEverythingFromMemoryStream(_headersStream); + Common::String fieldName = ""; + readFromThatUntilDoubleQuote(headers.c_str(), "name=\"", fieldName); + if (!fieldName.hasPrefix("upload_file")) + return; + + Common::String filename = ""; + readFromThatUntilDoubleQuote(headers.c_str(), "filename=\"", filename); + + // skip block if <filename> is empty + if (filename.empty()) + return; + + if (HandlerUtils::hasForbiddenCombinations(filename)) + return; + + // check that <path>/<filename> doesn't exist + Common::String path = _parentDirectoryPath; + if (path.lastChar() != '/' && path.lastChar() != '\\') + path += '/'; + AbstractFSNode *originalNode = g_system->getFilesystemFactory()->makeFileNodePath(path + filename); + if (!HandlerUtils::permittedPath(originalNode->getPath())) { + setErrorMessageHandler(*client, _("Invalid path!")); + return; + } + if (originalNode->exists()) { + setErrorMessageHandler(*client, _("There is a file with that name in the parent directory!")); + return; + } + + // remove previous stream (if there is one) + if (_contentStream) { + delete _contentStream; + _contentStream = nullptr; + } + + // create file stream (and necessary subdirectories) + Common::DumpFile *f = new Common::DumpFile(); + if (!f->open(originalNode->getPath(), true)) { + delete f; + setErrorMessageHandler(*client, _("Failed to upload the file!")); + return; + } + + _contentStream = f; +} + +void UploadFileClientHandler::handleBlockContent(Client *client) { + _state = UFH_READING_BLOCK_HEADERS; + + // if previous block headers were file-related and created a stream + if (_contentStream) { + _contentStream->flush(); + ++_uploadedFiles; + + delete _contentStream; + _contentStream = nullptr; + + if (client->noMoreContent()) { + // success - redirect back to directory listing + setSuccessHandler(*client); + return; + } + } + + // no more content avaiable + if (client->noMoreContent()) { + // if no file field was found - failure + if (_uploadedFiles == 0) { + setErrorMessageHandler(*client, _("No file was passed!")); + } else { + setSuccessHandler(*client); + } + } +} + +void UploadFileClientHandler::setErrorMessageHandler(Client &client, Common::String message) { + HandlerUtils::setFilesManagerErrorMessageHandler(client, message); + _state = UFH_ERROR; +} + +void UploadFileClientHandler::setSuccessHandler(Client &client) { + // success - redirect back to directory listing + HandlerUtils::setMessageHandler( + client, + Common::String::format( + "%s<br/><a href=\"files?path=%s\">%s</a>", + _("Uploaded successfully!"), + client.queryParameter("path").c_str(), + _("Back to parent directory") + ), + (client.queryParameter("ajax") == "true" ? "/filesAJAX?path=" : "/files?path=") + + LocalWebserver::urlEncodeQueryParameterValue(client.queryParameter("path")) + ); + _state = UFH_STOP; +} + +} // End of namespace Networking diff --git a/backends/networking/sdl_net/uploadfileclienthandler.h b/backends/networking/sdl_net/uploadfileclienthandler.h new file mode 100644 index 0000000000..1a2cd68d9a --- /dev/null +++ b/backends/networking/sdl_net/uploadfileclienthandler.h @@ -0,0 +1,70 @@ +/* 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 BACKENDS_NETWORKING_SDL_NET_UPLOADFILECLIENTHANDLER_H +#define BACKENDS_NETWORKING_SDL_NET_UPLOADFILECLIENTHANDLER_H + +#include "backends/networking/sdl_net/client.h" +#include "common/stream.h" + +namespace Networking { + +enum UploadFileHandlerState { + UFH_READING_CONTENT, + UFH_READING_BLOCK_HEADERS, + UFH_READING_BLOCK_CONTENT, + UFH_ERROR, + UFH_STOP +}; + +/** + * This class handles POST form/multipart upload. + * + * handleBlockHeaders() looks for filename and, if it's found, + * handleBlockContent() saves content into the file with such name. + * + * If no file found or other error occurs, it sets + * default error message handler. + */ + +class UploadFileClientHandler: public ClientHandler { + UploadFileHandlerState _state; + Common::MemoryReadWriteStream *_headersStream; + Common::WriteStream *_contentStream; + Common::String _parentDirectoryPath; + uint32 _uploadedFiles; + + void handleBlockHeaders(Client *client); + void handleBlockContent(Client *client); + void setErrorMessageHandler(Client &client, Common::String message); + void setSuccessHandler(Client &client); + +public: + UploadFileClientHandler(Common::String parentDirectoryPath); + virtual ~UploadFileClientHandler(); + + virtual void handle(Client *client); +}; + +} // End of namespace Networking + +#endif diff --git a/backends/platform/3ds/3ds.mk b/backends/platform/3ds/3ds.mk new file mode 100644 index 0000000000..7ab58995f6 --- /dev/null +++ b/backends/platform/3ds/3ds.mk @@ -0,0 +1,64 @@ +TARGET := scummvm + +APP_TITLE := ScummVM +APP_DESCRIPTION := Point-and-click adventure game engines +APP_AUTHOR := ScummVM Team +APP_ICON := backends/platform/3ds/app/icon.png + +APP_RSF := backends/platform/3ds/app/scummvm.rsf +APP_BANNER_IMAGE:= backends/platform/3ds/app/banner.png +APP_BANNER_AUDIO:= backends/platform/3ds/app/banner.wav + +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft +CXXFLAGS += -std=gnu++11 +ASFLAGS += -mfloat-abi=hard +LDFLAGS += -specs=3dsx.specs $(ARCH) -L$(DEVKITPRO)/libctru/lib -L$(DEVKITPRO)/portlibs/3ds/lib + +.PHONY: clean_3ds + +clean: clean_3ds + +clean_3ds: + $(RM) $(TARGET).3dsx + $(RM) $(TARGET).cia + +$(TARGET).smdh: $(APP_ICON) + @bannertool makesmdh -s "$(APP_TITLE)" -l "$(APP_DESCRIPTION)" -p "$(APP_AUTHOR)" -i $(APP_ICON) -o $@ + @echo built ... $(notdir $@) + +$(TARGET).3dsx: $(EXECUTABLE) $(TARGET).smdh + @3dsxtool $< $@ --smdh=$(TARGET).smdh + @echo built ... $(notdir $@) + +$(TARGET).bnr: $(APP_BANNER_IMAGE) $(APP_BANNER_AUDIO) + @bannertool makebanner -o $@ -i $(APP_BANNER_IMAGE) -a $(APP_BANNER_AUDIO) + @echo built ... $(notdir $@) + +$(TARGET).cia: $(EXECUTABLE) $(APP_RSF) $(TARGET).smdh $(TARGET).bnr + @makerom -f cia -target t -exefslogo -o $@ -elf $(EXECUTABLE) -rsf $(APP_RSF) -banner $(TARGET).bnr -icon $(TARGET).smdh + @echo built ... $(notdir $@) + +#--------------------------------------------------------------------------------- +# rules for assembling GPU shaders +#--------------------------------------------------------------------------------- +define shader-as + $(eval FILEPATH := $(patsubst %.shbin.o,%.shbin,$@)) + $(eval FILE := $(patsubst %.shbin.o,%.shbin,$(notdir $@))) + picasso -o $(FILEPATH) $1 + bin2s $(FILEPATH) | $(AS) -o $@ + echo "extern const u8" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(FILEPATH) | tr . _)`.h + echo "extern const u8" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(FILEPATH) | tr . _)`.h + echo "extern const u32" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(FILEPATH) | tr . _)`.h +endef + +%.shbin.o : %.v.pica %.g.pica + @echo $(notdir $^) + @$(call shader-as,$^) + +%.shbin.o : %.v.pica + @echo $(notdir $<) + @$(call shader-as,$<) + +%.shbin.o : %.shlist + @echo $(notdir $<) + @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)/$(file))) diff --git a/backends/platform/3ds/README b/backends/platform/3ds/README new file mode 100644 index 0000000000..516e694f64 --- /dev/null +++ b/backends/platform/3ds/README @@ -0,0 +1,185 @@ +ScummVM 3DS README +------------------------------------------------------------------------ + +Table of Contents: +------------------ +1.0) Installation + * 1.1 3DSX installation + * 1.2 CIA installation +2.0) Controls + * 2.1 Default key mappings + * 2.2 Hover mode + * 2.3 Drag mode +3.0) Supported Games +4.0) Compiling + * 4.1 Prerequisites + * * 4.1.1 Compiling third-party libraries + * 4.2 Compiling ScummVM + * 4.3 Warning for 3DSX build + + + +1.0) Installation +----------------- +There are two possible formats to be used: 3DSX and CIA (recommended). +The 3DSX format is exclusively used by the Homebrew Launcher and its derivatives. +The CIA format can be installed directly to the 3DS home menu and can be launched +using any CFW (Custom Firmware) of your choice. + +Installing the Homebrew Launcher or any CFW is beyond the scope of this README. +Look elsewhere to see how to install those if you do not already have them set up. + + +1.1) 3DSX installation +---------------- +The CIA format is recommended for stability and maximum game support. If that is +not an option, you will need one of a collection of 3DS titles installed on your +system in order to properly launch ScummVM as a 3DSX. This is because the +Homebrew Launcher hijacks other processes to run 3DSX homebrew, and ScummVM is a +particularly large homebrew that can't be launched with the resources provided +by standard system applications. + +You will need one of the following (installed or physically in cart slot): + +- Youtube +- Monster Hunter 4 Ultimate Special Demo +- Monster Hunter 4 Ultimate +- Monster Hunter 4G +- Super Smash Bros. for Nintendo 3DS Demo +- Super Smash Bros. for Nintendo 3DS Special Demo +- Super Smash Bros. for Nintendo 3DS + +Once you have one of the above, you need to merely extract all ScummVM 3DS files +to the root of your SD card so that all files reside in the /3ds/scummvm/ directory. + + +1.2) CIA installation +--------------------- +The CIA format requires a DSP binary dump saved on your SD card as /3ds/dspfirm.cdc +for proper audio support. You can search online to find software to dump this. +Not having this file will cause many problems with games that need audio, sometimes +even crashing, so this is NOT considered optional. + +Using any CIA installation software (search elsewhere for that), you need to install +the scummvm.cia file. Then, just like what is done with the 3DSX installation, you +need to extract all ScummVM 3DS files (scummvm.cia excluded) to the root of your SD +card so that all files reside in the /3ds/scummvm/ directory. + + + +2.0) Controls +------------- + +2.1) Default key mappings +------------------------- +The D-Pad and A/B/X/Y buttons have mirrored usage. So they do the same things +depending on if you're right or left-handed. + +| Buttons | Function | +|------------|--------------------------------| +| A / D-left | Left-click | +| X / D-up | Right-click | +| B / D-down | ESC (skips cutscenes and such) | +| L | Use virtual keyboard | +| R | Toggle hover/drag modes | +| Start | Open game menu | +| Select | Open 3DS config menu | +| Circle Pad | Move the cursor | + + +2.2) Hover mode +--------------- +When you use the touchscreen, you are simulating the mere moving of the mouse. You +can click only with taps, meaning it is impossible to drag stuff or hold down a +mouse button without using buttons mapped to right/left-click. + + +2.3) Drag mode +-------------- +Every time you touch and release the touchscreen, you are simulating the click and +release of the mouse buttons. At the moment, this is only a left-click. + + + +3.0) Supported Games +-------------------- +The full game engine compatibility list can be found here: +http://scummvm.org/compatibility/ + +While all the above games should run on the 3DS (report if they do not), there are +many games which are unplayable due to the lack of CPU speed on the 3DS. So if +you play any games that run really slow, this is not considered a bug, but rather +a hardware limitation. Though possible GPU optimizations are always in the works. +The New 3DS console has much better performance, but there are still many newer and +high-resolution games that cannot be played. A list of these unplayable games and +game engines will eventually be listed here. + + + +4.0) Compiling +-------------- + +4.1) Prerequisites +------------------ + - devkitARM (presumably with libctru, picasso and such) + - citro3d + - Optional: You should compile third-party libraries for the 3ds (commonly referred + to as portlibs in the devkitPRO community). Some games requires these to operate + properly. + + +4.1.1) Compiling third-party libraries +-------------------------------------- +Most libraries used can be compiled with same commands and configuration flags. + +It is assumed that you have these environment variables defined: + - DEVKITPRO Your root devkitPro directory + - DEVKITARM Your root devkitARM directory (probably same as $DEVKITPRO/devkitARM) + - CTRULIB Your root libctru directory (probably same as $DEVKITPRO/libctru) + +In the source directory of the library: + - $ export PORTLIBS=$DEVKITPRO/portlibs/armv6k + - $ export PATH=$DEVKITARM/bin:$PATH + - $ export PKG_CONFIG_PATH=$PORTLIBS/lib/pkgconfig + - $ export CFLAGS="-g -march=armv6k -mtune=mpcore -mfloat-abi=hard -O2 + -mword-relocations -ffunction-sections -fdata-sections" + - $ export CPPFLAGS="-I$PORTLIBS/include -I$CTRULIB/include" + - $ export LDFLAGS="-L$PORTLIBS/lib" + - $ mkdir -p $PORTLIBS + - $ ./configure --prefix=$PORTLIBS --host=arm-none-eabi --disable-shared + --enable-static + - $ make + - $ make install + +Useful libraries (and special config flags needed): + - zlib + - libpng + - libjpeg + - freetype2 --without-bzip2 --without-harfbuzz + - libmad + - tremor + - flac --disable-cpplibs --without-flac + - faad + + +4.2) Compiling ScummVM +---------------------- + - $ ./configure --host=3ds + - $ make + +Additionally compile to specific formats to be used on the 3ds: + - $ make scummvm.3dsx + - $ make scummvm.cia + + +4.3) Warning for 3DSX build +--------------------------- +The above configuration command will include all game engines by default and will +likely be too massive to run using the 3DSX format. Until dynamic modules are figured +out, you should configure engines like this for 3DSX builds: + + - $ ./configure --host=3ds --disable-all-engines--enable-engine=scumm-7-8,myst,riven, + sword1,sword2,sword25,sci,lure,sky,agi,agos + +Choose whatever engines you want, but if the ELF's .text section exceeds ~10MB, it +won't be playable unless it's a CIA. diff --git a/backends/platform/3ds/app/banner.png b/backends/platform/3ds/app/banner.png Binary files differnew file mode 100644 index 0000000000..a3b02150ec --- /dev/null +++ b/backends/platform/3ds/app/banner.png diff --git a/backends/platform/3ds/app/banner.wav b/backends/platform/3ds/app/banner.wav Binary files differnew file mode 100644 index 0000000000..e0b684b62f --- /dev/null +++ b/backends/platform/3ds/app/banner.wav diff --git a/backends/platform/3ds/app/icon.png b/backends/platform/3ds/app/icon.png Binary files differnew file mode 100644 index 0000000000..07022fbac1 --- /dev/null +++ b/backends/platform/3ds/app/icon.png diff --git a/backends/platform/3ds/app/scummvm.rsf b/backends/platform/3ds/app/scummvm.rsf new file mode 100644 index 0000000000..a4518949bb --- /dev/null +++ b/backends/platform/3ds/app/scummvm.rsf @@ -0,0 +1,219 @@ +BasicInfo: + Title : ScummVM + ProductCode : ScummVM + Logo : Nintendo # Nintendo / Licensed / Distributed / iQue / iQueForSystem + +TitleInfo: + Category : Application + UniqueId : 0xFF321 + +Option: + UseOnSD : true # true if App is to be installed to SD + FreeProductCode : true # Removes limitations on ProductCode + MediaFootPadding : false # If true CCI files are created with padding + EnableCrypt : false # Enables encryption for NCCH and CIA + EnableCompress : false # Compresses where applicable (currently only exefs:/.code) + +AccessControlInfo: + CoreVersion : 2 + + # Exheader Format Version + DescVersion : 2 + + # Minimum Required Kernel Version (below is for 4.5.0) + ReleaseKernelMajor : "02" + ReleaseKernelMinor : "33" + + # ExtData + UseExtSaveData : false # enables ExtData + #ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId + + # FS:USER Archive Access Permissions + # Uncomment as required + FileSystemAccess: + #- CategorySystemApplication + #- CategoryHardwareCheck + #- CategoryFileSystemTool + #- Debug + #- TwlCardBackup + #- TwlNandData + #- Boss + - DirectSdmc + #- Core + #- CtrNandRo + #- CtrNandRw + #- CtrNandRoWrite + #- CategorySystemSettings + #- CardBoard + #- ExportImportIvs + #- DirectSdmcWrite + #- SwitchCleanup + #- SaveDataMove + #- Shop + #- Shell + #- CategoryHomeMenu + + # Process Settings + MemoryType : Application # Application/System/Base + SystemMode : 64MB # 64MB(Default)/96MB/80MB/72MB/32MB + IdealProcessor : 0 + AffinityMask : 1 + Priority : 16 + MaxCpu : 0 # Let system decide + HandleTableSize : 0x200 + DisableDebug : false + EnableForceDebug : false + CanWriteSharedPage : true + CanUsePrivilegedPriority : false + CanUseNonAlphabetAndNumber : true + PermitMainFunctionArgument : true + CanShareDeviceMemory : true + RunnableOnSleep : false + SpecialMemoryArrange : true + + # New3DS Exclusive Process Settings + SystemModeExt : 124MB # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode + CpuSpeed : 804MHz # 268MHz(Default)/804MHz + EnableL2Cache : true # false(default)/true + CanAccessCore2 : true + + # Virtual Address Mappings + IORegisterMapping: + - 1ff00000-1ff7ffff # DSP memory + MemoryMapping: + - 1f000000-1f5fffff:r # VRAM + + # Accessible SVCs, <Name>:<ID> + SystemCallAccess: + ArbitrateAddress: 34 + Break: 60 + CancelTimer: 28 + ClearEvent: 25 + ClearTimer: 29 + CloseHandle: 35 + ConnectToPort: 45 + ControlMemory: 1 + CreateAddressArbiter: 33 + CreateEvent: 23 + CreateMemoryBlock: 30 + CreateMutex: 19 + CreateSemaphore: 21 + CreateThread: 8 + CreateTimer: 26 + DuplicateHandle: 39 + ExitProcess: 3 + ExitThread: 9 + GetCurrentProcessorNumber: 17 + GetHandleInfo: 41 + GetProcessId: 53 + GetProcessIdOfThread: 54 + GetProcessIdealProcessor: 6 + GetProcessInfo: 43 + GetResourceLimit: 56 + GetResourceLimitCurrentValues: 58 + GetResourceLimitLimitValues: 57 + GetSystemInfo: 42 + GetSystemTick: 40 + GetThreadContext: 59 + GetThreadId: 55 + GetThreadIdealProcessor: 15 + GetThreadInfo: 44 + GetThreadPriority: 11 + MapMemoryBlock: 31 + OutputDebugString: 61 + QueryMemory: 2 + ReleaseMutex: 20 + ReleaseSemaphore: 22 + SendSyncRequest1: 46 + SendSyncRequest2: 47 + SendSyncRequest3: 48 + SendSyncRequest4: 49 + SendSyncRequest: 50 + SetThreadPriority: 12 + SetTimer: 27 + SignalEvent: 24 + SleepThread: 10 + UnmapMemoryBlock: 32 + WaitSynchronization1: 36 + WaitSynchronizationN: 37 + Backdoor: 123 + + # Service List + # Maximum 34 services (32 if firmware is prior to 9.3.0) + ServiceAccessControl: + - cfg:u + - fs:USER + - gsp::Gpu + - hid:USER + - ndm:u + - pxi:dev + - APT:U + - ac:u + - act:u + - am:net + - boss:U + - cam:u + - cecd:u + - dsp::DSP + - frd:u + - http:C + - ir:USER + - ir:u + - ir:rst + - ldr:ro + - mic:u + - news:u + - nim:aoc + - nwm::UDS + - ptm:u + - qtm:u + - soc:U + - ssl:C + - y2r:u + - gsp::Lcd + + +SystemControlInfo: + SaveDataSize: 0K + RemasterVersion: 0 + StackSize: 0x40000 + + # Modules that run services listed above should be included below + # Maximum 48 dependencies + # If a module is listed that isn't present on the 3DS, the title will get stuck at the logo (3ds waves) + # So act, nfc and qtm are commented for 4.x support. Uncomment if you need these. + # <module name>:<module titleid> + Dependency: + ac: 0x0004013000002402 + #act: 0x0004013000003802 + am: 0x0004013000001502 + boss: 0x0004013000003402 + camera: 0x0004013000001602 + cecd: 0x0004013000002602 + cfg: 0x0004013000001702 + codec: 0x0004013000001802 + csnd: 0x0004013000002702 + dlp: 0x0004013000002802 + dsp: 0x0004013000001a02 + friends: 0x0004013000003202 + gpio: 0x0004013000001b02 + gsp: 0x0004013000001c02 + hid: 0x0004013000001d02 + http: 0x0004013000002902 + i2c: 0x0004013000001e02 + ir: 0x0004013000003302 + mcu: 0x0004013000001f02 + mic: 0x0004013000002002 + ndm: 0x0004013000002b02 + news: 0x0004013000003502 + #nfc: 0x0004013000004002 + nim: 0x0004013000002c02 + nwm: 0x0004013000002d02 + pdn: 0x0004013000002102 + ps: 0x0004013000003102 + ptm: 0x0004013000002202 + #qtm: 0x0004013020004202 + ro: 0x0004013000003702 + socket: 0x0004013000002e02 + spi: 0x0004013000002302 + ssl: 0x0004013000002f02 diff --git a/backends/platform/3ds/config.cpp b/backends/platform/3ds/config.cpp new file mode 100644 index 0000000000..bfadc5ac06 --- /dev/null +++ b/backends/platform/3ds/config.cpp @@ -0,0 +1,87 @@ +/* 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 "config.h" +#include "osystem.h" +#include "options-dialog.h" +#include "common/config-manager.h" +#include <3ds.h> + +namespace _3DS { + +Config config; +static Common::String prefix = "3ds_"; + +static bool confGetBool(Common::String key, bool defaultVal) { + if (ConfMan.hasKey(prefix + key)) + return ConfMan.getBool(prefix + key); + return defaultVal; +} + +static void confSetBool(Common::String key, bool val) { + ConfMan.setBool(prefix + key, val); +} + +static int confGetInt(Common::String key, int defaultVal) { + if (ConfMan.hasKey(prefix + key)) + return ConfMan.getInt(prefix + key); + return defaultVal; +} + +static void confSetInt(Common::String key, int val) { + ConfMan.setInt(prefix + key, val); +} + +void loadConfig() { + config.showCursor = confGetBool("showcursor", true); + config.snapToBorder = confGetBool("snaptoborder", true); + config.stretchToFit = confGetBool("stretchtofit", false); + config.sensitivity = confGetInt("sensitivity", -5); + config.screen = confGetInt("screen", kScreenBoth); + + // Turn off the backlight of any screen not used + if (R_SUCCEEDED(gspLcdInit())) { + if (config.screen == kScreenTop) { + GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_TOP); + GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_BOTTOM); + } else if (config.screen == kScreenBottom) { + GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTTOM); + GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_TOP); + } else + GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH); + gspLcdExit(); + } + + OSystem_3DS *osys = (OSystem_3DS *)g_system; + osys->updateConfig(); +} + +void saveConfig() { + confSetBool("showcursor", config.showCursor); + confSetBool("snaptoborder", config.snapToBorder); + confSetBool("stretchtofit", config.stretchToFit); + confSetInt("sensitivity", config.sensitivity); + confSetInt("screen", config.screen); + ConfMan.flushToDisk(); +} + +} // namespace _3DS diff --git a/backends/platform/3ds/config.h b/backends/platform/3ds/config.h new file mode 100644 index 0000000000..02560b2040 --- /dev/null +++ b/backends/platform/3ds/config.h @@ -0,0 +1,45 @@ +/* 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 CONFIG_3DS_H +#define CONFIG_3DS_H + +#include "common/str.h" + +namespace _3DS { + +struct Config { + bool showCursor; + bool snapToBorder; + bool stretchToFit; + int sensitivity; + int screen; +}; + +extern Config config; + +void loadConfig(); +void saveConfig(); + +} // namespace _3DS + +#endif // CONFIG_3DS_H diff --git a/backends/platform/3ds/gui.cpp b/backends/platform/3ds/gui.cpp new file mode 100644 index 0000000000..0883d5a102 --- /dev/null +++ b/backends/platform/3ds/gui.cpp @@ -0,0 +1,46 @@ +/* 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 "backends/platform/3ds/gui.h" +#include "common/system.h" + +StatusMessageDialog* StatusMessageDialog::_opened = 0; + +StatusMessageDialog::StatusMessageDialog(const Common::String &message, uint32 duration) + : MessageDialog(message, 0, 0) { + _timer = g_system->getMillis() + duration; + if (_opened) + _opened->close(); + _opened = this; +} + +void StatusMessageDialog::handleTickle() { + MessageDialog::handleTickle(); + if (g_system->getMillis() > _timer) + close(); +} + +void StatusMessageDialog::close() { + GUI::Dialog::close(); + if (_opened) + _opened = 0; +} diff --git a/backends/platform/3ds/gui.h b/backends/platform/3ds/gui.h new file mode 100644 index 0000000000..8e82c6c851 --- /dev/null +++ b/backends/platform/3ds/gui.h @@ -0,0 +1,41 @@ +/* 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 GUI_3DS_H +#define GUI_3DS_H + +#include "gui/message.h" + +class StatusMessageDialog : public GUI::MessageDialog { +public: + StatusMessageDialog(const Common::String &message, uint32 duration); + + void handleTickle(); + +protected: + virtual void close(); + + uint32 _timer; + static StatusMessageDialog* _opened; +}; + +#endif // GUI_3DS_H diff --git a/backends/platform/3ds/main.cpp b/backends/platform/3ds/main.cpp new file mode 100644 index 0000000000..e9046d7c28 --- /dev/null +++ b/backends/platform/3ds/main.cpp @@ -0,0 +1,54 @@ +/* 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 "osystem.h" +#include <3ds.h> + +int main(int argc, char *argv[]) { + // Initialize basic libctru stuff + gfxInitDefault(); + cfguInit(); + osSetSpeedupEnable(true); +// consoleInit(GFX_TOP, NULL); + + g_system = new _3DS::OSystem_3DS(); + assert(g_system); + + // Invoke the actual ScummVM main entry point +// if (argc > 2) +// res = scummvm_main(argc-2, &argv[2]); +// else +// res = scummvm_main(argc, argv); + scummvm_main(0, nullptr); + + delete dynamic_cast<_3DS::OSystem_3DS*>(g_system); + + // Turn on both screen backlights before exiting. + if (R_SUCCEEDED(gspLcdInit())) { + GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH); + gspLcdExit(); + } + + cfguExit(); + gfxExit(); + return 0; +} diff --git a/backends/platform/3ds/module.mk b/backends/platform/3ds/module.mk new file mode 100644 index 0000000000..3eb15aef81 --- /dev/null +++ b/backends/platform/3ds/module.mk @@ -0,0 +1,18 @@ +MODULE := backends/platform/3ds + +MODULE_OBJS := \ + main.o \ + shader.shbin.o \ + sprite.o \ + gui.o \ + config.o \ + options-dialog.o \ + osystem.o \ + osystem-graphics.o \ + osystem-audio.o \ + osystem-events.o + +# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS. +MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS)) +OBJS := $(MODULE_OBJS) $(OBJS) +MODULE_DIRS += $(sort $(dir $(MODULE_OBJS))) diff --git a/backends/platform/3ds/options-dialog.cpp b/backends/platform/3ds/options-dialog.cpp new file mode 100644 index 0000000000..60fe7f818f --- /dev/null +++ b/backends/platform/3ds/options-dialog.cpp @@ -0,0 +1,98 @@ +/* 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 "options-dialog.h" +#include "config.h" +#include "gui/dialog.h" +#include "gui/gui-manager.h" +#include "gui/widgets/list.h" +#include "gui/widgets/tab.h" +#include "osystem.h" +#include "engines/scumm/scumm.h" +#include "gui/widgets/popup.h" + +#include "common/translation.h" + +namespace _3DS { + +bool optionMenuOpened = false; + +OptionsDialog::OptionsDialog() : GUI::Dialog(20, 20, 280, 200) { + + optionMenuOpened = true; + + new GUI::ButtonWidget(this, 120, 180, 72, 16, _("~C~lose"), 0, GUI::kCloseCmd); + new GUI::ButtonWidget(this, 200, 180, 72, 16, _("~S~ave"), 0, GUI::kOKCmd); + + _showCursorCheckbox = new GUI::CheckboxWidget(this, 5, 5, 130, 20, _("Show mouse cursor"), 0, 0, 'T'); + _showCursorCheckbox->setState(config.showCursor); + + _snapToBorderCheckbox = new GUI::CheckboxWidget(this, 5, 22, 130, 20, _("Snap to edges"), 0, 0, 'T'); + _snapToBorderCheckbox->setState(config.snapToBorder); + + _stretchToFitCheckbox = new GUI::CheckboxWidget(this, 140, 5, 130, 20, _("Stretch to fit"), 0, 0, 'T'); + _stretchToFitCheckbox->setState(config.stretchToFit); + + new GUI::StaticTextWidget(this, 0, 60, 110, 15, _("Use Screen:"), Graphics::kTextAlignRight); + _screenRadioGroup = new GUI::RadiobuttonGroup(this, kScreenRadioGroup); + _screenTopRadioWidget = new GUI::RadiobuttonWidget(this, 120, 50, 60, 20, _screenRadioGroup, kScreenTop, _("Top")); + _screenBottomRadioWidget = new GUI::RadiobuttonWidget(this, 190, 50, 80, 20, _screenRadioGroup, kScreenBottom, _("Bottom")); + _screenBothRadioWidget = new GUI::RadiobuttonWidget(this, 155, 70, 80, 20, _screenRadioGroup, kScreenBoth, _("Both")); + _screenRadioGroup->setValue(config.screen); + + new GUI::StaticTextWidget(this, 0, 100, 110, 15, _("C-Pad Sensitivity:"), Graphics::kTextAlignRight); + _sensitivity = new GUI::SliderWidget(this, 115, 100, 160, 15, "TODO: Add tooltip", 1); + _sensitivity->setMinValue(-15); + _sensitivity->setMaxValue(30); + _sensitivity->setValue(config.sensitivity); + _sensitivity->setFlags(GUI::WIDGET_CLEARBG); +} + +OptionsDialog::~OptionsDialog() { + optionMenuOpened = false; +} + +void OptionsDialog::updateConfigManager() { + config.showCursor = _showCursorCheckbox->getState(); + config.snapToBorder = _snapToBorderCheckbox->getState(); + config.stretchToFit = _stretchToFitCheckbox->getState(); + config.sensitivity = _sensitivity->getValue(); + config.screen = _screenRadioGroup->getValue(); + saveConfig(); + loadConfig(); +} + +void OptionsDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) { + switch(cmd) { + case GUI::kOKCmd: + updateConfigManager(); + // Fall through + case GUI::kCloseCmd: + close(); + break; + default: + Dialog::handleCommand(sender, cmd, data); + break; + } +} + +} // namespace _3DS diff --git a/backends/platform/3ds/options-dialog.h b/backends/platform/3ds/options-dialog.h new file mode 100644 index 0000000000..d8c0d7bc7d --- /dev/null +++ b/backends/platform/3ds/options-dialog.h @@ -0,0 +1,70 @@ +/* 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 OPTIONS_DIALOG_3DS_H +#define OPTIONS_DIALOG_3DS_H + + +#include "common/scummsys.h" +#include "common/str.h" +#include "gui/object.h" +#include "gui/widget.h" +#include "gui/dialog.h" +#include "gui/widgets/tab.h" +#include "scumm/dialogs.h" + +namespace _3DS { + +enum { + kSave = 0x10000000, + kScreenRadioGroup, + kScreenTop, + kScreenBottom, + kScreenBoth, +}; + +extern bool optionMenuOpened; + +class OptionsDialog : public GUI::Dialog { + +public: + OptionsDialog(); + ~OptionsDialog(); + +protected: + virtual void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data); + void updateConfigManager(); + + GUI::SliderWidget *_sensitivity; + GUI::CheckboxWidget *_showCursorCheckbox; + GUI::CheckboxWidget *_snapToBorderCheckbox; + GUI::CheckboxWidget *_stretchToFitCheckbox; + + GUI::RadiobuttonGroup *_screenRadioGroup; + GUI::RadiobuttonWidget *_screenTopRadioWidget; + GUI::RadiobuttonWidget *_screenBottomRadioWidget; + GUI::RadiobuttonWidget *_screenBothRadioWidget; +}; + +} // namespace _3DS + +#endif // OPTIONS_DIALOG_3DS_H diff --git a/backends/platform/3ds/osystem-audio.cpp b/backends/platform/3ds/osystem-audio.cpp new file mode 100644 index 0000000000..58262372cc --- /dev/null +++ b/backends/platform/3ds/osystem-audio.cpp @@ -0,0 +1,110 @@ +/* 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 "osystem.h" +#include "audio/mixer.h" + +namespace _3DS { + +static bool hasAudio = false; + +static void audioThreadFunc(void *arg) { + Audio::MixerImpl *mixer = (Audio::MixerImpl *)arg; + OSystem_3DS *osys = (OSystem_3DS *)g_system; + + int i; + const int channel = 0; + int bufferIndex = 0; + const int bufferCount = 3; + const int bufferSize = 80000; // Can't be too small, based on delayMillis duration + const int sampleRate = mixer->getOutputRate(); + int sampleLen = 0; + uint32 lastTime = osys->getMillis(true); + uint32 time = lastTime; + ndspWaveBuf buffers[bufferCount]; + + for (i = 0; i < bufferCount; ++i) { + memset(&buffers[i], 0, sizeof(ndspWaveBuf)); + buffers[i].data_vaddr = linearAlloc(bufferSize); + buffers[i].looping = false; + buffers[i].status = NDSP_WBUF_FREE; + } + + ndspChnReset(channel); + ndspChnSetInterp(channel, NDSP_INTERP_LINEAR); + ndspChnSetRate(channel, sampleRate); + ndspChnSetFormat(channel, NDSP_FORMAT_STEREO_PCM16); + + while (!osys->exiting) { + osys->delayMillis(100); // Note: Increasing the delay requires a bigger buffer + + time = osys->getMillis(true); + sampleLen = (time - lastTime) * 22 * 4; // sampleRate / 1000 * channelCount * sizeof(int16); + lastTime = time; + + if (!osys->sleeping && sampleLen > 0) { + bufferIndex++; + bufferIndex %= bufferCount; + ndspWaveBuf *buf = &buffers[bufferIndex]; + + buf->nsamples = mixer->mixCallback(buf->data_adpcm, sampleLen); + if (buf->nsamples > 0) { + DSP_FlushDataCache(buf->data_vaddr, bufferSize); + ndspChnWaveBufAdd(channel, buf); + } + } + } + + for (i = 0; i < bufferCount; ++i) + linearFree(buffers[i].data_pcm8); +} + +void OSystem_3DS::initAudio() { + _mixer = new Audio::MixerImpl(this, 22050); + + hasAudio = R_SUCCEEDED(ndspInit()); + _mixer->setReady(false); + + if (hasAudio) { + s32 prio = 0; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + audioThread = threadCreate(&audioThreadFunc, _mixer, 32 * 1048, prio - 1, -2, false); + } +} + +void OSystem_3DS::destroyAudio() { + if (hasAudio) { + threadJoin(audioThread, U64_MAX); + threadFree(audioThread); + ndspExit(); + } + + delete _mixer; + _mixer = 0; +} + +Audio::Mixer *OSystem_3DS::getMixer() { + assert(_mixer); + return _mixer; +} + +} // namespace _3DS diff --git a/backends/platform/3ds/osystem-events.cpp b/backends/platform/3ds/osystem-events.cpp new file mode 100644 index 0000000000..064540fa66 --- /dev/null +++ b/backends/platform/3ds/osystem-events.cpp @@ -0,0 +1,302 @@ +/* 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 "backends/timer/default/default-timer.h" +#include "engines/engine.h" +#include "gui.h" +#include "options-dialog.h" +#include "config.h" +#include "osystem.h" + +namespace _3DS { + +static Common::Mutex *eventMutex; +static InputMode inputMode = MODE_DRAG; +static aptHookCookie cookie; +static bool optionMenuOpening = false; +static Common::String messageOSD; +static bool showMessageOSD = false; + +static void pushEventQueue(Common::Queue<Common::Event> *queue, Common::Event &event) { + Common::StackLock lock(*eventMutex); + queue->push(event); +} + +static void eventThreadFunc(void *arg) { + OSystem_3DS *osys = (OSystem_3DS *)g_system; + auto eventQueue = (Common::Queue<Common::Event> *)arg; + + uint32 touchStartTime = osys->getMillis(); + touchPosition lastTouch = {0, 0}; + bool isRightClick = false; + float cursorX = 0; + float cursorY = 0; + float cursorDeltaX = 0; + float cursorDeltaY = 0; + int circleDeadzone = 20; + int borderSnapZone = 6; + Common::Event event; + + while (!osys->exiting) { + do { + osys->delayMillis(10); + } while (osys->sleeping && !osys->exiting); + + hidScanInput(); + touchPosition touch; + circlePosition circle; + u32 held = hidKeysHeld(); + u32 keysPressed = hidKeysDown(); + u32 keysReleased = hidKeysUp(); + + // C-Pad used to control the cursor + hidCircleRead(&circle); + if (circle.dx < circleDeadzone && circle.dx > -circleDeadzone) + circle.dx = 0; + if (circle.dy < circleDeadzone && circle.dy > -circleDeadzone) + circle.dy = 0; + cursorDeltaX = (0.0002f + config.sensitivity / 100000.f) * circle.dx * abs(circle.dx); + cursorDeltaY = (0.0002f + config.sensitivity / 100000.f) * circle.dy * abs(circle.dy); + + // Touch screen events + if (held & KEY_TOUCH) { + hidTouchRead(&touch); + if (config.snapToBorder) { + if (touch.px < borderSnapZone) + touch.px = 0; + if (touch.px > 319 - borderSnapZone) + touch.px = 319; + if (touch.py < borderSnapZone) + touch.py = 0; + if (touch.py > 239 - borderSnapZone) + touch.py = 239; + } + cursorX = touch.px; + cursorY = touch.py; + osys->transformPoint(touch); + + osys->warpMouse(touch.px, touch.py); + event.mouse.x = touch.px; + event.mouse.y = touch.py; + + if (keysPressed & KEY_TOUCH) { + touchStartTime = osys->getMillis(); + isRightClick = (held & KEY_X || held & KEY_DUP); + if (inputMode == MODE_DRAG) { + event.type = isRightClick ? Common::EVENT_RBUTTONDOWN : Common::EVENT_LBUTTONDOWN; + pushEventQueue(eventQueue, event); + } + } else if (touch.px != lastTouch.px || touch.py != lastTouch.py) { + event.type = Common::EVENT_MOUSEMOVE; + pushEventQueue(eventQueue, event); + } + + lastTouch = touch; + } else if (keysReleased & KEY_TOUCH) { + event.mouse.x = lastTouch.px; + event.mouse.y = lastTouch.py; + if (inputMode == MODE_DRAG) { + event.type = isRightClick ? Common::EVENT_RBUTTONUP : Common::EVENT_LBUTTONUP; + pushEventQueue(eventQueue, event); + } else if (osys->getMillis() - touchStartTime < 200) { + // Process click in MODE_HOVER + event.type = Common::EVENT_MOUSEMOVE; + pushEventQueue(eventQueue, event); + event.type = isRightClick ? Common::EVENT_RBUTTONDOWN : Common::EVENT_LBUTTONDOWN; + pushEventQueue(eventQueue, event); + event.type = isRightClick ? Common::EVENT_RBUTTONUP : Common::EVENT_LBUTTONUP; + pushEventQueue(eventQueue, event); + } + } else if (cursorDeltaX != 0 || cursorDeltaY != 0) { + cursorX += cursorDeltaX; + cursorY -= cursorDeltaY; + if (cursorX < 0) cursorX = 0; + if (cursorY < 0) cursorY = 0; + if (cursorX > 320) cursorX = 320; + if (cursorY > 240) cursorY = 240; + lastTouch.px = cursorX; + lastTouch.py = cursorY; + osys->transformPoint(lastTouch); + osys->warpMouse(lastTouch.px, lastTouch.py); + event.mouse.x = lastTouch.px; + event.mouse.y = lastTouch.py; + event.type = Common::EVENT_MOUSEMOVE; + pushEventQueue(eventQueue, event); + } + + // Button events + if (keysPressed & KEY_R) { + if (inputMode == MODE_DRAG) { + inputMode = MODE_HOVER; + osys->displayMessageOnOSD("Hover Mode"); + } else { + inputMode = MODE_DRAG; + osys->displayMessageOnOSD("Drag Mode"); + } + } + if (keysPressed & KEY_A || keysPressed & KEY_DLEFT || keysReleased & KEY_A || keysReleased & KEY_DLEFT) { + // SIMULATE LEFT CLICK + event.mouse.x = lastTouch.px; + event.mouse.y = lastTouch.py; + if (keysPressed & KEY_A || keysPressed & KEY_DLEFT) + event.type = Common::EVENT_LBUTTONDOWN; + else + event.type = Common::EVENT_LBUTTONUP; + pushEventQueue(eventQueue, event); + } + if (keysPressed & KEY_X || keysPressed & KEY_DUP || keysReleased & KEY_X || keysReleased & KEY_DUP) { + // SIMULATE RIGHT CLICK + event.mouse.x = lastTouch.px; + event.mouse.y = lastTouch.py; + if (keysPressed & KEY_X || keysPressed & KEY_DUP) + event.type = Common::EVENT_RBUTTONDOWN; + else + event.type = Common::EVENT_RBUTTONUP; + pushEventQueue(eventQueue, event); + } + if (keysPressed & KEY_L) { + event.type = Common::EVENT_VIRTUAL_KEYBOARD; + pushEventQueue(eventQueue, event); + } + if (keysPressed & KEY_START) { + event.type = Common::EVENT_MAINMENU; + pushEventQueue(eventQueue, event); + } + if (keysPressed & KEY_SELECT) { + if (!optionMenuOpened) + optionMenuOpening = true; + } + if (keysPressed & KEY_B || keysReleased & KEY_B || keysPressed & KEY_DDOWN || keysReleased & KEY_DDOWN) { + if (keysPressed & KEY_B || keysPressed & KEY_DDOWN) + event.type = Common::EVENT_KEYDOWN; + else + event.type = Common::EVENT_KEYUP; + event.kbd.keycode = Common::KEYCODE_ESCAPE; + event.kbd.ascii = Common::ASCII_ESCAPE; + event.kbd.flags = 0; + pushEventQueue(eventQueue, event); + } + + // TODO: EVENT_PREDICTIVE_DIALOG + // EVENT_SCREEN_CHANGED + } +} + +static void aptHookFunc(APT_HookType hookType, void *param) { + OSystem_3DS *osys = (OSystem_3DS *)g_system; + + switch (hookType) { + case APTHOOK_ONSUSPEND: + case APTHOOK_ONSLEEP: + if (g_engine) + g_engine->pauseEngine(true); + osys->sleeping = true; + if (R_SUCCEEDED(gspLcdInit())) { + GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH); + gspLcdExit(); + } + break; + case APTHOOK_ONRESTORE: + case APTHOOK_ONWAKEUP: + if (g_engine) + g_engine->pauseEngine(false); + osys->sleeping = false; + loadConfig(); + break; + default: { + Common::StackLock lock(*eventMutex); + Common::Event event; + event.type = Common::EVENT_QUIT; + g_system->getEventManager()->pushEvent(event); + } + } +} + +static void timerThreadFunc(void *arg) { + OSystem_3DS *osys = (OSystem_3DS *)arg; + DefaultTimerManager *tm = (DefaultTimerManager *)osys->getTimerManager(); + while (!osys->exiting) { + g_system->delayMillis(10); + tm->handler(); + } +} + +void OSystem_3DS::initEvents() { + eventMutex = new Common::Mutex(); + s32 prio = 0; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + _timerThread = threadCreate(&timerThreadFunc, this, 32 * 1024, prio - 1, -2, false); + _eventThread = threadCreate(&eventThreadFunc, &_eventQueue, 32 * 1024, prio - 1, -2, false); + + aptHook(&cookie, aptHookFunc, this); +} + +void OSystem_3DS::destroyEvents() { + threadJoin(_timerThread, U64_MAX); + threadFree(_timerThread); + + threadJoin(_eventThread, U64_MAX); + threadFree(_eventThread); + delete eventMutex; +} + +void OSystem_3DS::transformPoint(touchPosition &point) { + if (!_overlayVisible) { + point.px = static_cast<float>(point.px) / _gameBottomTexture.getScaleX() - _gameBottomX; + point.py = static_cast<float>(point.py) / _gameBottomTexture.getScaleY() - _gameBottomY; + } +} + +void OSystem_3DS::displayMessageOnOSD(const char *msg) { + messageOSD = msg; + showMessageOSD = true; +} + +bool OSystem_3DS::pollEvent(Common::Event &event) { + if (showMessageOSD) { + showMessageOSD = false; + StatusMessageDialog dialog(messageOSD, 800); + dialog.runModal(); + } + + aptMainLoop(); // Call apt hook when necessary + + if (optionMenuOpening) { + optionMenuOpening = false; + OptionsDialog dialog; + if (g_engine) + g_engine->pauseEngine(true); + dialog.runModal(); + if (g_engine) + g_engine->pauseEngine(false); + } + + Common::StackLock lock(*eventMutex); + + if (_eventQueue.empty()) + return false; + + event = _eventQueue.pop(); + return true; +} + +} // namespace _3DS diff --git a/backends/platform/3ds/osystem-graphics.cpp b/backends/platform/3ds/osystem-graphics.cpp new file mode 100644 index 0000000000..ec60956805 --- /dev/null +++ b/backends/platform/3ds/osystem-graphics.cpp @@ -0,0 +1,517 @@ +/* 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 "backends/platform/3ds/osystem.h" +#include "backends/platform/3ds/shader_shbin.h" +#include "common/rect.h" +#include "options-dialog.h" +#include "config.h" + +// Used to transfer the final rendered display to the framebuffer +#define DISPLAY_TRANSFER_FLAGS \ + (GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | \ + GX_TRANSFER_RAW_COPY(0) | GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | \ + GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \ + GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) + +namespace _3DS { + +void OSystem_3DS::initGraphics() { + _pfGame = Graphics::PixelFormat::createFormatCLUT8(); + _pfGameTexture = Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0); + + C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); + + // Initialize the render targets + _renderTargetTop = + C3D_RenderTargetCreate(240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); + C3D_RenderTargetSetClear(_renderTargetTop, C3D_CLEAR_ALL, 0x0000000, 0); + C3D_RenderTargetSetOutput(_renderTargetTop, GFX_TOP, GFX_LEFT, + DISPLAY_TRANSFER_FLAGS); + + _renderTargetBottom = + C3D_RenderTargetCreate(240, 320, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); + C3D_RenderTargetSetClear(_renderTargetBottom, C3D_CLEAR_ALL, 0x00000000, 0); + C3D_RenderTargetSetOutput(_renderTargetBottom, GFX_BOTTOM, GFX_LEFT, + DISPLAY_TRANSFER_FLAGS); + + // Load and bind simple default shader (shader.v.pica) + _dvlb = DVLB_ParseFile((u32*)shader_shbin, shader_shbin_size); + shaderProgramInit(&_program); + shaderProgramSetVsh(&_program, &_dvlb->DVLE[0]); + C3D_BindProgram(&_program); + + _projectionLocation = shaderInstanceGetUniformLocation(_program.vertexShader, "projection"); + _modelviewLocation = shaderInstanceGetUniformLocation(_program.vertexShader, "modelView"); + + C3D_AttrInfo *attrInfo = C3D_GetAttrInfo(); + AttrInfo_Init(attrInfo); + AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position + AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 2); // v1=texcoord + + Mtx_OrthoTilt(&_projectionTop, 0.0, 400.0, 240.0, 0.0, 0.0, 1.0); + Mtx_OrthoTilt(&_projectionBottom, 0.0, 320.0, 240.0, 0.0, 0.0, 1.0); + + C3D_TexEnv *env = C3D_GetTexEnv(0); + C3D_TexEnvSrc(env, C3D_Both, GPU_TEXTURE0, 0, 0); + C3D_TexEnvOp(env, C3D_Both, 0, 0, 0); + C3D_TexEnvFunc(env, C3D_Both, GPU_REPLACE); + + C3D_DepthTest(false, GPU_GEQUAL, GPU_WRITE_ALL); + C3D_CullFace(GPU_CULL_NONE); +} + +void OSystem_3DS::destroyGraphics() { + _gameScreen.free(); + _gameTopTexture.free(); + _gameBottomTexture.free(); + _overlay.free(); + + shaderProgramFree(&_program); + DVLB_Free(_dvlb); + + C3D_RenderTargetDelete(_renderTargetTop); + C3D_RenderTargetDelete(_renderTargetBottom); + + C3D_Fini(); +} + +bool OSystem_3DS::hasFeature(OSystem::Feature f) { + return (f == OSystem::kFeatureCursorPalette || + f == OSystem::kFeatureOverlaySupportsAlpha); +} + +void OSystem_3DS::setFeatureState(OSystem::Feature f, bool enable) { + switch (f) { + case OSystem::kFeatureCursorPalette: + _cursorPaletteEnabled = enable; + flushCursor(); + break; + default: + break; + } +} + +bool OSystem_3DS::getFeatureState(OSystem::Feature f) { + switch (f) { + case OSystem::kFeatureCursorPalette: + return _cursorPaletteEnabled; + default: + return false; + } +} + +const OSystem::GraphicsMode * +OSystem_3DS::getSupportedGraphicsModes() const { + return s_graphicsModes; +} + +int OSystem_3DS::getDefaultGraphicsMode() const { + return GFX_LINEAR; +} + +bool OSystem_3DS::setGraphicsMode(int mode) { + return true; +} + +void OSystem_3DS::resetGraphicsScale() { + debug("resetGraphicsScale"); +} + +int OSystem_3DS::getGraphicsMode() const { + return GFX_LINEAR; +} +void OSystem_3DS::initSize(uint width, uint height, + const Graphics::PixelFormat *format) { + debug("3ds initsize w:%d h:%d", width, height); + _gameWidth = width; + _gameHeight = height; + _gameTopTexture.create(width, height, _pfGameTexture); + _overlay.create(getOverlayWidth(), getOverlayHeight(), _pfGameTexture); + + if (format) { + debug("pixelformat: %d %d %d %d %d", format->bytesPerPixel, format->rBits(), format->gBits(), format->bBits(), format->aBits());; + _pfGame = *format; + } + + _gameScreen.create(width, height, _pfGame); + + _focusDirty = true; + _focusRect = Common::Rect(_gameWidth, _gameHeight); + + updateSize(); +} + +void OSystem_3DS::updateSize() { + if (config.stretchToFit) { + _gameTopX = _gameTopY = _gameBottomX = _gameBottomY = 0; + _gameTopTexture.setScale(400.f / _gameWidth, 240.f / _gameHeight); + _gameBottomTexture.setScale(320.f / _gameWidth, 240.f / _gameHeight); + } else { + float ratio = static_cast<float>(_gameWidth) / _gameHeight; + + if (ratio > 400.f / 240.f) { + float r = 400.f / _gameWidth; + _gameTopTexture.setScale(r, r); + _gameTopX = 0; + _gameTopY = (240.f - r * _gameHeight) / 2.f; + } else { + float r = 240.f / _gameHeight; + _gameTopTexture.setScale(r, r); + _gameTopY = 0; + _gameTopX = (400.f - r * _gameWidth) / 2.f; + } + if (ratio > 320.f / 240.f) { + float r = 320.f / _gameWidth; + _gameBottomTexture.setScale(r, r); + _gameBottomX = 0; + _gameBottomY = (240.f - r * _gameHeight) / 2.f; + } else { + float r = 240.f / _gameHeight; + _gameBottomTexture.setScale(r, r); + _gameBottomY = 0; + _gameBottomX = (320.f - r * _gameWidth) / 2.f; + } + } + _gameTopTexture.setPosition(_gameTopX, _gameTopY); + _gameBottomTexture.setPosition(_gameBottomX, _gameBottomY); + if (_overlayVisible) + _cursorTexture.setScale(1.f, 1.f); + else if (config.screen == kScreenTop) + _cursorTexture.setScale(_gameTopTexture.getScaleX(), _gameTopTexture.getScaleY()); + else + _cursorTexture.setScale(_gameBottomTexture.getScaleX(), _gameBottomTexture.getScaleY()); +} + +Common::List<Graphics::PixelFormat> OSystem_3DS::getSupportedFormats() const { + Common::List<Graphics::PixelFormat> list; + list.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); // GPU_RGBA8 + list.push_back(Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); // GPU_RGB565 +// list.push_back(Graphics::PixelFormat(3, 0, 0, 0, 8, 0, 8, 16, 0)); // GPU_RGB8 + list.push_back(Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)); // RGB555 (needed for FMTOWNS?) + list.push_back(Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0)); // GPU_RGBA5551 + list.push_back(Graphics::PixelFormat::createFormatCLUT8()); + return list; +} + +void OSystem_3DS::beginGFXTransaction() { + // +} +OSystem::TransactionError OSystem_3DS::endGFXTransaction() { + return OSystem::kTransactionSuccess; +} + +void OSystem_3DS::setPalette(const byte *colors, uint start, uint num) { + assert(start + num <= 256); + memcpy(_palette + 3 * start, colors, 3 * num); + + // Manually update all color that were changed + if (_gameScreen.format.bytesPerPixel == 1) { + flushGameScreen(); + } +} +void OSystem_3DS::grabPalette(byte *colors, uint start, uint num) { + assert(start + num <= 256); + memcpy(colors, _palette + 3 * start, 3 * num); +} + +void OSystem_3DS::copyRectToScreen(const void *buf, int pitch, int x, + int y, int w, int h) { + Common::Rect rect(x, y, x+w, y+h); + _gameScreen.copyRectToSurface(buf, pitch, x, y, w, h); + Graphics::Surface subSurface = _gameScreen.getSubArea(rect); + + Graphics::Surface *convertedSubSurface = subSurface.convertTo(_pfGameTexture, _palette); + _gameTopTexture.copyRectToSurface(*convertedSubSurface, x, y, Common::Rect(w, h)); + + convertedSubSurface->free(); + delete convertedSubSurface; + _gameTopTexture.markDirty(); +} + +void OSystem_3DS::flushGameScreen() { + Graphics::Surface *converted = _gameScreen.convertTo(_pfGameTexture, _palette); + _gameTopTexture.copyRectToSurface(*converted, 0, 0, Common::Rect(converted->w, converted->h)); + _gameTopTexture.markDirty(); + converted->free(); + delete converted; +} + +Graphics::Surface *OSystem_3DS::lockScreen() { + return &_gameScreen; +} +void OSystem_3DS::unlockScreen() { + flushGameScreen(); +} + +void OSystem_3DS::updateScreen() { + + if (sleeping || exiting) + return; + +// updateFocus(); + + C3D_FrameBegin(C3D_FRAME_SYNCDRAW); + // Render top screen + C3D_FrameDrawOn(_renderTargetTop); + if (config.screen == kScreenTop || config.screen == kScreenBoth) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _projectionLocation, &_projectionTop); + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _gameTopTexture.getMatrix()); + _gameTopTexture.render(); + _gameTopTexture.render(); + if (_overlayVisible && config.screen == kScreenTop) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _overlay.getMatrix()); + _overlay.render(); + } + if (_cursorVisible && config.showCursor && config.screen == kScreenTop) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _cursorTexture.getMatrix()); + _cursorTexture.render(); + } + } + + // Render bottom screen + C3D_FrameDrawOn(_renderTargetBottom); + if (config.screen == kScreenBottom || config.screen == kScreenBoth) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _projectionLocation, &_projectionBottom); + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _gameBottomTexture.getMatrix()); + _gameTopTexture.render(); + _gameTopTexture.render(); + if (_overlayVisible) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _overlay.getMatrix()); + _overlay.render(); + } + if (_cursorVisible && config.showCursor) { + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _cursorTexture.getMatrix()); + _cursorTexture.render(); + } + } + C3D_FrameEnd(0); +} + +void OSystem_3DS::setShakePos(int shakeOffset) { + // TODO: implement this in overlay, top screen, and mouse too + _screenShakeOffset = shakeOffset; + _gameTopTexture.setPosition(_gameTopX, _gameTopY + _gameTopTexture.getScaleY() * shakeOffset); + _gameBottomTexture.setPosition(_gameBottomX, _gameBottomY + _gameBottomTexture.getScaleY() * shakeOffset); +} + +void OSystem_3DS::setFocusRectangle(const Common::Rect &rect) { + debug("setfocus: %d %d %d %d", rect.left, rect.top, rect.width(), rect.height()); + _focusRect = rect; + _focusDirty = true; + _focusClearTime = 0; +} + +void OSystem_3DS::clearFocusRectangle() { + _focusClearTime = getMillis(); +} + +void OSystem_3DS::updateFocus() { + + if (_focusClearTime && getMillis() - _focusClearTime > 5000) { + _focusClearTime = 0; + _focusDirty = true; + _focusRect = Common::Rect(_gameWidth, _gameHeight); + } + + if (_focusDirty) { + float duration = 1.f / 20.f; // Focus animation in frame duration + float w = 400.f; + float h = 240.f; + float ratio = _focusRect.width() / _focusRect.height(); + if (ratio > w/h) { + _focusTargetScaleX = w / _focusRect.width(); + float newHeight = (float)_focusRect.width() / w/h; + _focusTargetScaleY = h / newHeight; + _focusTargetPosX = _focusTargetScaleX * _focusRect.left; + _focusTargetPosY = _focusTargetScaleY * ((float)_focusRect.top - (newHeight - _focusRect.height())/2.f); + } else { + _focusTargetScaleY = h / _focusRect.height(); + float newWidth = (float)_focusRect.height() * w/h; + _focusTargetScaleX = w / newWidth; + _focusTargetPosY = _focusTargetScaleY * _focusRect.top; + _focusTargetPosX = _focusTargetScaleX * ((float)_focusRect.left - (newWidth - _focusRect.width())/2.f); + } + if (_focusTargetPosX < 0 && _focusTargetScaleY != 240.f / _gameHeight) + _focusTargetPosX = 0; + if (_focusTargetPosY < 0 && _focusTargetScaleX != 400.f / _gameWidth) + _focusTargetPosY = 0; + _focusStepPosX = duration * (_focusTargetPosX - _focusPosX); + _focusStepPosY = duration * (_focusTargetPosY - _focusPosY); + _focusStepScaleX = duration * (_focusTargetScaleX - _focusScaleX); + _focusStepScaleY = duration * (_focusTargetScaleY - _focusScaleY); + } + + if (_focusDirty || _focusPosX != _focusTargetPosX || _focusPosY != _focusTargetPosY || + _focusScaleX != _focusTargetScaleX || _focusScaleY != _focusTargetScaleY) { + _focusDirty = false; + + if ((_focusStepPosX > 0 && _focusPosX > _focusTargetPosX) || (_focusStepPosX < 0 && _focusPosX < _focusTargetPosX)) + _focusPosX = _focusTargetPosX; + else if (_focusPosX != _focusTargetPosX) + _focusPosX += _focusStepPosX; + + if ((_focusStepPosY > 0 && _focusPosY > _focusTargetPosY) || (_focusStepPosY < 0 && _focusPosY < _focusTargetPosY)) + _focusPosY = _focusTargetPosY; + else if (_focusPosY != _focusTargetPosY) + _focusPosY += _focusStepPosY; + + if ((_focusStepScaleX > 0 && _focusScaleX > _focusTargetScaleX) || (_focusStepScaleX < 0 && _focusScaleX < _focusTargetScaleX)) + _focusScaleX = _focusTargetScaleX; + else if (_focusScaleX != _focusTargetScaleX) + _focusScaleX += _focusStepScaleX; + + if ((_focusStepScaleY > 0 && _focusScaleY > _focusTargetScaleY) || (_focusStepScaleY < 0 && _focusScaleY < _focusTargetScaleY)) + _focusScaleY = _focusTargetScaleY; + else if (_focusScaleY != _focusTargetScaleY) + _focusScaleY += _focusStepScaleY; + + Mtx_Identity(&_focusMatrix); + Mtx_Translate(&_focusMatrix, -_focusPosX, -_focusPosY, 0); + Mtx_Scale(&_focusMatrix, _focusScaleX, _focusScaleY, 1.f); + } +} + +void OSystem_3DS::showOverlay() { + _overlayVisible = true; + updateSize(); + updateScreen(); +} + +void OSystem_3DS::hideOverlay() { + _overlayVisible = false; + updateSize(); + updateScreen(); +} + +Graphics::PixelFormat OSystem_3DS::getOverlayFormat() const { + return _pfGameTexture; +} + +void OSystem_3DS::clearOverlay() { + _overlay.clear(); +} + +void OSystem_3DS::grabOverlay(void *buf, int pitch) { + for (int y = 0; y < getOverlayHeight(); ++y) { + memcpy(buf, _overlay.getBasePtr(0, y), pitch); + } +} + +void OSystem_3DS::copyRectToOverlay(const void *buf, int pitch, int x, + int y, int w, int h) { + _overlay.copyRectToSurface(buf, pitch, x, y, w, h); + _overlay.markDirty(); +} + +int16 OSystem_3DS::getOverlayHeight() { + return 240; +} + +int16 OSystem_3DS::getOverlayWidth() { + return 320; +} + +bool OSystem_3DS::showMouse(bool visible) { + _cursorVisible = visible; + flushCursor(); + return !visible; +} + +void OSystem_3DS::warpMouse(int x, int y) { + _cursorX = x; + _cursorY = y; + warning("x:%d y:%d", x, y); + // TODO: adjust for _cursorScalable ? + int offsetx = 0; + int offsety = 0; + x -= _cursorHotspotX; + y -= _cursorHotspotY; + if (!_overlayVisible) { + offsetx += config.screen == kScreenTop ? _gameTopX : _gameBottomX; + offsety += config.screen == kScreenTop ? _gameTopY : _gameBottomY; + } + float scalex = config.screen == kScreenTop ? (float)_gameTopTexture.actualWidth / _gameWidth : 1.f; + float scaley = config.screen == kScreenTop ? (float)_gameTopTexture.actualHeight / _gameHeight : 1.f; + _cursorTexture.setPosition(scalex * x + offsetx, + scaley * y + offsety); +} + +void OSystem_3DS::setCursorDelta(float deltaX, float deltaY) { + _cursorDeltaX = deltaX; + _cursorDeltaY = deltaY; +} + +void OSystem_3DS::setMouseCursor(const void *buf, uint w, uint h, + int hotspotX, int hotspotY, + uint32 keycolor, bool dontScale, + const Graphics::PixelFormat *format) { + _cursorScalable = !dontScale; + _cursorHotspotX = hotspotX; + _cursorHotspotY = hotspotY; + _cursorKeyColor = keycolor; + _pfCursor = !format ? Graphics::PixelFormat::createFormatCLUT8() : *format; + + if (w != _cursor.w || h != _cursor.h || _cursor.format != _pfCursor) { + _cursor.create(w, h, _pfCursor); + _cursorTexture.create(w, h, _pfGameTexture); + } + + _cursor.copyRectToSurface(buf, w, 0, 0, w, h); + flushCursor(); + + warpMouse(_cursorX, _cursorY); +} + +void OSystem_3DS::setCursorPalette(const byte *colors, uint start, uint num) { + assert(start + num <= 256); + memcpy(_cursorPalette + 3 * start, colors, 3 * num); + _cursorPaletteEnabled = true; + flushCursor(); +} + +void OSystem_3DS::flushCursor() { + if (_cursor.getPixels()) { + Graphics::Surface *converted = _cursor.convertTo(_pfGameTexture, _cursorPaletteEnabled ? _cursorPalette : _palette); + _cursorTexture.copyRectToSurface(*converted, 0, 0, Common::Rect(converted->w, converted->h)); + _cursorTexture.markDirty(); + converted->free(); + delete converted; + + if (_pfCursor.bytesPerPixel == 1) { + uint* dest = (uint*) _cursorTexture.getPixels(); + byte* src = (byte*) _cursor.getPixels(); + for (int y = 0; y < _cursor.h; ++y) { + for (int x = 0; x < _cursor.w; ++x) { + if (*src++ == _cursorKeyColor) + *dest++ = 0; + else + dest++; + } + dest += _cursorTexture.w - _cursorTexture.actualWidth; + } + } + } +} + +} // namespace _3DS diff --git a/backends/platform/3ds/osystem.cpp b/backends/platform/3ds/osystem.cpp new file mode 100644 index 0000000000..e19ff8f8de --- /dev/null +++ b/backends/platform/3ds/osystem.cpp @@ -0,0 +1,193 @@ +/* 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_time_h +#define FORBIDDEN_SYMBOL_EXCEPTION_unistd_h + +#include "osystem.h" + +#include "backends/saves/default/default-saves.h" +#include "backends/timer/default/default-timer.h" +#include "backends/events/default/default-events.h" +#include "audio/mixer_intern.h" +#include "common/scummsys.h" +#include "common/config-manager.h" +#include "common/str.h" +#include "config.h" + +#include "backends/fs/posix/posix-fs-factory.h" +#include "backends/fs/posix/posix-fs.h" +#include <unistd.h> +#include <time.h> + +namespace _3DS { + +OSystem_3DS::OSystem_3DS(): + _focusDirty(true), + _focusRect(Common::Rect(1, 1)), + _focusPosX(0), + _focusPosY(0), + _focusTargetPosX(0), + _focusTargetPosY(0), + _focusStepPosX(0), + _focusStepPosY(0), + _focusScaleX(1.f), + _focusScaleY(1.f), + _focusTargetScaleX(1.f), + _focusTargetScaleY(1.f), + _focusStepScaleX(0.f), + _focusStepScaleY(0.f), + _focusClearTime(0), + _cursorPaletteEnabled(false), + _cursorVisible(false), + _cursorScalable(false), + _cursorX(0), + _cursorY(0), + _cursorHotspotX(0), + _cursorHotspotY(0), + _gameTopX(0), + _gameTopY(0), + _gameBottomX(0), + _gameBottomY(0), + _gameWidth(320), + _gameHeight(240), + _overlayVisible(false), + exiting(false), + sleeping(false) +{ + chdir("sdmc:/"); + _fsFactory = new POSIXFilesystemFactory(); + Posix::assureDirectoryExists("/3ds/scummvm/saves/"); +} + +OSystem_3DS::~OSystem_3DS() { + exiting = true; + destroyEvents(); + destroyAudio(); + destroyGraphics(); + + delete _timerManager; + _timerManager = 0; +} + +void OSystem_3DS::quit() { + printf("OSystem_3DS::quit()\n"); +} + +void OSystem_3DS::initBackend() { + loadConfig(); + ConfMan.registerDefault("fullscreen", true); + ConfMan.registerDefault("aspect_ratio", true); + if (!ConfMan.hasKey("vkeybd_pack_name")) + ConfMan.set("vkeybd_pack_name", "vkeybd_small"); + if (!ConfMan.hasKey("vkeybdpath")) + ConfMan.set("vkeybdpath", "/3ds/scummvm/kb"); + if (!ConfMan.hasKey("themepath")) + ConfMan.set("themepath", "/3ds/scummvm"); + if (!ConfMan.hasKey("gui_theme")) + ConfMan.set("gui_theme", "builtin"); + + _timerManager = new DefaultTimerManager(); + _savefileManager = new DefaultSaveFileManager("/3ds/scummvm/saves/"); + + initGraphics(); + initAudio(); + initEvents(); + EventsBaseBackend::initBackend(); +} + +void OSystem_3DS::updateConfig() { + if (_gameScreen.getPixels()) { + updateSize(); + warpMouse(_cursorX, _cursorY); + } +} + +Common::String OSystem_3DS::getDefaultConfigFileName() { + return "/3ds/scummvm/scummvm.ini"; +} + +uint32 OSystem_3DS::getMillis(bool skipRecord) { + return svcGetSystemTick() / TICKS_PER_MSEC; +} + +void OSystem_3DS::delayMillis(uint msecs) { + svcSleepThread(msecs * 1000000); +} + +void OSystem_3DS::getTimeAndDate(TimeDate& td) const { + time_t curTime = time(0); + struct tm t = *localtime(&curTime); + td.tm_sec = t.tm_sec; + td.tm_min = t.tm_min; + td.tm_hour = t.tm_hour; + td.tm_mday = t.tm_mday; + td.tm_mon = t.tm_mon; + td.tm_year = t.tm_year; + td.tm_wday = t.tm_wday; +} + +OSystem::MutexRef OSystem_3DS::createMutex() { + RecursiveLock *mutex = new RecursiveLock(); + RecursiveLock_Init(mutex); + return (OSystem::MutexRef) mutex; +} +void OSystem_3DS::lockMutex(MutexRef mutex) { + RecursiveLock_Lock((RecursiveLock*)mutex); +} +void OSystem_3DS::unlockMutex(MutexRef mutex) { + RecursiveLock_Unlock((RecursiveLock*)mutex); +} +void OSystem_3DS::deleteMutex(MutexRef mutex) { + delete (RecursiveLock*)mutex; +} + +Common::String OSystem_3DS::getSystemLanguage() const { + u8 langcode; + CFGU_GetSystemLanguage(&langcode); + switch (langcode) { + case CFG_LANGUAGE_JP: return "ja_JP"; + case CFG_LANGUAGE_EN: return "en_US"; + case CFG_LANGUAGE_FR: return "fr_FR"; + case CFG_LANGUAGE_DE: return "de_DE"; + case CFG_LANGUAGE_IT: return "it_IT"; + case CFG_LANGUAGE_ES: return "es_ES"; + case CFG_LANGUAGE_ZH: return "zh_CN"; + case CFG_LANGUAGE_KO: return "ko_KR"; + case CFG_LANGUAGE_NL: return "nl_NL"; + case CFG_LANGUAGE_PT: return "pt_BR"; + case CFG_LANGUAGE_RU: return "ru_RU"; + case CFG_LANGUAGE_TW: return "zh_HK"; + default: return "en_US"; + } +} + +void OSystem_3DS::fatalError() { + printf("FatalError!\n"); +} + +void OSystem_3DS::logMessage(LogMessageType::Type type, const char *message) { + printf("3DS log: %s\n", message); +} + +} // namespace _3DS diff --git a/backends/platform/3ds/osystem.h b/backends/platform/3ds/osystem.h new file mode 100644 index 0000000000..5df49fe593 --- /dev/null +++ b/backends/platform/3ds/osystem.h @@ -0,0 +1,221 @@ +/* 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_3DS_H +#define PLATFORM_3DS_H + +#include <citro3d.h> +#include "backends/mutex/mutex.h" +#include "backends/base-backend.h" +#include "graphics/palette.h" +#include "base/main.h" +#include "audio/mixer_intern.h" +#include "backends/graphics/graphics.h" +#include "backends/platform/3ds/sprite.h" +#include "common/rect.h" +#include "common/queue.h" + +#define TICKS_PER_MSEC 268123 + +namespace _3DS { + +enum { + GFX_LINEAR = 0, + GFX_NEAREST = 1 +}; + +enum InputMode { + MODE_HOVER, + MODE_DRAG, +}; + +static const OSystem::GraphicsMode s_graphicsModes[] = { + {"default", "Default Test", GFX_LINEAR}, + { 0, 0, 0 } +}; + +class OSystem_3DS : public EventsBaseBackend, public PaletteManager { +public: + OSystem_3DS(); + virtual ~OSystem_3DS(); + + volatile bool exiting; + volatile bool sleeping; + + virtual void initBackend(); + + virtual bool hasFeature(OSystem::Feature f); + virtual void setFeatureState(OSystem::Feature f, bool enable); + virtual bool getFeatureState(OSystem::Feature f); + + virtual bool pollEvent(Common::Event &event); + + virtual uint32 getMillis(bool skipRecord = false); + virtual void delayMillis(uint msecs); + virtual void getTimeAndDate(TimeDate &t) const; + + virtual MutexRef createMutex(); + virtual void lockMutex(MutexRef mutex); + virtual void unlockMutex(MutexRef mutex); + virtual void deleteMutex(MutexRef mutex); + + virtual void logMessage(LogMessageType::Type type, const char *message); + + virtual Audio::Mixer *getMixer(); + virtual PaletteManager *getPaletteManager() { return this; } + virtual Common::String getSystemLanguage() const; + virtual void fatalError(); + virtual void quit(); + + virtual Common::String getDefaultConfigFileName(); + + // Graphics + virtual const OSystem::GraphicsMode *getSupportedGraphicsModes() const; + int getDefaultGraphicsMode() const; + bool setGraphicsMode(int mode); + void resetGraphicsScale(); + int getGraphicsMode() const; + inline Graphics::PixelFormat getScreenFormat() const { return _pfGame; } + virtual Common::List<Graphics::PixelFormat> getSupportedFormats() const; + void initSize(uint width, uint height, + const Graphics::PixelFormat *format = NULL); + virtual int getScreenChangeID() const { return 0; }; + + void beginGFXTransaction(); + OSystem::TransactionError endGFXTransaction(); + int16 getHeight(){ return _gameHeight; } + int16 getWidth(){ return _gameWidth; } + void setPalette(const byte *colors, uint start, uint num); + void grabPalette(byte *colors, uint start, uint num); + void copyRectToScreen(const void *buf, int pitch, int x, int y, int w, + int h); + Graphics::Surface *lockScreen(); + void unlockScreen(); + void updateScreen(); + void setShakePos(int shakeOffset); + void setFocusRectangle(const Common::Rect &rect); + void clearFocusRectangle(); + void showOverlay(); + void hideOverlay(); + Graphics::PixelFormat getOverlayFormat() const; + void clearOverlay(); + void grabOverlay(void *buf, int pitch); + void copyRectToOverlay(const void *buf, int pitch, int x, int y, int w, + int h); + virtual int16 getOverlayHeight(); + virtual int16 getOverlayWidth(); + virtual void displayMessageOnOSD(const char *msg); + + bool showMouse(bool visible); + void warpMouse(int x, int y); + void setMouseCursor(const void *buf, uint w, uint h, int hotspotX, + int hotspotY, uint32 keycolor, bool dontScale = false, + const Graphics::PixelFormat *format = NULL); + void setCursorPalette(const byte *colors, uint start, uint num); + + // Transform point from touchscreen coords into gamescreen coords + void transformPoint(touchPosition &point); + + void setCursorDelta(float deltaX, float deltaY); + + void updateFocus(); + void updateConfig(); + void updateSize(); + +private: + void initGraphics(); + void destroyGraphics(); + void initAudio(); + void destroyAudio(); + void initEvents(); + void destroyEvents(); + + void flushGameScreen(); + void flushCursor(); + +protected: + Audio::MixerImpl *_mixer; + +private: + u16 _gameWidth, _gameHeight; + u16 _gameTopX, _gameTopY; + u16 _gameBottomX, _gameBottomY; + + // Audio + Thread audioThread; + + // Graphics + Graphics::PixelFormat _pfGame; + Graphics::PixelFormat _pfGameTexture; + Graphics::PixelFormat _pfCursor; + byte _palette[3 * 256]; + byte _cursorPalette[3 * 256]; + + Graphics::Surface _gameScreen; + Sprite _gameTopTexture; + Sprite _gameBottomTexture; + Sprite _overlay; + + int _screenShakeOffset; + bool _overlayVisible; + + DVLB_s *_dvlb; + shaderProgram_s _program; + int _projectionLocation; + int _modelviewLocation; + C3D_Mtx _projectionTop; + C3D_Mtx _projectionBottom; + C3D_RenderTarget* _renderTargetTop; + C3D_RenderTarget* _renderTargetBottom; + + // Focus + Common::Rect _focusRect; + bool _focusDirty; + C3D_Mtx _focusMatrix; + int _focusPosX, _focusPosY; + int _focusTargetPosX, _focusTargetPosY; + float _focusStepPosX, _focusStepPosY; + float _focusScaleX, _focusScaleY; + float _focusTargetScaleX, _focusTargetScaleY; + float _focusStepScaleX, _focusStepScaleY; + uint32 _focusClearTime; + + // Events + Thread _eventThread; + Thread _timerThread; + Common::Queue<Common::Event> _eventQueue; + + // Cursor + Graphics::Surface _cursor; + Sprite _cursorTexture; + bool _cursorPaletteEnabled; + bool _cursorVisible; + bool _cursorScalable; + float _cursorX, _cursorY; + float _cursorDeltaX, _cursorDeltaY; + int _cursorHotspotX, _cursorHotspotY; + uint32 _cursorKeyColor; +}; + +} // namespace _3DS + +#endif diff --git a/backends/platform/3ds/shader.v.pica b/backends/platform/3ds/shader.v.pica new file mode 100644 index 0000000000..2d18985622 --- /dev/null +++ b/backends/platform/3ds/shader.v.pica @@ -0,0 +1,59 @@ +;* 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. +;* + +; Uniforms +.fvec projection[4], modelView[4] + +; Constants +.constf myconst(0.0, 1.0, -1.0, 0.1) +.alias zeros myconst.xxxx ; Vector full of zeros +.alias ones myconst.yyyy ; Vector full of ones + +; Outputs +.out outpos position +.out outtex texcoord0 + +; Inputs (defined as aliases for convenience) +.alias inpos v0 +.alias intex v1 + +.proc main + ; Force the w component of inpos to be 1.0 + mov r0.xyz, inpos + mov r0.w, ones + + ; r1 = modelView * inpos + dp4 r1.x, modelView[0], r0 + dp4 r1.y, modelView[1], r0 + dp4 r1.z, modelView[2], r0 + dp4 r1.w, modelView[3], r0 + + ; outpos = projection * r1 + dp4 outpos.x, projection[0], r1 + dp4 outpos.y, projection[1], r1 + dp4 outpos.z, projection[2], r1 + dp4 outpos.w, projection[3], r1 + + mov outtex, intex + + end +.end + diff --git a/backends/platform/3ds/sprite.cpp b/backends/platform/3ds/sprite.cpp new file mode 100644 index 0000000000..842729c051 --- /dev/null +++ b/backends/platform/3ds/sprite.cpp @@ -0,0 +1,144 @@ +/* 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 "backends/platform/3ds/sprite.h" +#include "common/util.h" +#include <3ds.h> + +static uint nextHigher2(uint v) { + if (v == 0) + return 1; + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + return ++v; +} + +Sprite::Sprite() + : dirtyPixels(true) + , dirtyMatrix(true) + , actualWidth(0) + , actualHeight(0) + , posX(0) + , posY(0) + , scaleX(1.f) + , scaleY(1.f) +{ + Mtx_Identity(&modelview); + + vertices = (vertex *)linearAlloc(sizeof(vertex) * 4); +} + +Sprite::~Sprite() { + // +} + +void Sprite::create(uint16 width, uint16 height, const Graphics::PixelFormat &f) { + free(); + + actualWidth = width; + actualHeight = height; + format = f; + w = MAX(nextHigher2(width), 64u); + h = MAX(nextHigher2(height), 64u); + pitch = w * format.bytesPerPixel; + dirtyPixels = true; + + if (width && height) { + pixels = linearAlloc(h * pitch); + C3D_TexInit(&texture, w, h, GPU_RGBA8); + C3D_TexSetFilter(&texture, GPU_LINEAR, GPU_LINEAR); + assert(pixels && texture.data); + clear(); + } + + float x = 0.f, y = 0.f; + float u = (float)width/w; + float v = (float)height/h; + vertex tmp[4] = { + {{x, y, 0.5f}, {0, 0}}, + {{x+width, y, 0.5f}, {u, 0}}, + {{x, y+height, 0.5f}, {0, v}}, + {{x+width, y+height, 0.5f}, {u, v}}, + }; + memcpy(vertices, tmp, sizeof(vertex) * 4); +} + + +void Sprite::free() { + linearFree(vertices); + linearFree(pixels); + C3D_TexDelete(&texture); + pixels = 0; + w = h = pitch = 0; + actualWidth = actualHeight = 0; + format = Graphics::PixelFormat(); +} + +void Sprite::convertToInPlace(const Graphics::PixelFormat &dstFormat, const byte *palette) { + // +} + +void Sprite::render() { + if (dirtyPixels) { + dirtyPixels = false; + GSPGPU_FlushDataCache(pixels, w * h * format.bytesPerPixel); + C3D_SafeDisplayTransfer((u32*)pixels, GX_BUFFER_DIM(w, h), (u32*)texture.data, GX_BUFFER_DIM(w, h), TEXTURE_TRANSFER_FLAGS); + gspWaitForPPF(); + } + C3D_TexBind(0, &texture); + + C3D_BufInfo *bufInfo = C3D_GetBufInfo(); + BufInfo_Init(bufInfo); + BufInfo_Add(bufInfo, vertices, sizeof(vertex), 2, 0x10); + C3D_DrawArrays(GPU_TRIANGLE_STRIP, 0, 4); +} + +void Sprite::clear(uint32 color) { + dirtyPixels = true; + memset(pixels, color, w * h * format.bytesPerPixel); +} + +void Sprite::setScale (float x, float y) { + scaleX = x; + scaleY = y; + dirtyMatrix = true; +} + +void Sprite::setPosition(int x, int y) { + posX = x; + posY = y; + dirtyMatrix = true; +} + +C3D_Mtx* Sprite::getMatrix() { + if (dirtyMatrix) { + dirtyMatrix = false; + Mtx_Identity(&modelview); + Mtx_Scale(&modelview, scaleX, scaleY, 1.f); + Mtx_Translate(&modelview, posX, posY, 0); + } + return &modelview; +} diff --git a/backends/platform/3ds/sprite.h b/backends/platform/3ds/sprite.h new file mode 100644 index 0000000000..129c2689e7 --- /dev/null +++ b/backends/platform/3ds/sprite.h @@ -0,0 +1,71 @@ +/* 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 GRAPHICS_SPRITE_3DS_H +#define GRAPHICS_SPRITE_3DS_H + +#include <citro3d.h> +#include "graphics/surface.h" + +#define TEXTURE_TRANSFER_FLAGS \ + (GX_TRANSFER_FLIP_VERT(1) | GX_TRANSFER_OUT_TILED(1) | GX_TRANSFER_RAW_COPY(0) | \ + GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGBA8) | \ + GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) + +typedef struct { + float position[3]; + float texcoord[2]; +} vertex; + +class Sprite : public Graphics::Surface { +public: + Sprite(); + ~Sprite(); + void create(uint16 width, uint16 height, const Graphics::PixelFormat &format); + void free(); + void convertToInPlace(const Graphics::PixelFormat &dstFormat, const byte *palette = 0); + void render(); + void clear(uint32 color = 0); + void markDirty(){ dirtyPixels = true; } + + void setPosition(int x, int y); + void setScale(float x, float y); + float getScaleX(){ return scaleX; } + float getScaleY(){ return scaleY; } + C3D_Mtx* getMatrix(); + + uint16 actualWidth; + uint16 actualHeight; + +private: + bool dirtyPixels; + bool dirtyMatrix; + C3D_Mtx modelview; + C3D_Tex texture; + vertex* vertices; + int posX; + int posY; + float scaleX; + float scaleY; +}; + +#endif diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp index 798772cc24..4e4417400c 100644 --- a/backends/platform/android/android.cpp +++ b/backends/platform/android/android.cpp @@ -401,7 +401,8 @@ bool OSystem_Android::hasFeature(Feature f) { f == kFeatureAspectRatioCorrection || f == kFeatureCursorPalette || f == kFeatureVirtualKeyboard || - f == kFeatureOverlaySupportsAlpha); + f == kFeatureOverlaySupportsAlpha || + f == kFeatureOpenUrl); } void OSystem_Android::setFeatureState(Feature f, bool enable) { @@ -586,6 +587,10 @@ Common::String OSystem_Android::getSystemLanguage() const { getSystemProperty("persist.sys.country").c_str()); } +bool OSystem_Android::openUrl(const Common::String &url) { + return JNI::openUrl(url.c_str()); +} + Common::String OSystem_Android::getSystemProperty(const char *name) const { char value[PROP_VALUE_MAX]; diff --git a/backends/platform/android/android.h b/backends/platform/android/android.h index ade84dd42d..2935d96381 100644 --- a/backends/platform/android/android.h +++ b/backends/platform/android/android.h @@ -286,6 +286,7 @@ public: virtual void logMessage(LogMessageType::Type type, const char *message); virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); + virtual bool openUrl(const Common::String &url); virtual Common::String getSystemLanguage() const; }; diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk index e11ad0724e..4a29526941 100644 --- a/backends/platform/android/android.mk +++ b/backends/platform/android/android.mk @@ -1,9 +1,9 @@ # Android specific build targets # These must be incremented for each market upload -ANDROID_VERSIONCODE = 6 +ANDROID_VERSIONCODE = 16 -ANDROID_TARGET_VERSION = 14 +ANDROID_TARGET_VERSION = 23 NDK_BUILD = $(ANDROID_NDK)/ndk-build APP_ABI=$(ABI) SDK_ANDROID = $(ANDROID_SDK)/tools/android @@ -20,6 +20,7 @@ RESOURCES = \ $(PATH_BUILD_RES)/layout/main.xml \ $(PATH_BUILD_RES)/drawable/scummvm.png \ $(PATH_BUILD_RES)/drawable/scummvm_big.png \ + $(PATH_BUILD_RES)/drawable-xhdpi/leanback_icon.png \ $(PATH_BUILD_RES)/drawable-xhdpi/ouya_icon.png DIST_ANDROID_MK = $(PATH_DIST)/jni/Android.mk diff --git a/backends/platform/android/asset-archive.cpp b/backends/platform/android/asset-archive.cpp index 6680081c16..0ee6efc111 100644 --- a/backends/platform/android/asset-archive.cpp +++ b/backends/platform/android/asset-archive.cpp @@ -22,8 +22,6 @@ #if defined(__ANDROID__) -#include <jni.h> - #include <sys/types.h> #include <unistd.h> @@ -37,443 +35,112 @@ #include "backends/platform/android/jni.h" #include "backends/platform/android/asset-archive.h" -// Must match android.content.res.AssetManager.ACCESS_* -const jint ACCESS_UNKNOWN = 0; -const jint ACCESS_RANDOM = 1; +#include <android/asset_manager.h> +#include <android/asset_manager_jni.h> -// This might be useful to someone else. Assumes markSupported() == true. -class JavaInputStream : public Common::SeekableReadStream { +class AssetInputStream : public Common::SeekableReadStream { public: - JavaInputStream(JNIEnv *env, jobject is); - virtual ~JavaInputStream(); - - virtual bool eos() const { - return _eos; - } + AssetInputStream(AAssetManager *as, const Common::String &path); + virtual ~AssetInputStream(); - virtual bool err() const { - return _err; - } + virtual bool eos() const { return _eos; } - virtual void clearErr() { - _eos = _err = false; - } + virtual void clearErr() {_eos = false; } virtual uint32 read(void *dataPtr, uint32 dataSize); - virtual int32 pos() const { - return _pos; - } + virtual int32 pos() const { return _pos; } - virtual int32 size() const { - return _len; - } + virtual int32 size() const { return _len; } virtual bool seek(int32 offset, int whence = SEEK_SET); private: - void close(JNIEnv *env); - - jmethodID MID_mark; - jmethodID MID_available; - jmethodID MID_close; - jmethodID MID_read; - jmethodID MID_reset; - jmethodID MID_skip; + void close(); + AAsset *_asset; - jobject _input_stream; - jsize _buflen; - jbyteArray _buf; uint32 _pos; - jint _len; + uint32 _len; bool _eos; - bool _err; }; -JavaInputStream::JavaInputStream(JNIEnv *env, jobject is) : - _eos(false), - _err(false), - _pos(0) -{ - _input_stream = env->NewGlobalRef(is); - _buflen = 8192; - jobject buf = env->NewByteArray(_buflen); - _buf = (jbyteArray)env->NewGlobalRef(buf); - env->DeleteLocalRef(buf); - - jclass cls = env->GetObjectClass(_input_stream); - MID_mark = env->GetMethodID(cls, "mark", "(I)V"); - assert(MID_mark); - MID_available = env->GetMethodID(cls, "available", "()I"); - assert(MID_available); - MID_close = env->GetMethodID(cls, "close", "()V"); - assert(MID_close); - MID_read = env->GetMethodID(cls, "read", "([BII)I"); - assert(MID_read); - MID_reset = env->GetMethodID(cls, "reset", "()V"); - assert(MID_reset); - MID_skip = env->GetMethodID(cls, "skip", "(J)J"); - assert(MID_skip); - env->DeleteLocalRef(cls); - - // Mark start of stream, so we can reset back to it. - // readlimit is set to something bigger than anything we might - // want to seek within. - env->CallVoidMethod(_input_stream, MID_mark, 10 * 1024 * 1024); - _len = env->CallIntMethod(_input_stream, MID_available); +AssetInputStream::AssetInputStream(AAssetManager *as, const Common::String &path) : + _eos(false), _pos(0) { + _asset = AAssetManager_open(as, path.c_str(), AASSET_MODE_RANDOM); + _len = AAsset_getLength(_asset); } -JavaInputStream::~JavaInputStream() { - JNIEnv *env = JNI::getEnv(); - close(env); - - env->DeleteGlobalRef(_buf); - env->DeleteGlobalRef(_input_stream); +AssetInputStream::~AssetInputStream() { } -void JavaInputStream::close(JNIEnv *env) { - env->CallVoidMethod(_input_stream, MID_close); - - if (env->ExceptionCheck()) - env->ExceptionClear(); +void AssetInputStream::close() { + AAsset_close(_asset); } -uint32 JavaInputStream::read(void *dataPtr, uint32 dataSize) { - JNIEnv *env = JNI::getEnv(); - - if (_buflen < jint(dataSize)) { - _buflen = dataSize; - - env->DeleteGlobalRef(_buf); - jobject buf = env->NewByteArray(_buflen); - _buf = static_cast<jbyteArray>(env->NewGlobalRef(buf)); - env->DeleteLocalRef(buf); - } - - jint ret = env->CallIntMethod(_input_stream, MID_read, _buf, 0, dataSize); - - if (env->ExceptionCheck()) { - warning("Exception during JavaInputStream::read(%p, %d)", - dataPtr, dataSize); - - env->ExceptionDescribe(); - env->ExceptionClear(); - - _err = true; - ret = -1; - } else if (ret == -1) { +uint32 AssetInputStream::read(void *dataPtr, uint32 dataSize) { + uint32 readlen = AAsset_read(_asset, dataPtr, dataSize); + _pos += readlen; + if (readlen != dataSize) { _eos = true; - ret = 0; - } else { - env->GetByteArrayRegion(_buf, 0, ret, static_cast<jbyte *>(dataPtr)); - _pos += ret; } - - return ret; + return readlen; } -bool JavaInputStream::seek(int32 offset, int whence) { - JNIEnv *env = JNI::getEnv(); - uint32 newpos; - - switch (whence) { - case SEEK_SET: - newpos = offset; - break; - case SEEK_CUR: - newpos = _pos + offset; - break; - case SEEK_END: - newpos = _len + offset; - break; - default: - debug("Unknown 'whence' arg %d", whence); +bool AssetInputStream::seek(int32 offset, int whence) { + int res = AAsset_seek(_asset, offset, whence); + if (res == -1) { return false; } - - jlong skip_bytes; - if (newpos > _pos) { - skip_bytes = newpos - _pos; - } else { - // Can't skip backwards, so jump back to start and skip from there. - env->CallVoidMethod(_input_stream, MID_reset); - - if (env->ExceptionCheck()) { - warning("Failed to rewind to start of asset stream"); - - env->ExceptionDescribe(); - env->ExceptionClear(); - - return false; - } - - _pos = 0; - skip_bytes = newpos; - } - - while (skip_bytes > 0) { - jlong ret = env->CallLongMethod(_input_stream, MID_skip, skip_bytes); - - if (env->ExceptionCheck()) { - warning("Failed to skip %ld bytes into asset stream", - static_cast<long>(skip_bytes)); - - env->ExceptionDescribe(); - env->ExceptionClear(); - - return false; - } else if (ret == 0) { - warning("InputStream->skip(%ld) didn't skip any bytes. Aborting seek.", - static_cast<long>(skip_bytes)); - - // No point looping forever... - return false; - } - - _pos += ret; - skip_bytes -= ret; - } - - _eos = false; - return true; -} - -// Must match android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH -const jlong UNKNOWN_LENGTH = -1; - -// Reading directly from a fd is so much more efficient, that it is -// worth optimising for. -class AssetFdReadStream : public Common::SeekableReadStream { -public: - AssetFdReadStream(JNIEnv *env, jobject assetfd); - virtual ~AssetFdReadStream(); - - virtual bool eos() const { - return _eos; - } - - virtual bool err() const { - return _err; - } - - virtual void clearErr() { - _eos = _err = false; - } - - virtual uint32 read(void *dataPtr, uint32 dataSize); - - virtual int32 pos() const { - return _pos; - } - - virtual int32 size() const { - return _declared_len; - } - - virtual bool seek(int32 offset, int whence = SEEK_SET); - -private: - void close(JNIEnv *env); - - int _fd; - jmethodID MID_close; - jobject _assetfd; - jlong _start_off; - jlong _declared_len; - uint32 _pos; - bool _eos; - bool _err; -}; - -AssetFdReadStream::AssetFdReadStream(JNIEnv *env, jobject assetfd) : - _eos(false), - _err(false), - _pos(0) -{ - _assetfd = env->NewGlobalRef(assetfd); - - jclass cls = env->GetObjectClass(_assetfd); - MID_close = env->GetMethodID(cls, "close", "()V"); - assert(MID_close); - - jmethodID MID_getStartOffset = - env->GetMethodID(cls, "getStartOffset", "()J"); - assert(MID_getStartOffset); - _start_off = env->CallLongMethod(_assetfd, MID_getStartOffset); - - jmethodID MID_getDeclaredLength = - env->GetMethodID(cls, "getDeclaredLength", "()J"); - assert(MID_getDeclaredLength); - _declared_len = env->CallLongMethod(_assetfd, MID_getDeclaredLength); - - jmethodID MID_getFileDescriptor = - env->GetMethodID(cls, "getFileDescriptor", - "()Ljava/io/FileDescriptor;"); - assert(MID_getFileDescriptor); - jobject javafd = env->CallObjectMethod(_assetfd, MID_getFileDescriptor); - assert(javafd); - - jclass fd_cls = env->GetObjectClass(javafd); - jfieldID FID_descriptor = env->GetFieldID(fd_cls, "descriptor", "I"); - assert(FID_descriptor); - env->DeleteLocalRef(fd_cls); - - _fd = env->GetIntField(javafd, FID_descriptor); - env->DeleteLocalRef(javafd); - - env->DeleteLocalRef(cls); -} - -AssetFdReadStream::~AssetFdReadStream() { - JNIEnv *env = JNI::getEnv(); - env->CallVoidMethod(_assetfd, MID_close); - - if (env->ExceptionCheck()) - env->ExceptionClear(); - - env->DeleteGlobalRef(_assetfd); -} - -uint32 AssetFdReadStream::read(void *dataPtr, uint32 dataSize) { - if (_declared_len != UNKNOWN_LENGTH) { - jlong cap = _declared_len - _pos; - if (dataSize > cap) - dataSize = cap; - } - - int ret = ::read(_fd, dataPtr, dataSize); - - if (ret == 0) - _eos = true; - else if (ret == -1) - _err = true; - else - _pos += ret; - - return ret; -} - -bool AssetFdReadStream::seek(int32 offset, int whence) { - if (whence == SEEK_SET) { - if (_declared_len != UNKNOWN_LENGTH && offset > _declared_len) - offset = _declared_len; - - offset += _start_off; - } else if (whence == SEEK_END && _declared_len != UNKNOWN_LENGTH) { - whence = SEEK_SET; - offset = _start_off + _declared_len + offset; + if (whence == SEEK_CUR) { + _pos += offset; + } else if (whence == SEEK_SET) { + _pos = offset; + } else if (whence == SEEK_END) { + _pos = _len + offset; } - - int ret = lseek(_fd, offset, whence); - - if (ret == -1) - return false; - - _pos = ret - _start_off; + assert(_pos <= _len); _eos = false; - return true; } -AndroidAssetArchive::AndroidAssetArchive(jobject am) { +AndroidAssetArchive::AndroidAssetArchive(jobject am) : _hasCached(false) { JNIEnv *env = JNI::getEnv(); - _am = env->NewGlobalRef(am); - jclass cls = env->GetObjectClass(_am); - MID_open = env->GetMethodID(cls, "open", - "(Ljava/lang/String;I)Ljava/io/InputStream;"); - assert(MID_open); - - MID_openFd = env->GetMethodID(cls, "openFd", "(Ljava/lang/String;)" - "Landroid/content/res/AssetFileDescriptor;"); - assert(MID_openFd); - - MID_list = env->GetMethodID(cls, "list", - "(Ljava/lang/String;)[Ljava/lang/String;"); - assert(MID_list); - env->DeleteLocalRef(cls); + _am = AAssetManager_fromJava(env, am); } AndroidAssetArchive::~AndroidAssetArchive() { - JNIEnv *env = JNI::getEnv(); - env->DeleteGlobalRef(_am); } bool AndroidAssetArchive::hasFile(const Common::String &name) const { - JNIEnv *env = JNI::getEnv(); - jstring path = env->NewStringUTF(name.c_str()); - jobject result = env->CallObjectMethod(_am, MID_open, path, ACCESS_UNKNOWN); - if (env->ExceptionCheck()) { - // Assume FileNotFoundException - //warning("Error while calling AssetManager->open(%s)", name.c_str()); - //env->ExceptionDescribe(); - env->ExceptionClear(); - env->DeleteLocalRef(path); - - return false; + AAsset *asset = AAssetManager_open(_am, name.c_str(), AASSET_MODE_RANDOM); + bool exists = false; + if (asset != NULL) { + exists = true; + AAsset_close(asset); } - - env->DeleteLocalRef(result); - env->DeleteLocalRef(path); - - return true; + return exists; } int AndroidAssetArchive::listMembers(Common::ArchiveMemberList &member_list) const { - JNIEnv *env = JNI::getEnv(); - Common::List<Common::String> dirlist; - dirlist.push_back(""); + if (_hasCached) { + member_list.insert(member_list.end(), _cachedMembers.begin(), _cachedMembers.end()); + return _cachedMembers.size(); + } int count = 0; - while (!dirlist.empty()) { - const Common::String dir = dirlist.back(); - dirlist.pop_back(); - - jstring jpath = env->NewStringUTF(dir.c_str()); - jobjectArray jpathlist = - (jobjectArray)env->CallObjectMethod(_am, MID_list, jpath); - - if (env->ExceptionCheck()) { - warning("Error while calling AssetManager->list(%s). Ignoring.", - dir.c_str()); - env->ExceptionDescribe(); - env->ExceptionClear(); - - // May as well keep going ... - continue; - } - - env->DeleteLocalRef(jpath); + AAssetDir *dir = AAssetManager_openDir(_am, ""); + const char *file = AAssetDir_getNextFileName(dir); - for (jsize i = 0; i < env->GetArrayLength(jpathlist); ++i) { - jstring elem = (jstring)env->GetObjectArrayElement(jpathlist, i); - const char *p = env->GetStringUTFChars(elem, 0); - - if (strlen(p)) { - Common::String thispath = dir; - - if (!thispath.empty()) - thispath += "/"; - - thispath += p; - - // Assume files have a . in them, and directories don't - if (strchr(p, '.')) { - member_list.push_back(getMember(thispath)); - ++count; - } else { - // AssetManager is ridiculously slow and we don't care - // about subdirectories at the moment, so ignore them. - // dirlist.push_back(thispath); - } - } - - env->ReleaseStringUTFChars(elem, p); - env->DeleteLocalRef(elem); - } - - env->DeleteLocalRef(jpathlist); + while (file) { + member_list.push_back(getMember(file)); + ++count; + file = AAssetDir_getNextFileName(dir); } + AAssetDir_close(dir); + + _cachedMembers = Common::ArchiveMemberList(member_list); + _hasCached = true; return count; } @@ -483,39 +150,10 @@ const Common::ArchiveMemberPtr AndroidAssetArchive::getMember(const Common::Stri } Common::SeekableReadStream *AndroidAssetArchive::createReadStreamForMember(const Common::String &path) const { - JNIEnv *env = JNI::getEnv(); - jstring jpath = env->NewStringUTF(path.c_str()); - - // Try openFd() first ... - jobject afd = env->CallObjectMethod(_am, MID_openFd, jpath); - - if (env->ExceptionCheck()) - env->ExceptionClear(); - else if (afd != 0) { - // success :) - Common::SeekableReadStream *stream = new AssetFdReadStream(env, afd); - env->DeleteLocalRef(jpath); - env->DeleteLocalRef(afd); - return stream; + if (!hasFile(path)) { + return nullptr; } - - // ... and fallback to normal open() if that doesn't work - jobject is = env->CallObjectMethod(_am, MID_open, jpath, ACCESS_RANDOM); - - if (env->ExceptionCheck()) { - // Assume FileNotFoundException - //warning("Error opening %s", path.c_str()); - //env->ExceptionDescribe(); - env->ExceptionClear(); - env->DeleteLocalRef(jpath); - - return 0; - } - - Common::SeekableReadStream *stream = new JavaInputStream(env, is); - env->DeleteLocalRef(jpath); - env->DeleteLocalRef(is); - return stream; + return new AssetInputStream(_am, path); } #endif diff --git a/backends/platform/android/asset-archive.h b/backends/platform/android/asset-archive.h index 6a0033d24e..8ae55b22c9 100644 --- a/backends/platform/android/asset-archive.h +++ b/backends/platform/android/asset-archive.h @@ -32,6 +32,8 @@ #include "common/util.h" #include "common/archive.h" +#include <android/asset_manager.h> + class AndroidAssetArchive : public Common::Archive { public: AndroidAssetArchive(jobject am); @@ -43,11 +45,9 @@ public: virtual Common::SeekableReadStream *createReadStreamForMember(const Common::String &name) const; private: - jmethodID MID_open; - jmethodID MID_openFd; - jmethodID MID_list; - - jobject _am; + AAssetManager *_am; + mutable Common::ArchiveMemberList _cachedMembers; + mutable bool _hasCached; }; #endif diff --git a/backends/platform/android/events.cpp b/backends/platform/android/events.cpp index 8039981a92..b146945a01 100644 --- a/backends/platform/android/events.cpp +++ b/backends/platform/android/events.cpp @@ -101,7 +101,9 @@ enum { JKEYCODE_MEDIA_NEXT = 87, JKEYCODE_MEDIA_PREVIOUS = 88, JKEYCODE_MEDIA_REWIND = 89, - JKEYCODE_MEDIA_FAST_FORWARD = 90 + JKEYCODE_MEDIA_FAST_FORWARD = 90, + JKEYCODE_MEDIA_PLAY = 126, + JKEYCODE_MEDIA_PAUSE = 127 }; // five-way navigation control @@ -380,6 +382,19 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3, return; + case JKEYCODE_MEDIA_PAUSE: + case JKEYCODE_MEDIA_PLAY: + case JKEYCODE_MEDIA_PLAY_PAUSE: + if (arg1 == JACTION_DOWN) { + e.type = Common::EVENT_MAINMENU; + + lockMutex(_event_queue_lock); + _event_queue.push(e); + unlockMutex(_event_queue_lock); + } + + return; + case JKEYCODE_CAMERA: case JKEYCODE_SEARCH: if (arg1 == JACTION_DOWN) @@ -888,6 +903,10 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3, e.kbd.ascii = Common::ASCII_ESCAPE; break; + case JKEYCODE_BUTTON_Y: + e.type = Common::EVENT_MAINMENU; + break; + default: LOGW("unmapped gamepad key: %d", arg2); return; diff --git a/backends/platform/android/jni.cpp b/backends/platform/android/jni.cpp index 22e6a749c2..256ae09ef8 100644 --- a/backends/platform/android/jni.cpp +++ b/backends/platform/android/jni.cpp @@ -76,6 +76,8 @@ bool JNI::_ready_for_events = 0; jmethodID JNI::_MID_getDPI = 0; jmethodID JNI::_MID_displayMessageOnOSD = 0; +jmethodID JNI::_MID_openUrl = 0; +jmethodID JNI::_MID_isConnectionLimited = 0; jmethodID JNI::_MID_setWindowCaption = 0; jmethodID JNI::_MID_showVirtualKeyboard = 0; jmethodID JNI::_MID_getSysArchives = 0; @@ -232,6 +234,41 @@ void JNI::displayMessageOnOSD(const char *msg) { env->DeleteLocalRef(java_msg); } +bool JNI::openUrl(const char *url) { + bool success = true; + JNIEnv *env = JNI::getEnv(); + jstring javaUrl = env->NewStringUTF(url); + + env->CallVoidMethod(_jobj, _MID_openUrl, javaUrl); + + if (env->ExceptionCheck()) { + LOGE("Failed to open URL"); + + env->ExceptionDescribe(); + env->ExceptionClear(); + success = false; + } + + env->DeleteLocalRef(javaUrl); + return success; +} + +bool JNI::isConnectionLimited() { + bool limited = false; + JNIEnv *env = JNI::getEnv(); + limited = env->CallBooleanMethod(_jobj, _MID_isConnectionLimited); + + if (env->ExceptionCheck()) { + LOGE("Failed to check whether connection's limited"); + + env->ExceptionDescribe(); + env->ExceptionClear(); + limited = true; + } + + return limited; +} + void JNI::setWindowCaption(const char *caption) { JNIEnv *env = JNI::getEnv(); jstring java_caption = env->NewStringUTF(caption); @@ -411,6 +448,8 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager, FIND_METHOD(, setWindowCaption, "(Ljava/lang/String;)V"); FIND_METHOD(, getDPI, "([F)V"); FIND_METHOD(, displayMessageOnOSD, "(Ljava/lang/String;)V"); + FIND_METHOD(, openUrl, "(Ljava/lang/String;)V"); + FIND_METHOD(, isConnectionLimited, "()Z"); FIND_METHOD(, showVirtualKeyboard, "(Z)V"); FIND_METHOD(, getSysArchives, "()[Ljava/lang/String;"); FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;"); diff --git a/backends/platform/android/jni.h b/backends/platform/android/jni.h index 70feaaf72a..0798db448a 100644 --- a/backends/platform/android/jni.h +++ b/backends/platform/android/jni.h @@ -58,6 +58,8 @@ public: static void setWindowCaption(const char *caption); static void getDPI(float *values); static void displayMessageOnOSD(const char *msg); + static bool openUrl(const char *url); + static bool isConnectionLimited(); static void showVirtualKeyboard(bool enable); static void addSysArchivesToSearchSet(Common::SearchSet &s, int priority); @@ -89,6 +91,8 @@ private: static jmethodID _MID_getDPI; static jmethodID _MID_displayMessageOnOSD; + static jmethodID _MID_openUrl; + static jmethodID _MID_isConnectionLimited; static jmethodID _MID_setWindowCaption; static jmethodID _MID_showVirtualKeyboard; static jmethodID _MID_getSysArchives; diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java index 3b370a583d..47dcb32b22 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java @@ -53,6 +53,8 @@ public abstract class ScummVM implements SurfaceHolder.Callback, Runnable { // Callbacks from C++ peer instance abstract protected void getDPI(float[] values); abstract protected void displayMessageOnOSD(String msg); + abstract protected void openUrl(String url); + abstract protected boolean isConnectionLimited(); abstract protected void setWindowCaption(String caption); abstract protected void showVirtualKeyboard(boolean enable); abstract protected String[] getSysArchives(); diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java index 5b2dcae175..225496ca0d 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java @@ -2,9 +2,13 @@ package org.scummvm.scummvm; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.media.AudioManager; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiInfo; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -75,6 +79,21 @@ public class ScummVMActivity extends Activity { } @Override + protected void openUrl(String url) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + } + + @Override + protected boolean isConnectionLimited() { + WifiManager wifiMgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); + if (wifiMgr != null && wifiMgr.isWifiEnabled()) { + WifiInfo wifiInfo = wifiMgr.getConnectionInfo(); + return (wifiInfo == null || wifiInfo.getNetworkId() == -1); //WiFi is on, but it's not connected to any network + } + return true; + } + + @Override protected void setWindowCaption(final String caption) { runOnUiThread(new Runnable() { public void run() { diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java b/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java index 32c65d3395..e81000d8b1 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java @@ -119,6 +119,8 @@ public class ScummVMEvents implements case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_CAMERA: case KeyEvent.KEYCODE_SEARCH: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: break; default: diff --git a/backends/platform/androidsdl/androidsdl-main.cpp b/backends/platform/androidsdl/androidsdl-main.cpp new file mode 100644 index 0000000000..364d686c1b --- /dev/null +++ b/backends/platform/androidsdl/androidsdl-main.cpp @@ -0,0 +1,47 @@ +/* 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_system + +#include "backends/platform/androidsdl/androidsdl-sdl.h" +#include "base/main.h" + +int main(int argc, char *argv[]) { + + // Copy over config file from previous version of ScummVM if applicable + system("ls $DATADIR/.config/scummvm/scummvm.ini || (mkdir -p $DATADIR/.config/scummvm && cp $APPDIR/scummvmrc $DATADIR/.config/scummvm/scummvm.ini )"); + + // Create our OSystem instance + g_system = new OSystem_ANDROIDSDL(); + assert(g_system); + + // Pre initialize the backend + ((OSystem_POSIX *)g_system)->init(); + + // Invoke the actual ScummVM main entry point: + int res = scummvm_main(argc, argv); + + // Free OSystem + delete (OSystem_ANDROIDSDL *)g_system; + + return res; +} diff --git a/backends/platform/androidsdl/androidsdl-sdl.cpp b/backends/platform/androidsdl/androidsdl-sdl.cpp new file mode 100644 index 0000000000..d04512475a --- /dev/null +++ b/backends/platform/androidsdl/androidsdl-sdl.cpp @@ -0,0 +1,48 @@ +/* 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_getenv(a) + +#include "common/config-manager.h" + +#include "backends/platform/androidsdl/androidsdl-sdl.h" +#include "backends/events/androidsdl/androidsdl-events.h" +#include "backends/graphics/androidsdl/androidsdl-graphics.h" + +void OSystem_ANDROIDSDL::initBackend() { + // Create the backend custom managers + + if (_eventSource == 0) + _eventSource = new AndroidSdlEventSource(); + + if (_graphicsManager == 0) + _graphicsManager = new AndroidSdlGraphicsManager(_eventSource, _window); + + if (!ConfMan.hasKey("browser_lastpath")) + ConfMan.set("browser_lastpath", getenv("SDCARD")); + + if (!ConfMan.hasKey("gfx_mode")) + ConfMan.set("gfx_mode", "2x"); + + // Call parent implementation of this method + OSystem_POSIX::initBackend(); +} diff --git a/backends/platform/androidsdl/androidsdl-sdl.h b/backends/platform/androidsdl/androidsdl-sdl.h new file mode 100644 index 0000000000..6ebe5022eb --- /dev/null +++ b/backends/platform/androidsdl/androidsdl-sdl.h @@ -0,0 +1,38 @@ +/* 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_ANDROIDSDL_H +#define PLATFORM_SDL_ANDROIDSDL_H + +#include "backends/platform/sdl/posix/posix.h" + +class OSystem_ANDROIDSDL : public OSystem_POSIX { +public: + virtual void initBackend(); + +#ifdef ENABLE_KEYMAPPER + // FIXME: This just calls parent methods, is it needed? + virtual Common::HardwareInputSet *getHardwareInputSet(); +#endif +}; + +#endif diff --git a/backends/platform/androidsdl/androidsdl.mk b/backends/platform/androidsdl/androidsdl.mk new file mode 100644 index 0000000000..1defb81b97 --- /dev/null +++ b/backends/platform/androidsdl/androidsdl.mk @@ -0,0 +1,11 @@ +# Special target to create an AndroidSDL snapshot +androidsdl: + $(MKDIR) release + $(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) release + $(INSTALL) -c -m 644 $(DIST_FILES_DOCS) release + $(CP) $(srcdir)/backends/vkeybd/packs/vkeybd_default.zip release + zip -j scummvm190-git-appdata.zip release/* + split -d -b 1000000 scummvm190-git-appdata.zip scummvm190-git-appdata.zip0 + $(RM) -r scummvm190-git-appdata.zip + +.PHONY: androidsdl diff --git a/backends/platform/androidsdl/module.mk b/backends/platform/androidsdl/module.mk new file mode 100644 index 0000000000..df927163b8 --- /dev/null +++ b/backends/platform/androidsdl/module.mk @@ -0,0 +1,13 @@ +MODULE := backends/platform/androidsdl + +MODULE_OBJS := \ + androidsdl-main.o \ + androidsdl-sdl.o + +# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS. +MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS)) +OBJS := $(MODULE_OBJS) $(OBJS) +MODULE_DIRS += $(sort $(dir $(MODULE_OBJS))) + +# Hack to ensure the SDL backend is built so we can use OSystem_SDL. +-include $(srcdir)/backends/platform/sdl/module.mk diff --git a/backends/platform/dc/README b/backends/platform/dc/README index e26df500e8..3524f57bac 100644 --- a/backends/platform/dc/README +++ b/backends/platform/dc/README @@ -4,13 +4,13 @@ Compiling ScummVM for SEGA Dreamcast If you want to compile ScummVM for your Dreamcast, you'll need the following: -* gcc-3.4.2 configured as a cross-compiler for `sh-elf' +* gcc-4.6.3 configured as a cross-compiler for `sh-elf' * binutils-2.18 configured likewise -* newlib for sh-elf : <URL:http://mc.pp.se/dc/files/newlib-1.12.0.tar.gz> +* newlib for sh-elf : <URL:http://mc.pp.se/dc/files/newlib-1.19.0.tar.gz> -* libronin-0.5 : <URL:http://peter.bortas.org/scumm/libronin-0.5.tar.gz> +* libronin-0.6 : <URL:http://peter.bortas.org/scumm/libronin-0.6.tar.gz> * libmad : <URL:http://mc.pp.se/dc/files/libmad-0.15.1b.tar.gz> diff --git a/backends/platform/dc/dc-fs.cpp b/backends/platform/dc/dc-fs.cpp index 77fe4143dd..4bd7e5a777 100644 --- a/backends/platform/dc/dc-fs.cpp +++ b/backends/platform/dc/dc-fs.cpp @@ -57,6 +57,7 @@ public: virtual Common::SeekableReadStream *createReadStream(); virtual Common::WriteStream *createWriteStream() { return 0; } + virtual bool create(bool isDirectory) { return false; } static AbstractFSNode *makeFileNodePath(const Common::String &path); }; diff --git a/backends/platform/dc/dc.h b/backends/platform/dc/dc.h index d8ab549c3a..6cd938ec9c 100644 --- a/backends/platform/dc/dc.h +++ b/backends/platform/dc/dc.h @@ -57,21 +57,16 @@ class DCHardware { }; class DCCDManager : public DefaultAudioCDManager { - // Initialize the specified CD drive for audio playback. - bool openCD(int drive); - - // Poll cdrom status - // Returns true if cd audio is playing - bool pollCD(); - - // Play cdrom audio track - void playCD(int track, int num_loops, int start_frame, int duration); +public: + // Poll cdrom status + // Returns true if cd audio is playing + bool isPlaying() const; - // Stop cdrom audio track - void stopCD(); + // Play cdrom audio track + bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false); - // Update cdrom audio status - void updateCD(); + // Stop cdrom audio track + void stop(); }; class OSystem_Dreamcast : private DCHardware, public EventsBaseBackend, public PaletteManager, public FilesystemFactory diff --git a/backends/platform/dc/dcloader.cpp b/backends/platform/dc/dcloader.cpp index bc5e3a733b..2627773b13 100644 --- a/backends/platform/dc/dcloader.cpp +++ b/backends/platform/dc/dcloader.cpp @@ -26,6 +26,8 @@ #include "dcloader.h" +#include <cxxabi.h> + #ifdef DL_DEBUG #define DBG(x) reportf x #else @@ -345,6 +347,7 @@ bool DLObject::open(const char *path) ctors_end = symbol("__plugin_ctors_end"); dtors_start = symbol("__plugin_dtors"); dtors_end = symbol("__plugin_dtors_end"); + dso_handle = symbol("__dso_handle"); if (ctors_start == NULL || ctors_end == NULL || dtors_start == NULL || dtors_end == NULL) { @@ -364,6 +367,10 @@ bool DLObject::open(const char *path) bool DLObject::close() { + if (dso_handle != NULL) { + __cxxabiv1::__cxa_finalize(dso_handle); + dso_handle = NULL; + } if (dtors_start != NULL && dtors_end != NULL) for (void (**f)(void) = (void (**)(void))dtors_start; f != dtors_end; f++) (**f)(); diff --git a/backends/platform/dc/dcloader.h b/backends/platform/dc/dcloader.h index 074ae7aa29..daa2802ec4 100644 --- a/backends/platform/dc/dcloader.h +++ b/backends/platform/dc/dcloader.h @@ -34,7 +34,7 @@ class DLObject { void *segment, *symtab; char *strtab; int symbol_cnt; - void *dtors_start, *dtors_end; + void *dtors_start, *dtors_end, *dso_handle; void seterror(const char *fmt, ...); void unload(); diff --git a/backends/platform/dc/dcmain.cpp b/backends/platform/dc/dcmain.cpp index eede796991..c84aef9c47 100644 --- a/backends/platform/dc/dcmain.cpp +++ b/backends/platform/dc/dcmain.cpp @@ -90,43 +90,53 @@ static bool find_track(int track, int &first_sec, int &last_sec) return false; } -void DCCDManager::playCD(int track, int num_loops, int start_frame, int duration) -{ - int first_sec, last_sec; +bool DCCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) { + DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate); + + // If we're playing now return here + if (isPlaying()) { + return true; + } + + // If we should only play emulated tracks stop here. + if (onlyEmulate) { + return false; + } + + int firstSec, lastSec; #if 1 - if (num_loops) - --num_loops; + if (numLoops) + --numLoops; #endif - if (num_loops>14) num_loops=14; - else if (num_loops<0) num_loops=15; // infinity - if (!find_track(track, first_sec, last_sec)) - return; - if (duration) - last_sec = first_sec + start_frame + duration; - first_sec += start_frame; - play_cdda_sectors(first_sec, last_sec, num_loops); -} -void DCCDManager::stopCD() -{ - stop_cdda(); -} + if (numLoops > 14) + numLoops = 14; + else if (numLoops < 0) + numLoops = 15; // infinity -bool DCCDManager::pollCD() -{ - extern int getCdState(); - return getCdState() == 3; + if (!find_track(track, firstSec, lastSec)) + return false; + + if (duration) + lastSec = firstSec + startFrame + duration; + + firstSec += startFrame; + play_cdda_sectors(firstSec, lastSec, numLoops); + + return true; } -void DCCDManager::updateCD() -{ - // Dummy. The CD drive takes care of itself. +void DCCDManager::stop() { + DefaultAudioCDManager::stop(); + stop_cdda(); } -bool DCCDManager::openCD(int drive) -{ - // Dummy. - return true; +bool DCCDManager::isPlaying() const { + if (DefaultAudioCDManager::isPlaying()) + return true; + + extern int getCdState(); + return getCdState() == 3; } void OSystem_Dreamcast::setWindowCaption(const char *caption) diff --git a/backends/platform/dc/dreamcast.mk b/backends/platform/dc/dreamcast.mk index 9d8a53e25a..98b5bae087 100644 --- a/backends/platform/dc/dreamcast.mk +++ b/backends/platform/dc/dreamcast.mk @@ -6,6 +6,8 @@ ASFLAGS := $(CXXFLAGS) dist : SCUMMVM.BIN IP.BIN plugin_dist +clean : dcclean + plugin_dist : plugins @[ -z "$(PLUGINS)" ] || for p in $(or $(PLUGINS),none); do \ t="`basename \"$$p\" | LC_CTYPE=C tr '[:lower:]' '[:upper:]'`"; \ @@ -37,3 +39,10 @@ ip.txt : $(srcdir)/backends/platform/dc/ip.txt.in dcdist : dist mkdir -p dcdist/scummvm cp scummvm.elf SCUMMVM.BIN IP.BIN *.PLG dcdist/scummvm/ + +dcclean : + $(RM) backends/platform/dc/plugin_head.o + $(RM) scummvm.bin SCUMMVM.BIN ip.txt IP.BIN *.PLG + $(RM_REC) dcdist + +.PHONY: dcclean diff --git a/backends/platform/dc/plugin.syms b/backends/platform/dc/plugin.syms index 2cab08c9c7..9340ca20b8 100644 --- a/backends/platform/dc/plugin.syms +++ b/backends/platform/dc/plugin.syms @@ -6,3 +6,4 @@ ___plugin_ctors ___plugin_ctors_end ___plugin_dtors ___plugin_dtors_end +___dso_handle diff --git a/backends/platform/dc/plugin.x b/backends/platform/dc/plugin.x index 2c8469a335..039fca81cf 100644 --- a/backends/platform/dc/plugin.x +++ b/backends/platform/dc/plugin.x @@ -9,6 +9,7 @@ SECTIONS . = 0; .text : { + ___dso_handle = .; *(.text .stub .text.* .gnu.linkonce.t.*) *(.gnu.warning) } :plugin =0 diff --git a/backends/platform/dc/vmsave.cpp b/backends/platform/dc/vmsave.cpp index 75fc1ed0df..df02813b67 100644 --- a/backends/platform/dc/vmsave.cpp +++ b/backends/platform/dc/vmsave.cpp @@ -266,18 +266,19 @@ public: { return ::readSaveGame(buffer, _size, filename); } }; -class OutVMSave : public Common::OutSaveFile { +class OutVMSave : public Common::WriteStream { private: char *buffer; - int pos, size, committed; + int _pos, size, committed; char filename[16]; bool iofailed; public: uint32 write(const void *buf, uint32 cnt); + virtual int32 pos() const { return _pos; } OutVMSave(const char *_filename) - : pos(0), committed(-1), iofailed(false) + : _pos(0), committed(-1), iofailed(false) { strncpy(filename, _filename, 16); buffer = new char[size = MAX_SAVE_SIZE]; @@ -291,12 +292,34 @@ public: }; class VMSaveManager : public Common::SaveFileManager { +private: + static int nameCompare(const unsigned char *entry, const char *match) { + return !scumm_strnicmp(reinterpret_cast<const char *>(entry), match, 12); + } + public: + virtual void updateSavefilesList(Common::StringArray &lockedFiles) { + // TODO: implement this (locks files, preventing them from being listed, saved or loaded) + } - virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { - OutVMSave *s = new OutVMSave(filename.c_str()); - return compress ? Common::wrapCompressedWriteStream(s) : s; - } + VMSaveManager() { + vmsfs_name_compare_function = nameCompare; + } + + virtual Common::InSaveFile *openRawFile(const Common::String &filename) { + InVMSave *s = new InVMSave(); + if (s->readSaveGame(filename.c_str())) { + return s; + } else { + delete s; + return NULL; + } + } + + virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { + OutVMSave *s = new OutVMSave(filename.c_str()); + return new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(s) : s); + } virtual Common::InSaveFile *openForLoading(const Common::String &filename) { InVMSave *s = new InVMSave(); @@ -320,14 +343,14 @@ void OutVMSave::finalize() extern const char *gGameName; extern Icon icon; - if (committed >= pos) + if (committed >= _pos) return; char *data = buffer; - int len = pos; + int len = _pos; vmsaveResult r = writeSaveGame(gGameName, data, len, filename, icon); - committed = pos; + committed = _pos; if (r != VMSAVE_OK) iofailed = true; displaySaveResult(r); @@ -386,13 +409,13 @@ bool InVMSave::seek(int32 offs, int whence) uint32 OutVMSave::write(const void *buf, uint32 cnt) { int nbyt = cnt; - if (pos + nbyt > size) { - cnt = (size - pos); + if (_pos + nbyt > size) { + cnt = (size - _pos); nbyt = cnt; } if (nbyt) - memcpy(buffer + pos, buf, nbyt); - pos += nbyt; + memcpy(buffer + _pos, buf, nbyt); + _pos += nbyt; return cnt; } diff --git a/backends/platform/dingux/README.GCW0 b/backends/platform/dingux/README.GCW0 index 1875e5323a..95438084c2 100644 --- a/backends/platform/dingux/README.GCW0 +++ b/backends/platform/dingux/README.GCW0 @@ -24,3 +24,17 @@ It's pretty simple if you are running Linux on an x86/amd64 machine: 3. Run backends/platform/dingux/build.gcw0.sh script 4. Copy the resulting file scummvm.opk to your device 5. Enjoy + +Troubleshooting +=============== +Please be advised that currently games with a resolution above 320x240 +are disabled due to the limited screen resolution on the GCW0. This could be +changed in the future, but higher resolution games will require use of +an HDMI connection. + +In case you need to submit a bugreport, you may find the log file at the +following path: + + /var/tmp/scummvm.log + +The log file is being overwritten at every ScummVM run. diff --git a/backends/platform/dingux/build.gcw0.sh b/backends/platform/dingux/build.gcw0.sh index c4bb9d2641..7a31d4fd27 100755 --- a/backends/platform/dingux/build.gcw0.sh +++ b/backends/platform/dingux/build.gcw0.sh @@ -3,4 +3,4 @@ export PATH=/opt/gcw0-toolchain/usr/bin:$PATH # Disable high resolution engines since we have 320x240 hardware -./configure --host=gcw0 --enable-plugins --default-dynamic --enable-release --disable-engine=mohawk,neverhood,sword25,toltecs,wintermute,zvision --disable-mt32emu --disable-hq-scalers && make -j6 gcw-opk && ls -l scummvm.opk +./configure --host=gcw0 --enable-plugins --default-dynamic --enable-release && make -j6 gcw-opk && ls -l scummvm.opk diff --git a/backends/platform/dingux/dingux.mk b/backends/platform/dingux/dingux.mk index 56c26c3be1..dc87e41241 100644 --- a/backends/platform/dingux/dingux.mk +++ b/backends/platform/dingux/dingux.mk @@ -59,19 +59,20 @@ endif echo >> $(gcw0_bundle)/README.man.txt echo '[General README]' >> $(gcw0_bundle)/README.man.txt echo >> $(gcw0_bundle)/README.man.txt - cat README >> $(gcw0_bundle)/README.man.txt + cat $(srcdir)/README | sed -e 's/\[/⟦/g' -e 's/\]/⟧/g' -e '/^1\.1)/,$$ s/^[0-9][0-9]*\.[0-9][0-9]*.*/\[&\]/' >> $(gcw0_bundle)/README.man.txt + # $(CP) GeneralUser\ GS\ FluidSynth\ v1.44.sf2 $(gcw0_bundle)/ gcw0-opk-unstripped: $(gcw0_bundle) $(CP) $(PLUGINS) $(gcw0_bundle)/plugins/ $(CP) $(EXECUTABLE) $(gcw0_bundle)/scummvm - ./dists/gcw0/opk_make.sh -d $(gcw0_bundle) -o scummvm + $(srcdir)/dists/gcw0/opk_make.sh -d $(gcw0_bundle) -o scummvm gcw-opk: $(gcw0_bundle) $(STRIP) $(gcw0_bundle)/plugins/* $(STRIP) $(gcw0_bundle)/scummvm - ./dists/gcw0/opk_make.sh -d $(gcw0_bundle) -o scummvm + $(srcdir)/dists/gcw0/opk_make.sh -d $(gcw0_bundle) -o scummvm GeneralUser_GS_1.44-FluidSynth.zip: curl -s http://www.scummvm.org/frs/extras/SoundFont/GeneralUser_GS_1.44-FluidSynth.zip -o GeneralUser_GS_1.44-FluidSynth.zip diff --git a/backends/platform/ds/arm9/source/gbampsave.cpp b/backends/platform/ds/arm9/source/gbampsave.cpp index ef6091e2a2..9991a3253a 100644 --- a/backends/platform/ds/arm9/source/gbampsave.cpp +++ b/backends/platform/ds/arm9/source/gbampsave.cpp @@ -45,6 +45,17 @@ static Common::String getSavePath() { // GBAMP Save File Manager ////////////////////////// +void GBAMPSaveFileManager::updateSavefilesList(Common::StringArray &lockedFiles) { + // TODO: implement this + // in this method manager should remember lockedFiles + // these files must not be opened for loading or saving, or listed by listSavefiles() +} + +Common::InSaveFile *GBAMPSaveFileManager::openRawFile(const Common::String &filename) { + // TODO: make sure it returns raw file, not uncompressed save contents + return openForLoading(filename); +} + Common::OutSaveFile *GBAMPSaveFileManager::openForSaving(const Common::String &filename, bool compress) { Common::String fileSpec = getSavePath(); if (fileSpec.lastChar() != '/') @@ -56,7 +67,7 @@ Common::OutSaveFile *GBAMPSaveFileManager::openForSaving(const Common::String &f Common::WriteStream *stream = DS::DSFileStream::makeFromPath(fileSpec, true); // Use a write buffer stream = Common::wrapBufferedWriteStream(stream, SAVE_BUFFER_SIZE); - return stream; + return new Common::OutSaveFile(stream); } Common::InSaveFile *GBAMPSaveFileManager::openForLoading(const Common::String &filename) { diff --git a/backends/platform/ds/arm9/source/gbampsave.h b/backends/platform/ds/arm9/source/gbampsave.h index d86db2ec70..065ea7de68 100644 --- a/backends/platform/ds/arm9/source/gbampsave.h +++ b/backends/platform/ds/arm9/source/gbampsave.h @@ -27,6 +27,9 @@ class GBAMPSaveFileManager : public Common::SaveFileManager { public: + virtual void updateSavefilesList(Common::StringArray &lockedFiles); + virtual Common::InSaveFile *openRawFile(const Common::String &filename); + virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true); virtual Common::InSaveFile *openForLoading(const Common::String &filename); diff --git a/backends/platform/ds/arm9/source/osystem_ds.cpp b/backends/platform/ds/arm9/source/osystem_ds.cpp index c53f57523d..f23192cd9d 100644 --- a/backends/platform/ds/arm9/source/osystem_ds.cpp +++ b/backends/platform/ds/arm9/source/osystem_ds.cpp @@ -715,7 +715,7 @@ void OSystem_DS::deleteMutex(MutexRef mutex) { // and should be replaced by an AudioCDManager subclass, // see backends/audiocd/ and common/system.h -bool OSystem_DS::openCD(int drive) { +bool OSystem_DS::openCD() { return DS::CD::checkCD(); } diff --git a/backends/platform/ds/arm9/source/osystem_ds.h b/backends/platform/ds/arm9/source/osystem_ds.h index f4dbac66f7..9f73e125c2 100644 --- a/backends/platform/ds/arm9/source/osystem_ds.h +++ b/backends/platform/ds/arm9/source/osystem_ds.h @@ -130,7 +130,8 @@ public: // FIXME/TODO: The CD API as follows is *obsolete* // and should be replaced by an AudioCDManager subclass, // see backends/audiocd/ and common/system.h - virtual bool openCD(int drive); + virtual bool openCD(); + virtual void closeCD() {} virtual bool pollCD(); virtual void playCD(int track, int num_loops, int start_frame, int duration); virtual void stopCD(); diff --git a/backends/platform/ios7/ios7_common.h b/backends/platform/ios7/ios7_common.h index 12740d4ae9..3609387efd 100644 --- a/backends/platform/ios7/ios7_common.h +++ b/backends/platform/ios7/ios7_common.h @@ -62,7 +62,6 @@ enum UIViewTapDescription { }; enum GraphicsModes { - kGraphicsModeLinear = 0, kGraphicsModeNone = 1, kGraphicsMode2xSaI, @@ -80,7 +79,7 @@ struct VideoContext { VideoContext() : asprectRatioCorrection(), screenWidth(), screenHeight(), overlayVisible(false), overlayWidth(), overlayHeight(), mouseX(), mouseY(), mouseHotspotX(), mouseHotspotY(), mouseWidth(), mouseHeight(), - mouseIsVisible(), graphicsMode(kGraphicsModeNone), shakeOffsetY() { + mouseIsVisible(), graphicsMode(kGraphicsModeNone), filtering(false), shakeOffsetY() { } // Game screen state @@ -102,6 +101,7 @@ struct VideoContext { // Misc state GraphicsModes graphicsMode; + bool filtering; int shakeOffsetY; }; diff --git a/backends/platform/ios7/ios7_osys_events.cpp b/backends/platform/ios7/ios7_osys_events.cpp index 3621c084db..da467cf5d6 100644 --- a/backends/platform/ios7/ios7_osys_events.cpp +++ b/backends/platform/ios7/ios7_osys_events.cpp @@ -91,16 +91,16 @@ bool OSystem_iOS7::pollEvent(Common::Event &event) { if (!handleEvent_secondMouseUp(event, internalEvent.value1, internalEvent.value2)) return false; break; - + case kInputKeyPressed: handleEvent_keyPressed(event, internalEvent.value1); break; - + case kInputSwipe: if (!handleEvent_swipe(event, internalEvent.value1, internalEvent.value2)) return false; break; - + case kInputTap: if (!handleEvent_tap(event, (UIViewTapDescription) internalEvent.value1, internalEvent.value2)) return false; @@ -109,7 +109,7 @@ bool OSystem_iOS7::pollEvent(Common::Event &event) { default: break; } - + return true; } return false; @@ -499,14 +499,14 @@ bool OSystem_iOS7::handleEvent_swipe(Common::Event &event, int direction, int to } break; } - + event.kbd.keycode = _queuedInputEvent.kbd.keycode = keycode; event.kbd.ascii = _queuedInputEvent.kbd.ascii = 0; event.type = Common::EVENT_KEYDOWN; _queuedInputEvent.type = Common::EVENT_KEYUP; event.kbd.flags = _queuedInputEvent.kbd.flags = 0; _queuedEventTime = getMillis() + kQueuedInputEventDelay; - + return true; } else if (touches == 2) { diff --git a/backends/platform/ios7/ios7_osys_main.cpp b/backends/platform/ios7/ios7_osys_main.cpp index 25d9cbed15..3a627478f9 100644 --- a/backends/platform/ios7/ios7_osys_main.cpp +++ b/backends/platform/ios7/ios7_osys_main.cpp @@ -52,8 +52,7 @@ const OSystem::GraphicsMode OSystem_iOS7::s_supportedGraphicsModes[] = { - { "none", "No filtering", kGraphicsModeNone }, - { "linear", "Linear filtering", kGraphicsModeLinear }, + { "none", "Normal", kGraphicsModeNone }, #ifdef ENABLE_IOS7_SCALERS #ifdef USE_SCALERS @@ -171,6 +170,7 @@ void OSystem_iOS7::initBackend() { bool OSystem_iOS7::hasFeature(Feature f) { switch (f) { case kFeatureCursorPalette: + case kFeatureFilteringMode: return true; default: @@ -187,6 +187,9 @@ void OSystem_iOS7::setFeatureState(Feature f, bool enable) { _mouseCursorPaletteEnabled = enable; } break; + case kFeatureFilteringMode: + _videoContext->filtering = enable; + break; case kFeatureAspectRatioCorrection: _videoContext->asprectRatioCorrection = enable; break; @@ -200,6 +203,8 @@ bool OSystem_iOS7::getFeatureState(Feature f) { switch (f) { case kFeatureCursorPalette: return _mouseCursorPaletteEnabled; + case kFeatureFilteringMode: + return _videoContext->filtering; case kFeatureAspectRatioCorrection: return _videoContext->asprectRatioCorrection; diff --git a/backends/platform/ios7/ios7_osys_main.h b/backends/platform/ios7/ios7_osys_main.h index 174c160bd6..0f89cf7aa5 100644 --- a/backends/platform/ios7/ios7_osys_main.h +++ b/backends/platform/ios7/ios7_osys_main.h @@ -123,6 +123,9 @@ public: static OSystem_iOS7 *sharedInstance(); virtual void initBackend(); + + virtual void engineInit(); + virtual void engineDone(); virtual bool hasFeature(Feature f); virtual void setFeatureState(Feature f, bool enable); diff --git a/backends/platform/ios7/ios7_osys_video.mm b/backends/platform/ios7/ios7_osys_video.mm index 6784cf46f5..1ec0defd7e 100644 --- a/backends/platform/ios7/ios7_osys_video.mm +++ b/backends/platform/ios7/ios7_osys_video.mm @@ -61,6 +61,18 @@ void OSystem_iOS7::fatalError() { } } +void OSystem_iOS7::engineInit() { + EventsBaseBackend::engineInit(); + // Prevent the device going to sleep during game play (and in particular cut scenes) + [[UIApplication sharedApplication] setIdleTimerDisabled:YES]; +} + +void OSystem_iOS7::engineDone() { + EventsBaseBackend::engineDone(); + // Allow the device going to sleep if idle while in the Launcher + [[UIApplication sharedApplication] setIdleTimerDisabled:NO]; +} + void OSystem_iOS7::initVideoContext() { _videoContext = [[iOS7AppDelegate iPhoneView] getVideoContext]; } @@ -76,7 +88,6 @@ int OSystem_iOS7::getDefaultGraphicsMode() const { bool OSystem_iOS7::setGraphicsMode(int mode) { switch (mode) { case kGraphicsModeNone: - case kGraphicsModeLinear: case kGraphicsMode2xSaI: case kGraphicsModeSuper2xSaI: case kGraphicsModeSuperEagle: diff --git a/backends/platform/ios7/ios7_video.mm b/backends/platform/ios7/ios7_video.mm index 5c0434d43e..5baa83e8e8 100644 --- a/backends/platform/ios7/ios7_video.mm +++ b/backends/platform/ios7/ios7_video.mm @@ -442,26 +442,7 @@ uint getSizeNextPOT(uint size) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); printOpenGLError(); - GLint filter = GL_LINEAR; - - switch (_videoContext.graphicsMode) { - case kGraphicsModeNone: - filter = GL_NEAREST; - break; - - case kGraphicsModeLinear: - case kGraphicsMode2xSaI: - case kGraphicsModeSuper2xSaI: - case kGraphicsModeSuperEagle: - case kGraphicsModeAdvMame2x: - case kGraphicsModeAdvMame3x: - case kGraphicsModeHQ2x: - case kGraphicsModeHQ3x: - case kGraphicsModeTV2x: - case kGraphicsModeDotMatrix: - filter = GL_LINEAR; - break; - } + GLint filter = _videoContext.filtering ? GL_LINEAR : GL_NEAREST; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); printOpenGLError(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); printOpenGLError(); @@ -478,9 +459,6 @@ uint getSizeNextPOT(uint size) { int scalerScale = 1; switch (_videoContext.graphicsMode) { - case kGraphicsModeLinear: - break; - case kGraphicsModeNone: break; #ifdef USE_SCALERS @@ -931,7 +909,7 @@ uint getSizeNextPOT(uint size) { CGPoint point = [touch locationInView:self]; if (![self getMouseCoords:point eventX:&x eventY:&y]) return; - + [self addEvent:InternalEvent(kInputMouseSecondDragged, x, y)]; } } diff --git a/backends/platform/maemo/debian/changelog b/backends/platform/maemo/debian/changelog index 6b6d1aebd8..0d7d07443e 100644 --- a/backends/platform/maemo/debian/changelog +++ b/backends/platform/maemo/debian/changelog @@ -1,8 +1,20 @@ -scummvm (1.9.0~git) unstable; urgency=low +scummvm (1.10.0~git) unstable; urgency=low * Development snapshot - -- Tarek Soliman <tsoliman@scummvm.org> Fri, 26 Feb 2016 21:11:20 -0600 + -- Tarek Soliman <tsoliman@scummvm.org> Mon, 10 Oct 2016 20:35:19 -0500 + +scummvm (1.9.0) unstable; urgency=low + + * 1.9.0 release + + -- Tarek Soliman <tsoliman@scummvm.org> Mon, 10 Oct 2016 20:35:18 -0500 + +scummvm (1.8.1) unstable; urgency=low + + * 1.8.1 release + + -- Tarek Soliman <tsoliman@scummvm.org> Fri, 20 May 2016 20:05:11 -0500 scummvm (1.8.0) unstable; urgency=low diff --git a/backends/platform/n64/framfs_save_manager.h b/backends/platform/n64/framfs_save_manager.h index a066854aab..24f9bf10ce 100644 --- a/backends/platform/n64/framfs_save_manager.h +++ b/backends/platform/n64/framfs_save_manager.h @@ -65,12 +65,15 @@ public: } }; -class OutFRAMSave : public Common::OutSaveFile { +class OutFRAMSave : public Common::WriteStream { private: FRAMFILE *fd; public: uint32 write(const void *buf, uint32 cnt); + virtual int32 pos() const { + return framfs_tell(fd); + } OutFRAMSave(const char *_filename) : fd(NULL) { fd = framfs_open(_filename, "w"); @@ -99,11 +102,26 @@ public: class FRAMSaveManager : public Common::SaveFileManager { public: + virtual void updateSavefilesList(Common::StringArray &lockedFiles) { + // this method is used to lock saves while cloud syncing + // as there is no network on N64, this method wouldn't be used + // thus it's not implemtented + } + + virtual Common::InSaveFile *openRawFile(const Common::String &filename) { + InFRAMSave *s = new InFRAMSave(); + if (s->readSaveGame(filename.c_str())) { + return s; + } else { + delete s; + return 0; + } + } virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { OutFRAMSave *s = new OutFRAMSave(filename.c_str()); if (!s->err()) { - return compress ? Common::wrapCompressedWriteStream(s) : s; + return new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(s) : s); } else { delete s; return 0; diff --git a/backends/platform/n64/pakfs_save_manager.h b/backends/platform/n64/pakfs_save_manager.h index ec66c80b73..8e16d1fce4 100644 --- a/backends/platform/n64/pakfs_save_manager.h +++ b/backends/platform/n64/pakfs_save_manager.h @@ -65,13 +65,17 @@ public: } }; -class OutPAKSave : public Common::OutSaveFile { +class OutPAKSave : public Common::WriteStream { private: PAKFILE *fd; public: uint32 write(const void *buf, uint32 cnt); + virtual int32 pos() const { + return pakfs_tell(fd); + } + OutPAKSave(const char *_filename) : fd(NULL) { fd = pakfs_open(_filename, "w"); } @@ -100,11 +104,26 @@ public: class PAKSaveManager : public Common::SaveFileManager { public: + virtual void updateSavefilesList(Common::StringArray &lockedFiles) { + // this method is used to lock saves while cloud syncing + // as there is no network on N64, this method wouldn't be used + // thus it's not implemtented + } + + virtual Common::InSaveFile *openRawFile(const Common::String &filename) { + InPAKSave *s = new InPAKSave(); + if (s->readSaveGame(filename.c_str())) { + return s; + } else { + delete s; + return NULL; + } + } virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true) { OutPAKSave *s = new OutPAKSave(filename.c_str()); if (!s->err()) { - return compress ? Common::wrapCompressedWriteStream(s) : s; + return new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(s) : s); } else { delete s; return NULL; diff --git a/backends/platform/ps2/eecodyvdfs.c b/backends/platform/ps2/eecodyvdfs.c index f410b0c8ec..a4d60a7501 100644 --- a/backends/platform/ps2/eecodyvdfs.c +++ b/backends/platform/ps2/eecodyvdfs.c @@ -8,12 +8,12 @@ * 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. diff --git a/backends/platform/ps2/iop/CoDyVDfs/iop/codyvdfs.c b/backends/platform/ps2/iop/CoDyVDfs/iop/codyvdfs.c index 2a94560843..3fe0d408c4 100644 --- a/backends/platform/ps2/iop/CoDyVDfs/iop/codyvdfs.c +++ b/backends/platform/ps2/iop/CoDyVDfs/iop/codyvdfs.c @@ -8,12 +8,12 @@ * 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. diff --git a/backends/platform/ps2/iop/CoDyVDfs/iop/fiofs.c b/backends/platform/ps2/iop/CoDyVDfs/iop/fiofs.c index 611211a715..dcf9074916 100644 --- a/backends/platform/ps2/iop/CoDyVDfs/iop/fiofs.c +++ b/backends/platform/ps2/iop/CoDyVDfs/iop/fiofs.c @@ -8,12 +8,12 @@ * 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. diff --git a/backends/platform/ps2/iop/CoDyVDfs/iop/rpcfs.c b/backends/platform/ps2/iop/CoDyVDfs/iop/rpcfs.c index 983ae38716..6d12a451e3 100644 --- a/backends/platform/ps2/iop/CoDyVDfs/iop/rpcfs.c +++ b/backends/platform/ps2/iop/CoDyVDfs/iop/rpcfs.c @@ -8,12 +8,12 @@ * 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. diff --git a/backends/platform/ps2/irxboot.cpp b/backends/platform/ps2/irxboot.cpp index 0e05047c6e..64d6e989d6 100644 --- a/backends/platform/ps2/irxboot.cpp +++ b/backends/platform/ps2/irxboot.cpp @@ -151,7 +151,7 @@ int loadIrxModules(int device, const char *irxPath, IrxReference **modules, IrxT irxFiles = irxType[type]; numFiles = numIrx[type]; resModules = (IrxReference *)memalign(64, numFiles * sizeof(IrxReference)); - curModule = resModules; + curModule = resModules; for (int i = 0; i < numFiles; i++) { curModule->fileRef = irxFiles + i; diff --git a/backends/platform/ps2/savefilemgr.cpp b/backends/platform/ps2/savefilemgr.cpp index 4fd2b1c72b..4cd988074e 100644 --- a/backends/platform/ps2/savefilemgr.cpp +++ b/backends/platform/ps2/savefilemgr.cpp @@ -82,7 +82,11 @@ void Ps2SaveFileManager::mcSplit(char *full, char *game, char *ext) { // TODO } -Common::InSaveFile *Ps2SaveFileManager::openForLoading(const Common::String &filename) { +void Ps2SaveFileManager::updateSavefilesList(Common::StringArray &lockedFiles) { + // TODO: implement this (locks files, preventing them from being listed, saved or loaded) +} + +Common::InSaveFile *Ps2SaveFileManager::openRawFile(const Common::String &filename) { Common::FSNode savePath(ConfMan.get("savepath")); // TODO: is this fast? Common::SeekableReadStream *sf; @@ -141,7 +145,12 @@ Common::InSaveFile *Ps2SaveFileManager::openForLoading(const Common::String &fil // _screen->wantAnim(false); - return Common::wrapCompressedReadStream(sf); + return sf; +} + +Common::InSaveFile *Ps2SaveFileManager::openForLoading(const Common::String &filename) { + Common::SeekableReadStream *sf = openRawFile(filename); + return (sf == NULL ? NULL : Common::wrapCompressedReadStream(sf)); } Common::OutSaveFile *Ps2SaveFileManager::openForSaving(const Common::String &filename, bool compress) { @@ -192,7 +201,7 @@ Common::OutSaveFile *Ps2SaveFileManager::openForSaving(const Common::String &fil } _screen->wantAnim(false); - return compress ? Common::wrapCompressedWriteStream(sf) : sf; + return new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(sf) : sf); } bool Ps2SaveFileManager::removeSavefile(const Common::String &filename) { diff --git a/backends/platform/ps2/savefilemgr.h b/backends/platform/ps2/savefilemgr.h index 547f16fa77..3d45382c64 100644 --- a/backends/platform/ps2/savefilemgr.h +++ b/backends/platform/ps2/savefilemgr.h @@ -34,6 +34,9 @@ public: Ps2SaveFileManager(OSystem_PS2 *system, Gs2dScreen *screen); virtual ~Ps2SaveFileManager(); + virtual void updateSavefilesList(Common::StringArray &lockedFiles); + virtual Common::InSaveFile *openRawFile(const Common::String &filename); + virtual Common::InSaveFile *openForLoading(const Common::String &filename); virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true); virtual Common::StringArray listSavefiles(const Common::String &pattern); diff --git a/backends/platform/ps2/systemps2.cpp b/backends/platform/ps2/systemps2.cpp index 1aa74b7d9b..e914cdb9c9 100644 --- a/backends/platform/ps2/systemps2.cpp +++ b/backends/platform/ps2/systemps2.cpp @@ -628,7 +628,7 @@ bool OSystem_PS2::usbMassPresent(void) { } bool OSystem_PS2::netPresent(void) { - if (_useNet) + if (_useNet) return true; return false; diff --git a/backends/platform/psp/README.PSP b/backends/platform/psp/README.PSP index 9467d4b993..46e027f4f7 100644 --- a/backends/platform/psp/README.PSP +++ b/backends/platform/psp/README.PSP @@ -1,4 +1,4 @@ -ScummVM-PSP 1.9.0git README +ScummVM-PSP 1.10.0git README ============================================================================== Installation diff --git a/backends/platform/psp/build-psp.sh b/backends/platform/psp/build-psp.sh new file mode 100755 index 0000000000..1fbeb408a1 --- /dev/null +++ b/backends/platform/psp/build-psp.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +export PATH=/opt/toolchains/psp/bin:$PATH +export PSPDEV=/opt/toolchains/psp +CXXFLAGS="-isystem /opt/toolchains/psp/include" +export CXXFLAGS +export LDFLAGS=-L/opt/toolchains/psp/lib +export LD_LIBRARY_PATH=/opt/toolchains/legacy-shared-objects +make distclean +./configure --host=psp --disable-debug --enable-plugins --default-dynamic --enable-release +make -j4 +mkdir scummvm-1.9.0 +cp -r AUTHORS COPYING COPYING.LGPL COPYING.BSD COPYRIGHT NEWS README gui/themes/translations.dat gui/themes/scummclassic.zip gui/themes/scummmodern.zip dists/engine-data/access.dat dists/engine-data/drascula.dat dists/engine-data/hugo.dat dists/engine-data/kyra.dat dists/engine-data/lure.dat dists/engine-data/mort.dat dists/engine-data/neverhood.dat dists/engine-data/queen.tbl dists/engine-data/sky.cpt dists/engine-data/teenagent.dat dists/engine-data/tony.dat dists/engine-data/toon.dat dists/engine-data/wintermute.zip dists/pred.dic EBOOT.PBP plugins backends/platform/psp/kbd.zip scummvm-1.9.0 +zip -r9 scummvm-1.9.0-psp.zip scummvm-1.9.0 + diff --git a/backends/platform/sdl/amigaos/amigaos-main.cpp b/backends/platform/sdl/amigaos/amigaos-main.cpp index 65da6bbf85..7bbf8d1fff 100644 --- a/backends/platform/sdl/amigaos/amigaos-main.cpp +++ b/backends/platform/sdl/amigaos/amigaos-main.cpp @@ -24,13 +24,44 @@ #if defined(__amigaos4__) +#include "backends/fs/amigaos4/amigaos4-fs.h" #include "backends/platform/sdl/amigaos/amigaos.h" #include "backends/plugins/sdl/sdl-provider.h" #include "base/main.h" int main(int argc, char *argv[]) { - // Set up a stack cookie to avoid crashes due to too few stack set by users + // The following will gather the application name and add the install path + // to a variable in AmigaOS4's ENV(ARC) system. It will be placed in AppPaths + // so that ScummVM can become AmiUpdate aware + const char *const appname = "ScummVM"; + + BPTR lock; + APTR oldwin; + + // Obtain a lock to the home directory + if ((lock = IDOS->GetProgramDir())) { + TEXT progpath[2048]; + TEXT apppath[1024] = "AppPaths"; + + if (IDOS->DevNameFromLock(lock, + progpath, + sizeof(progpath), + DN_FULLPATH)) { + + // Stop any "Insert volume..." type requesters + oldwin = IDOS->SetProcWindow((APTR)-1); + + // Finally, set the variable to the path the executable was run from + IDOS->AddPart( apppath, appname, 1024); + IDOS->SetVar( apppath, progpath, -1, GVF_GLOBAL_ONLY|GVF_SAVE_VAR ); + + // Turn system requesters back on + IDOS->SetProcWindow( oldwin ); + } + } + + // Set up a stack cookie to avoid crashes from a stack set too low static const char *stack_cookie __attribute__((used)) = "$STACK: 600000"; // Create our OSystem instance @@ -44,7 +75,7 @@ int main(int argc, char *argv[]) { PluginManager::instance().addPluginProvider(new SDLPluginProvider()); #endif - // Invoke the actual ScummVM main entry point: + // Invoke the actual ScummVM main entry point int res = scummvm_main(argc, argv); // Free OSystem diff --git a/backends/platform/sdl/macosx/macosx.cpp b/backends/platform/sdl/macosx/macosx.cpp index 38a2d7441c..e90d459e67 100644 --- a/backends/platform/sdl/macosx/macosx.cpp +++ b/backends/platform/sdl/macosx/macosx.cpp @@ -27,11 +27,13 @@ #ifdef MACOSX -#include "backends/platform/sdl/macosx/macosx.h" +#include "backends/audiocd/macosx/macosx-audiocd.h" #include "backends/mixer/doublebuffersdl/doublebuffersdl-mixer.h" #include "backends/platform/sdl/macosx/appmenu_osx.h" +#include "backends/platform/sdl/macosx/macosx.h" #include "backends/updates/macosx/macosx-updates.h" #include "backends/taskbar/macosx/macosx-taskbar.h" +#include "backends/platform/sdl/macosx/macosx_wrapper.h" #include "common/archive.h" #include "common/config-manager.h" @@ -54,7 +56,7 @@ void OSystem_MacOSX::init() { // Initialize taskbar manager _taskbarManager = new MacOSXTaskbarManager(); #endif - + // Invoke parent implementation of this method OSystem_POSIX::init(); } @@ -105,7 +107,7 @@ void OSystem_MacOSX::addSysArchivesToSearchSet(Common::SearchSet &s, int priorit } bool OSystem_MacOSX::hasFeature(Feature f) { - if (f == kFeatureDisplayLogFile) + if (f == kFeatureDisplayLogFile || f == kFeatureClipboardSupport || f == kFeatureOpenUrl) return true; return OSystem_POSIX::hasFeature(f); } @@ -123,6 +125,21 @@ bool OSystem_MacOSX::displayLogFile() { return err != noErr; } +bool OSystem_MacOSX::hasTextInClipboard() { + return hasTextInClipboardMacOSX(); +} + +Common::String OSystem_MacOSX::getTextFromClipboard() { + return getTextFromClipboardMacOSX(); +} + +bool OSystem_MacOSX::openUrl(const Common::String &url) { + CFURLRef urlRef = CFURLCreateWithBytes (NULL, (UInt8*)url.c_str(), url.size(), kCFStringEncodingASCII, NULL); + OSStatus err = LSOpenCFURLRef(urlRef, NULL); + CFRelease(urlRef); + return err != noErr; +} + Common::String OSystem_MacOSX::getSystemLanguage() const { #if defined(USE_DETECTLANG) && defined(USE_TRANSLATION) CFArrayRef availableLocalizations = CFBundleCopyBundleLocalizations(CFBundleGetMainBundle()); @@ -170,4 +187,8 @@ Common::String OSystem_MacOSX::getSystemLanguage() const { #endif // USE_DETECTLANG } +AudioCDManager *OSystem_MacOSX::createAudioCDManager() { + return createMacOSXAudioCDManager(); +} + #endif diff --git a/backends/platform/sdl/macosx/macosx.h b/backends/platform/sdl/macosx/macosx.h index c8b4beaeec..72bb4a4787 100644 --- a/backends/platform/sdl/macosx/macosx.h +++ b/backends/platform/sdl/macosx/macosx.h @@ -33,11 +33,21 @@ public: virtual bool displayLogFile(); + virtual bool hasTextInClipboard(); + virtual Common::String getTextFromClipboard(); + + virtual bool openUrl(const Common::String &url); + virtual Common::String getSystemLanguage() const; virtual void init(); virtual void initBackend(); virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); + +protected: + // Override createAudioCDManager() to get our Mac-specific + // version. + virtual AudioCDManager *createAudioCDManager(); }; #endif diff --git a/backends/platform/sdl/macosx/macosx_wrapper.h b/backends/platform/sdl/macosx/macosx_wrapper.h new file mode 100644 index 0000000000..3b346fc486 --- /dev/null +++ b/backends/platform/sdl/macosx/macosx_wrapper.h @@ -0,0 +1,31 @@ +/* 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_MACOSX_WRAPPER_H +#define PLATFORM_SDL_MACOSX_WRAPPER_H + +#include <common/str.h> + +bool hasTextInClipboardMacOSX(); +Common::String getTextFromClipboardMacOSX(); + +#endif diff --git a/backends/platform/sdl/macosx/macosx_wrapper.mm b/backends/platform/sdl/macosx/macosx_wrapper.mm new file mode 100644 index 0000000000..8ec9eac5ac --- /dev/null +++ b/backends/platform/sdl/macosx/macosx_wrapper.mm @@ -0,0 +1,48 @@ +/* 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. + * + */ + +// Disable symbol overrides so that we can use system headers. +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/platform/sdl/macosx/macosx_wrapper.h" + +#include <AppKit/NSPasteboard.h> +#include <Foundation/NSArray.h> + +bool hasTextInClipboardMacOSX() { + return [[NSPasteboard generalPasteboard] availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]] != nil; +} + +Common::String getTextFromClipboardMacOSX() { + if (!hasTextInClipboardMacOSX()) + return Common::String(); + // Note: on OS X 10.6 and above it is recommanded to use NSPasteboardTypeString rather than NSStringPboardType. + // But since we still target older version use NSStringPboardType. + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + NSString* str = [pb stringForType:NSStringPboardType]; + if (str == nil) + return Common::String(); + // If the string cannot be represented using the requested encoding we get a null pointer below. + // This is fine as ScummVM would not know what to do with non-ASCII characters (although maybe + // we should use NSISOLatin1StringEncoding?). + return Common::String([str cStringUsingEncoding:NSASCIIStringEncoding]); +} diff --git a/backends/platform/sdl/module.mk b/backends/platform/sdl/module.mk index 74dd506d31..84ce272d3c 100644 --- a/backends/platform/sdl/module.mk +++ b/backends/platform/sdl/module.mk @@ -14,6 +14,7 @@ ifdef MACOSX MODULE_OBJS += \ macosx/macosx-main.o \ macosx/macosx.o \ + macosx/macosx_wrapper.o \ macosx/appmenu_osx.o endif diff --git a/backends/platform/sdl/posix/posix-main.cpp b/backends/platform/sdl/posix/posix-main.cpp index d07db11b0c..5deebb0ae3 100644 --- a/backends/platform/sdl/posix/posix-main.cpp +++ b/backends/platform/sdl/posix/posix-main.cpp @@ -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) +#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(ANDROIDSDL) #include "backends/platform/sdl/posix/posix.h" #include "backends/plugins/sdl/sdl-provider.h" diff --git a/backends/platform/sdl/posix/posix.cpp b/backends/platform/sdl/posix/posix.cpp index 525c74a91a..b805a452cf 100644 --- a/backends/platform/sdl/posix/posix.cpp +++ b/backends/platform/sdl/posix/posix.cpp @@ -25,6 +25,7 @@ #define FORBIDDEN_SYMBOL_EXCEPTION_exit #define FORBIDDEN_SYMBOL_EXCEPTION_unistd_h #define FORBIDDEN_SYMBOL_EXCEPTION_time_h //On IRIX, sys/stat.h includes sys/time.h +#define FORBIDDEN_SYMBOL_EXCEPTION_system #include "common/scummsys.h" @@ -36,6 +37,14 @@ #include "backends/fs/posix/posix-fs.h" #include "backends/taskbar/unity/unity-taskbar.h" +#ifdef USE_LINUXCD +#include "backends/audiocd/linux/linux-audiocd.h" +#endif + +#include "common/textconsole.h" + +#include <stdlib.h> +#include <errno.h> #include <sys/stat.h> #include <sys/wait.h> #include <unistd.h> @@ -73,7 +82,7 @@ void OSystem_POSIX::initBackend() { } bool OSystem_POSIX::hasFeature(Feature f) { - if (f == kFeatureDisplayLogFile) + if (f == kFeatureDisplayLogFile || f == kFeatureOpenUrl) return true; return OSystem_SDL::hasFeature(f); } @@ -136,6 +145,24 @@ Common::String OSystem_POSIX::getDefaultConfigFileName() { return configFile; } +void OSystem_POSIX::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) { +#ifdef DATA_PATH + const char *snap = getenv("SNAP"); + if (snap) { + Common::String dataPath = Common::String(snap) + DATA_PATH; + Common::FSNode dataNode(dataPath); + if (dataNode.exists() && dataNode.isDirectory()) { + // This is the same priority which is used for the data path (below), + // but we insert this one first, so it will be searched first. + s.add(dataPath, new Common::FSDirectory(dataNode, 4), priority); + } + } +#endif + + // For now, we always add the data path, just in case SNAP doesn't make sense. + OSystem_SDL::addSysArchivesToSearchSet(s, priority); +} + Common::WriteStream *OSystem_POSIX::createLogFile() { // Start out by resetting _logFilePath, so that in case // of a failure, we know that no log file is open. @@ -238,5 +265,58 @@ bool OSystem_POSIX::displayLogFile() { return WIFEXITED(status) && WEXITSTATUS(status) == 0; } +bool OSystem_POSIX::openUrl(const Common::String &url) { + // inspired by Qt's "qdesktopservices_x11.cpp" + + // try "standards" + if (launchBrowser("xdg-open", url)) + return true; + if (launchBrowser(getenv("DEFAULT_BROWSER"), url)) + return true; + if (launchBrowser(getenv("BROWSER"), url)) + return true; + + // try desktop environment specific tools + if (launchBrowser("gnome-open", url)) // gnome + return true; + if (launchBrowser("kfmclient openURL", url)) // kde + return true; + if (launchBrowser("exo-open", url)) // xfce + return true; + + // try browser names + if (launchBrowser("firefox", url)) + return true; + if (launchBrowser("mozilla", url)) + return true; + if (launchBrowser("netscape", url)) + return true; + if (launchBrowser("opera", url)) + return true; + if (launchBrowser("chromium-browser", url)) + return true; + if (launchBrowser("google-chrome", url)) + return true; + + warning("openUrl() (POSIX) failed to open URL"); + return false; +} + +bool OSystem_POSIX::launchBrowser(const Common::String& client, const Common::String &url) { + // FIXME: system's input must be heavily escaped + // well, when url's specified by user + // it's OK now (urls are hardcoded somewhere in GUI) + Common::String cmd = client + " " + url; + return (system(cmd.c_str()) != -1); +} + + +AudioCDManager *OSystem_POSIX::createAudioCDManager() { +#ifdef USE_LINUXCD + return createLinuxAudioCDManager(); +#else + return OSystem_SDL::createAudioCDManager(); +#endif +} #endif diff --git a/backends/platform/sdl/posix/posix.h b/backends/platform/sdl/posix/posix.h index f67515ddb3..bd3a069d5b 100644 --- a/backends/platform/sdl/posix/posix.h +++ b/backends/platform/sdl/posix/posix.h @@ -35,9 +35,13 @@ public: virtual bool displayLogFile(); + virtual bool openUrl(const Common::String &url); + virtual void init(); virtual void initBackend(); + virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); + protected: /** * Base string for creating the default path and filename for the @@ -59,6 +63,10 @@ protected: virtual Common::String getDefaultConfigFileName(); virtual Common::WriteStream *createLogFile(); + + virtual AudioCDManager *createAudioCDManager(); + + bool launchBrowser(const Common::String& client, const Common::String &url); }; #endif diff --git a/backends/platform/sdl/sdl-sys.h b/backends/platform/sdl/sdl-sys.h index 219755219f..9ebd123bb4 100644 --- a/backends/platform/sdl/sdl-sys.h +++ b/backends/platform/sdl/sdl-sys.h @@ -52,6 +52,72 @@ typedef struct { int FAKE; } FAKE_FILE; #define strncasecmp FAKE_strncasecmp #endif +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_exit) +#undef exit +#define exit FAKE_exit +#endif + +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_abort) +#undef abort +#define abort FAKE_abort +#endif + +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_system) +#undef system +#define system FAKE_system +#endif + +// Fix compilation with MacPorts SDL 2 +// It needs various (usually forbidden) symbols from time.h +#ifndef FORBIDDEN_SYMBOL_EXCEPTION_time_h + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_asctime) + #undef asctime + #define asctime FAKE_asctime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_clock) + #undef clock + #define clock FAKE_clock + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_ctime) + #undef ctime + #define ctime FAKE_ctime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_difftime) + #undef difftime + #define difftime FAKE_difftime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_getdate) + #undef getdate + #define getdate FAKE_getdate + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_gmtime) + #undef gmtime + #define gmtime FAKE_gmtime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_localtime) + #undef localtime + #define localtime FAKE_localtime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_mktime) + #undef mktime + #define mktime FAKE_mktime + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_time) + #undef time + #define time FAKE_time + #endif + +#endif // FORBIDDEN_SYMBOL_EXCEPTION_time_h + // HACK: SDL might include windows.h which defines its own ARRAYSIZE. // However, we want to use the version from common/util.h. Thus, we make sure // that we actually have this definition after including the SDL headers. @@ -146,6 +212,71 @@ typedef struct { int FAKE; } FAKE_FILE; #define strncasecmp FORBIDDEN_SYMBOL_REPLACEMENT #endif +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_exit) +#undef exit +#define exit(a) FORBIDDEN_SYMBOL_REPLACEMENT +#endif + +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_abort) +#undef abort +#define abort() FORBIDDEN_SYMBOL_REPLACEMENT +#endif + +#if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_system) +#undef system +#define system(a) FORBIDDEN_SYMBOL_REPLACEMENT +#endif + +// re-forbid all those time.h symbols again (if they were forbidden) +#ifndef FORBIDDEN_SYMBOL_EXCEPTION_time_h + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_asctime) + #undef asctime + #define asctime(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_clock) + #undef clock + #define clock() FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_ctime) + #undef ctime + #define ctime(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_difftime) + #undef difftime + #define difftime(a,b) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_getdate) + #undef getdate + #define getdate(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_gmtime) + #undef gmtime + #define gmtime(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_localtime) + #undef localtime + #define localtime(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_mktime) + #undef mktime + #define mktime(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + + #if !defined(FORBIDDEN_SYMBOL_ALLOW_ALL) && !defined(FORBIDDEN_SYMBOL_EXCEPTION_time) + #undef time + #define time(a) FORBIDDEN_SYMBOL_REPLACEMENT + #endif + +#endif // FORBIDDEN_SYMBOL_EXCEPTION_time_h + // SDL 2 has major API changes. We redefine constants which got renamed to // ease the transition. This is sometimes dangerous because the values changed // too! diff --git a/backends/platform/sdl/sdl-window.cpp b/backends/platform/sdl/sdl-window.cpp index 6d35f77ae0..609186a061 100644 --- a/backends/platform/sdl/sdl-window.cpp +++ b/backends/platform/sdl/sdl-window.cpp @@ -150,7 +150,7 @@ bool SdlWindow::hasMouseFocus() const { void SdlWindow::warpMouseInWindow(uint x, uint y) { #if SDL_VERSION_ATLEAST(2, 0, 0) - if (_window) { + if (_window && hasMouseFocus()) { SDL_WarpMouseInWindow(_window, x, y); } #else diff --git a/backends/platform/sdl/sdl-window.h b/backends/platform/sdl/sdl-window.h index 58b898f824..b62860960d 100644 --- a/backends/platform/sdl/sdl-window.h +++ b/backends/platform/sdl/sdl-window.h @@ -81,7 +81,7 @@ public: SDL_Window *getSDLWindow() const { return _window; } /** - * Creates a new SDL window (and destroies the old one). + * Creates a new SDL window (and destroys the old one). * * @param width Width of the window. * @param height Height of the window. @@ -91,7 +91,7 @@ public: bool createWindow(int width, int height, uint32 flags); /** - * Destroies the current SDL window. + * Destroys the current SDL window. */ void destroyWindow(); diff --git a/backends/platform/sdl/sdl.cpp b/backends/platform/sdl/sdl.cpp index fffb9d56d9..74c40ade10 100644 --- a/backends/platform/sdl/sdl.cpp +++ b/backends/platform/sdl/sdl.cpp @@ -60,6 +60,14 @@ #endif // !WIN32 #endif +#ifdef USE_SDL_NET +#include <SDL_net.h> +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 0) +#include <SDL_clipboard.h> +#endif + OSystem_SDL::OSystem_SDL() : #ifdef USE_OPENGL @@ -73,6 +81,9 @@ OSystem_SDL::OSystem_SDL() #endif _inited(false), _initedSDL(false), +#ifdef USE_SDL_NET + _initedSDLnet(false), +#endif _logger(0), _mixerManager(0), _eventSource(0), @@ -120,6 +131,10 @@ OSystem_SDL::~OSystem_SDL() { delete _logger; _logger = 0; +#ifdef USE_SDL_NET + if (_initedSDLnet) SDLNet_Quit(); +#endif + SDL_Quit(); } @@ -160,6 +175,13 @@ void OSystem_SDL::init() { } +bool OSystem_SDL::hasFeature(Feature f) { +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (f == kFeatureClipboardSupport) return true; +#endif + return ModularBackend::hasFeature(f); +} + void OSystem_SDL::initBackend() { // Check if backend has not been initialized assert(!_inited); @@ -245,15 +267,7 @@ void OSystem_SDL::initBackend() { _timerManager = new SdlTimerManager(); #endif - if (_audiocdManager == 0) { - // Audio CD support was removed with SDL 2.0 -#if SDL_VERSION_ATLEAST(2, 0, 0) - _audiocdManager = new DefaultAudioCDManager(); -#else - _audiocdManager = new SdlAudioCDManager(); -#endif - - } + _audiocdManager = createAudioCDManager(); // Setup a custom program icon. _window->setupIcon(); @@ -302,6 +316,17 @@ void OSystem_SDL::initSDL() { _initedSDL = true; } + +#ifdef USE_SDL_NET + // Check if SDL_net has not been initialized + if (!_initedSDLnet) { + // Initialize SDL_net + if (SDLNet_Init() == -1) + error("Could not initialize SDL_net: %s", SDLNet_GetError()); + + _initedSDLnet = true; + } +#endif } void OSystem_SDL::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) { @@ -439,6 +464,30 @@ Common::String OSystem_SDL::getSystemLanguage() const { #endif // USE_DETECTLANG } +bool OSystem_SDL::hasTextInClipboard() { +#if SDL_VERSION_ATLEAST(2, 0, 0) + return SDL_HasClipboardText() == SDL_TRUE; +#else + return false; +#endif +} + +Common::String OSystem_SDL::getTextFromClipboard() { + if (!hasTextInClipboard()) return ""; + +#if SDL_VERSION_ATLEAST(2, 0, 0) + char *text = SDL_GetClipboardText(); + Common::String strText = text; + SDL_free(text); + + // FIXME: The string returned by SDL is in UTF-8, it is not clear + // what encoding should be used for the returned string. + return strText; +#else + return ""; +#endif +} + uint32 OSystem_SDL::getMillis(bool skipRecord) { uint32 millis = SDL_GetTicks(); @@ -491,6 +540,23 @@ Common::TimerManager *OSystem_SDL::getTimerManager() { #endif } +AudioCDManager *OSystem_SDL::createAudioCDManager() { + // Audio CD support was removed with SDL 2.0 +#if SDL_VERSION_ATLEAST(2, 0, 0) + return new DefaultAudioCDManager(); +#else + return new SdlAudioCDManager(); +#endif +} + +Common::SaveFileManager *OSystem_SDL::getSavefileManager() { +#ifdef ENABLE_EVENTRECORDER + return g_eventRec.getSaveManager(_savefileManager); +#else + return _savefileManager; +#endif +} + #ifdef USE_OPENGL const OSystem::GraphicsMode *OSystem_SDL::getSupportedGraphicsModes() const { diff --git a/backends/platform/sdl/sdl.h b/backends/platform/sdl/sdl.h index 5ee56d0568..17b4e9b001 100644 --- a/backends/platform/sdl/sdl.h +++ b/backends/platform/sdl/sdl.h @@ -55,6 +55,8 @@ public: */ virtual SdlMixerManager *getMixerManager(); + virtual bool hasFeature(Feature f); + // Override functions from ModularBackend and OSystem virtual void initBackend(); #if defined(USE_TASKBAR) @@ -69,6 +71,10 @@ public: virtual Common::String getSystemLanguage() const; + // Clipboard + virtual bool hasTextInClipboard(); + virtual Common::String getTextFromClipboard(); + virtual void setWindowCaption(const char *caption); virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); virtual uint32 getMillis(bool skipRecord = false); @@ -76,10 +82,14 @@ public: virtual void getTimeAndDate(TimeDate &td) const; virtual Audio::Mixer *getMixer(); virtual Common::TimerManager *getTimerManager(); + virtual Common::SaveFileManager *getSavefileManager(); protected: bool _inited; bool _initedSDL; +#ifdef USE_SDL_NET + bool _initedSDLnet; +#endif /** * Mixer manager that configures and setups SDL for @@ -104,6 +114,11 @@ protected: */ virtual void initSDL(); + /** + * Create the audio CD manager + */ + virtual AudioCDManager *createAudioCDManager(); + // Logging virtual Common::WriteStream *createLogFile() { return 0; } Backends::Log::Log *_logger; diff --git a/backends/platform/sdl/win32/win32.cpp b/backends/platform/sdl/win32/win32.cpp index 0f70c00b40..99c71a49e0 100644 --- a/backends/platform/sdl/win32/win32.cpp +++ b/backends/platform/sdl/win32/win32.cpp @@ -35,11 +35,13 @@ #include "common/error.h" #include "common/textconsole.h" +#include "backends/audiocd/win32/win32-audiocd.h" #include "backends/platform/sdl/win32/win32.h" #include "backends/platform/sdl/win32/win32-window.h" #include "backends/saves/windows/windows-saves.h" #include "backends/fs/windows/windows-fs-factory.h" #include "backends/taskbar/win32/win32-taskbar.h" +#include "backends/updates/win32/win32-updates.h" #include "common/memstream.h" @@ -81,13 +83,18 @@ void OSystem_Win32::initBackend() { if (_savefileManager == 0) _savefileManager = new WindowsSaveFileManager(); +#if defined(USE_SPARKLE) + // Initialize updates manager + _updateManager = new Win32UpdateManager(); +#endif + // Invoke parent implementation of this method OSystem_SDL::initBackend(); } bool OSystem_Win32::hasFeature(Feature f) { - if (f == kFeatureDisplayLogFile) + if (f == kFeatureDisplayLogFile || f == kFeatureOpenUrl) return true; return OSystem_SDL::hasFeature(f); @@ -128,6 +135,16 @@ bool OSystem_Win32::displayLogFile() { return false; } +bool OSystem_Win32::openUrl(const Common::String &url) { + const uint64 result = (uint64)ShellExecute(0, 0, /*(wchar_t*)nativeFilePath.utf16()*/url.c_str(), 0, 0, SW_SHOWNORMAL); + // ShellExecute returns a value greater than 32 if successful + if (result <= 32) { + warning("ShellExecute failed: error = %u", result); + return false; + } + return true; +} + Common::String OSystem_Win32::getDefaultConfigFileName() { char configFile[MAXPATHLEN]; @@ -318,4 +335,8 @@ void OSystem_Win32::addSysArchivesToSearchSet(Common::SearchSet &s, int priority OSystem_SDL::addSysArchivesToSearchSet(s, priority); } +AudioCDManager *OSystem_Win32::createAudioCDManager() { + return createWin32AudioCDManager(); +} + #endif diff --git a/backends/platform/sdl/win32/win32.h b/backends/platform/sdl/win32/win32.h index 473e78ff0b..636ebae88f 100644 --- a/backends/platform/sdl/win32/win32.h +++ b/backends/platform/sdl/win32/win32.h @@ -36,6 +36,8 @@ public: virtual bool displayLogFile(); + virtual bool openUrl(const Common::String &url); + protected: /** * The path of the currently open log file, if any. @@ -49,6 +51,10 @@ protected: virtual Common::String getDefaultConfigFileName(); virtual Common::WriteStream *createLogFile(); + + // Override createAudioCDManager() to get our Mac-specific + // version. + virtual AudioCDManager *createAudioCDManager(); }; #endif diff --git a/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v1.pkg b/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v1.pkg index da69d455d7..6ec03557be 100644 --- a/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v1.pkg +++ b/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v1.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v2.pkg b/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v2.pkg index 0d22c9d016..fddf772b7f 100644 --- a/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v2.pkg +++ b/backends/platform/symbian/S60/scummvm-CVS-SymbianS60v2.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3.pkg b/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3.pkg index 4c83b32805..30d2aa3db6 100644 --- a/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3.pkg +++ b/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3.pkg @@ -7,12 +7,12 @@ ; 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. diff --git a/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3_split.pkg b/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3_split.pkg index 3d55584d28..8c953d30ea 100644 --- a/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3_split.pkg +++ b/backends/platform/symbian/S60v3/scummvm-CVS-SymbianS60v3_split.pkg @@ -7,19 +7,16 @@ ; 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. ; -; $URL$ -; $Id$ -; ; ;;; diff --git a/backends/platform/symbian/S80/scummvm-CVS-SymbianS80.pkg b/backends/platform/symbian/S80/scummvm-CVS-SymbianS80.pkg index c3563b4041..cfff4be7c5 100644 --- a/backends/platform/symbian/S80/scummvm-CVS-SymbianS80.pkg +++ b/backends/platform/symbian/S80/scummvm-CVS-SymbianS80.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/S90/scummvm-CVS-SymbianS90.pkg b/backends/platform/symbian/S90/scummvm-CVS-SymbianS90.pkg index 457f41998b..9057eb66a5 100644 --- a/backends/platform/symbian/S90/scummvm-CVS-SymbianS90.pkg +++ b/backends/platform/symbian/S90/scummvm-CVS-SymbianS90.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2.pkg b/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2.pkg index e2b8a85162..26038f4875 100644 --- a/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2.pkg +++ b/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2_SE.pkg b/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2_SE.pkg index 8284f64611..7d1e2bc249 100644 --- a/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2_SE.pkg +++ b/backends/platform/symbian/UIQ2/scummvm-CVS-SymbianUIQ2_SE.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3.pkg b/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3.pkg index 8ac6f97d9f..adb869f078 100644 --- a/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3.pkg +++ b/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3_split.pkg b/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3_split.pkg index 4c46be50a1..528e39adee 100644 --- a/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3_split.pkg +++ b/backends/platform/symbian/UIQ3/scummvm-CVS-SymbianUIQ3_split.pkg @@ -8,12 +8,12 @@ ; 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. diff --git a/backends/platform/symbian/src/portdefs.h b/backends/platform/symbian/src/portdefs.h index f9da09d3eb..e2465e4767 100644 --- a/backends/platform/symbian/src/portdefs.h +++ b/backends/platform/symbian/src/portdefs.h @@ -65,7 +65,8 @@ typedef signed long int int32; #undef remove #endif -#define SMALL_SCREEN_DEVICE +#define GUI_ONLY_FULLSCREEN +#define GUI_ENABLE_KEYSDIALOG #define DISABLE_COMMAND_LINE #define USE_RGB_COLOR diff --git a/backends/platform/tizen/form.cpp b/backends/platform/tizen/form.cpp index 568829dc96..2a9a3967cc 100644 --- a/backends/platform/tizen/form.cpp +++ b/backends/platform/tizen/form.cpp @@ -318,16 +318,16 @@ void TizenAppForm::invokeShortcut() { case kControlMouse: setButtonShortcut(); break; - + case kEscapeKey: pushKey(Common::KEYCODE_ESCAPE); break; - + case kGameMenu: _buttonState = kLeftButton; pushKey(Common::KEYCODE_F5); break; - + case kShowKeypad: showKeypad(); break; @@ -338,7 +338,7 @@ void TizenAppForm::showKeypad() { // display the soft keyboard if (_state == kActiveState) { _buttonState = kLeftButton; - + Common::Event e; e.type = Common::EVENT_VIRTUAL_KEYBOARD; if (_eventQueueLock) { diff --git a/backends/platform/tizen/graphics.cpp b/backends/platform/tizen/graphics.cpp index 759c4e519d..85dbbd2c08 100644 --- a/backends/platform/tizen/graphics.cpp +++ b/backends/platform/tizen/graphics.cpp @@ -56,6 +56,7 @@ result TizenGraphicsManager::Construct() { loadEgl(); // Notify the OpenGL code about our context. + setContextType(OpenGL::kContextGLES); // We default to RGB565 and RGBA5551 which is closest to the actual output // mode we setup. @@ -102,7 +103,7 @@ Common::List<Graphics::PixelFormat> TizenGraphicsManager::getSupportedFormats() } bool TizenGraphicsManager::hasFeature(OSystem::Feature f) { - bool result = + bool result = (f == OSystem::kFeatureVirtualKeyboard || OpenGLGraphicsManager::hasFeature(f)); return result; @@ -206,3 +207,7 @@ bool TizenGraphicsManager::loadVideoMode(uint requestedWidth, uint requestedHeig void TizenGraphicsManager::refreshScreen() { eglSwapBuffers(_eglDisplay, _eglSurface); } + +void *TizenGraphicsManager::getProcAddress(const char *name) const { + return eglGetProcAddress(name); +} diff --git a/backends/platform/tizen/graphics.h b/backends/platform/tizen/graphics.h index 1522d66bbe..1798b078d8 100644 --- a/backends/platform/tizen/graphics.h +++ b/backends/platform/tizen/graphics.h @@ -63,6 +63,8 @@ protected: void refreshScreen(); + void *getProcAddress(const char *name) const; + const Graphics::Font *getFontOSD(); private: diff --git a/backends/platform/tizen/system.cpp b/backends/platform/tizen/system.cpp index a235456670..1820a28791 100644 --- a/backends/platform/tizen/system.cpp +++ b/backends/platform/tizen/system.cpp @@ -81,36 +81,41 @@ struct TizenSaveFileManager : public DefaultSaveFileManager { }; bool TizenSaveFileManager::removeSavefile(const Common::String &filename) { - Common::String savePathName = getSavePath(); + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); + if (getError().getCode() != Common::kNoError) + return false; - checkPath(Common::FSNode(savePathName)); - if (getError().getCode() != Common::kNoError) { + // Obtain node if exists. + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + if (file == _saveFileCache.end()) { return false; - } + } else { + const Common::FSNode fileNode = file->_value; + // Remove from cache, this invalidates the 'file' iterator. + _saveFileCache.erase(file); + file = _saveFileCache.end(); - // recreate FSNode since checkPath may have changed/created the directory - Common::FSNode savePath(savePathName); - Common::FSNode file = savePath.getChild(filename); + String unicodeFileName; + StringUtil::Utf8ToString(fileNode.getPath().c_str(), unicodeFileName); - String unicodeFileName; - StringUtil::Utf8ToString(file.getPath().c_str(), unicodeFileName); + switch (Tizen::Io::File::Remove(unicodeFileName)) { + case E_SUCCESS: + return true; - switch (Tizen::Io::File::Remove(unicodeFileName)) { - case E_SUCCESS: - return true; + case E_ILLEGAL_ACCESS: + setError(Common::kWritePermissionDenied, "Search or write permission denied: " + + file.getName()); + break; - case E_ILLEGAL_ACCESS: - setError(Common::kWritePermissionDenied, "Search or write permission denied: " + - file.getName()); - break; + default: + setError(Common::kPathDoesNotExist, "removeSavefile: '" + file.getName() + + "' does not exist or path is invalid"); + break; + } - default: - setError(Common::kPathDoesNotExist, "removeSavefile: '" + file.getName() + - "' does not exist or path is invalid"); - break; + return false; } - - return false; } // diff --git a/backends/platform/wince/portdefs.h b/backends/platform/wince/portdefs.h index 3304ee0893..c74fc39169 100644 --- a/backends/platform/wince/portdefs.h +++ b/backends/platform/wince/portdefs.h @@ -30,7 +30,8 @@ // Missing string/stdlib/assert declarations for WinCE 2.xx #if _WIN32_WCE < 300 - #define SMALL_SCREEN_DEVICE + #define GUI_ONLY_FULLSCREEN + #define GUI_ENABLE_KEYSDIALOG void *calloc(size_t n, size_t s); int isalnum(int c); diff --git a/backends/saves/default/default-saves.cpp b/backends/saves/default/default-saves.cpp index 4f7013724a..a0c5470c9f 100644 --- a/backends/saves/default/default-saves.cpp +++ b/backends/saves/default/default-saves.cpp @@ -27,6 +27,11 @@ #include "common/scummsys.h" +#if defined(USE_CLOUD) && defined(USE_LIBCURL) +#include "backends/cloud/cloudmanager.h" +#include "common/file.h" +#endif + #if !defined(DISABLE_DEFAULT_SAVEFILEMANAGER) #include "backends/saves/default/default-saves.h" @@ -42,6 +47,10 @@ #include <errno.h> // for removeSavefile() #endif +#if defined(USE_CLOUD) && defined(USE_LIBCURL) +const char *DefaultSaveFileManager::TIMESTAMPS_FILENAME = "timestamps"; +#endif + DefaultSaveFileManager::DefaultSaveFileManager() { } @@ -59,92 +68,155 @@ void DefaultSaveFileManager::checkPath(const Common::FSNode &dir) { } } +void DefaultSaveFileManager::updateSavefilesList(Common::StringArray &lockedFiles) { + //make it refresh the cache next time it lists the saves + _cachedDirectory = ""; + + //remember the locked files list because some of these files don't exist yet + _lockedFiles = lockedFiles; +} + Common::StringArray DefaultSaveFileManager::listSavefiles(const Common::String &pattern) { - Common::String savePathName = getSavePath(); - checkPath(Common::FSNode(savePathName)); + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); if (getError().getCode() != Common::kNoError) return Common::StringArray(); - // recreate FSNode since checkPath may have changed/created the directory - Common::FSNode savePath(savePathName); + Common::HashMap<Common::String, bool> locked; + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + locked[*i] = true; + } - Common::FSDirectory dir(savePath); - Common::ArchiveMemberList savefiles; Common::StringArray results; - Common::String search(pattern); - - if (dir.listMatchingMembers(savefiles, search) > 0) { - for (Common::ArchiveMemberList::const_iterator file = savefiles.begin(); file != savefiles.end(); ++file) { - results.push_back((*file)->getName()); + for (SaveFileCache::const_iterator file = _saveFileCache.begin(), end = _saveFileCache.end(); file != end; ++file) { + if (!locked.contains(file->_key) && file->_key.matchString(pattern, true)) { + results.push_back(file->_key); } } - return results; } -Common::InSaveFile *DefaultSaveFileManager::openForLoading(const Common::String &filename) { - // Ensure that the savepath is valid. If not, generate an appropriate error. - Common::String savePathName = getSavePath(); - checkPath(Common::FSNode(savePathName)); +Common::InSaveFile *DefaultSaveFileManager::openRawFile(const Common::String &filename) { + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); if (getError().getCode() != Common::kNoError) - return 0; + return nullptr; - // recreate FSNode since checkPath may have changed/created the directory - Common::FSNode savePath(savePathName); + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + if (file == _saveFileCache.end()) { + return nullptr; + } else { + // Open the file for loading. + Common::SeekableReadStream *sf = file->_value.createReadStream(); + return sf; + } +} - Common::FSNode file = savePath.getChild(filename); - if (!file.exists()) - return 0; +Common::InSaveFile *DefaultSaveFileManager::openForLoading(const Common::String &filename) { + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); + if (getError().getCode() != Common::kNoError) + return nullptr; - // Open the file for reading - Common::SeekableReadStream *sf = file.createReadStream(); + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + if (filename == *i) { + return nullptr; //file is locked, no loading available + } + } - return Common::wrapCompressedReadStream(sf); + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + if (file == _saveFileCache.end()) { + return nullptr; + } else { + // Open the file for loading. + Common::SeekableReadStream *sf = file->_value.createReadStream(); + return Common::wrapCompressedReadStream(sf); + } } Common::OutSaveFile *DefaultSaveFileManager::openForSaving(const Common::String &filename, bool compress) { - // Ensure that the savepath is valid. If not, generate an appropriate error. - Common::String savePathName = getSavePath(); - checkPath(Common::FSNode(savePathName)); + // Assure the savefile name cache is up-to-date. + const Common::String savePathName = getSavePath(); + assureCached(savePathName); if (getError().getCode() != Common::kNoError) - return 0; + return nullptr; - // recreate FSNode since checkPath may have changed/created the directory - Common::FSNode savePath(savePathName); + for (Common::StringArray::const_iterator i = _lockedFiles.begin(), end = _lockedFiles.end(); i != end; ++i) { + if (filename == *i) { + return nullptr; //file is locked, no saving available + } + } - Common::FSNode file = savePath.getChild(filename); +#if defined(USE_CLOUD) && defined(USE_LIBCURL) + // Update file's timestamp + Common::HashMap<Common::String, uint32> timestamps = loadTimestamps(); + timestamps[filename] = INVALID_TIMESTAMP; + saveTimestamps(timestamps); +#endif - // Open the file for saving - Common::WriteStream *sf = file.createWriteStream(); + // Obtain node. + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + Common::FSNode fileNode; - return compress ? Common::wrapCompressedWriteStream(sf) : sf; + // If the file did not exist before, we add it to the cache. + if (file == _saveFileCache.end()) { + const Common::FSNode savePath(savePathName); + fileNode = savePath.getChild(filename); + } else { + fileNode = file->_value; + } + + // Open the file for saving. + Common::WriteStream *const sf = fileNode.createWriteStream(); + Common::OutSaveFile *const result = new Common::OutSaveFile(compress ? Common::wrapCompressedWriteStream(sf) : sf); + + // Add file to cache now that it exists. + _saveFileCache[filename] = Common::FSNode(fileNode.getPath()); + + return result; } bool DefaultSaveFileManager::removeSavefile(const Common::String &filename) { - Common::String savePathName = getSavePath(); - checkPath(Common::FSNode(savePathName)); + // Assure the savefile name cache is up-to-date. + assureCached(getSavePath()); if (getError().getCode() != Common::kNoError) return false; - // recreate FSNode since checkPath may have changed/created the directory - Common::FSNode savePath(savePathName); - - Common::FSNode file = savePath.getChild(filename); +#if defined(USE_CLOUD) && defined(USE_LIBCURL) + // Update file's timestamp + Common::HashMap<Common::String, uint32> timestamps = loadTimestamps(); + Common::HashMap<Common::String, uint32>::iterator it = timestamps.find(filename); + if (it != timestamps.end()) { + timestamps.erase(it); + saveTimestamps(timestamps); + } +#endif - // FIXME: remove does not exist on all systems. If your port fails to - // compile because of this, please let us know (scummvm-devel or Fingolfin). - // There is a nicely portable workaround, too: Make this method overloadable. - if (remove(file.getPath().c_str()) != 0) { + // Obtain node if exists. + SaveFileCache::const_iterator file = _saveFileCache.find(filename); + if (file == _saveFileCache.end()) { + return false; + } else { + const Common::FSNode fileNode = file->_value; + // Remove from cache, this invalidates the 'file' iterator. + _saveFileCache.erase(file); + file = _saveFileCache.end(); + + // FIXME: remove does not exist on all systems. If your port fails to + // compile because of this, please let us know (scummvm-devel). + // There is a nicely portable workaround, too: Make this method overloadable. + if (remove(fileNode.getPath().c_str()) != 0) { #ifndef _WIN32_WCE - if (errno == EACCES) - setError(Common::kWritePermissionDenied, "Search or write permission denied: "+file.getName()); + if (errno == EACCES) + setError(Common::kWritePermissionDenied, "Search or write permission denied: "+fileNode.getName()); - if (errno == ENOENT) - setError(Common::kPathDoesNotExist, "removeSavefile: '"+file.getName()+"' does not exist or path is invalid"); + if (errno == ENOENT) + setError(Common::kPathDoesNotExist, "removeSavefile: '"+fileNode.getName()+"' does not exist or path is invalid"); #endif - return false; - } else { - return true; + return false; + } else { + return true; + } } } @@ -171,4 +243,148 @@ Common::String DefaultSaveFileManager::getSavePath() const { return dir; } +void DefaultSaveFileManager::assureCached(const Common::String &savePathName) { + // Check that path exists and is usable. + checkPath(Common::FSNode(savePathName)); + +#if defined(USE_CLOUD) && defined(USE_LIBCURL) + Common::Array<Common::String> files = CloudMan.getSyncingFiles(); //returns empty array if not syncing + if (!files.empty()) updateSavefilesList(files); //makes this cache invalid + else _lockedFiles = files; +#endif + + if (_cachedDirectory == savePathName) { + return; + } + + _saveFileCache.clear(); + _cachedDirectory.clear(); + + if (getError().getCode() != Common::kNoError) { + warning("DefaultSaveFileManager::assureCached: Can not cache path '%s': '%s'", savePathName.c_str(), getErrorDesc().c_str()); + return; + } + + // FSNode can cache its members, thus create it after checkPath to reflect + // actual file system state. + const Common::FSNode savePath(savePathName); + + Common::FSList children; + if (!savePath.getChildren(children, Common::FSNode::kListFilesOnly)) { + return; + } + + // Build the savefile name cache. + for (Common::FSList::const_iterator file = children.begin(), end = children.end(); file != end; ++file) { + if (_saveFileCache.contains(file->getName())) { + warning("DefaultSaveFileManager::assureCached: Name clash when building cache, ignoring file '%s'", file->getName().c_str()); + } else { + _saveFileCache[file->getName()] = *file; + } + } + + // Only now store that we cached 'savePathName' to indicate we successfully + // cached the directory. + _cachedDirectory = savePathName; +} + +#if defined(USE_CLOUD) && defined(USE_LIBCURL) + +Common::HashMap<Common::String, uint32> DefaultSaveFileManager::loadTimestamps() { + Common::HashMap<Common::String, uint32> timestamps; + + //refresh the files list + Common::Array<Common::String> files; + g_system->getSavefileManager()->updateSavefilesList(files); + + //start with listing all the files in saves/ directory and setting invalid timestamp to them + Common::StringArray localFiles = g_system->getSavefileManager()->listSavefiles("*"); + for (uint32 i = 0; i < localFiles.size(); ++i) + timestamps[localFiles[i]] = INVALID_TIMESTAMP; + + //now actually load timestamps from file + Common::InSaveFile *file = g_system->getSavefileManager()->openRawFile(TIMESTAMPS_FILENAME); + if (!file) { + warning("DefaultSaveFileManager: failed to open '%s' file to load timestamps", TIMESTAMPS_FILENAME); + return timestamps; + } + + while (!file->eos()) { + //read filename into buffer (reading until the first ' ') + Common::String buffer; + while (!file->eos()) { + byte b = file->readByte(); + if (b == ' ') break; + buffer += (char)b; + } + + //read timestamp info buffer (reading until ' ' or some line ending char) + Common::String filename = buffer; + while (true) { + bool lineEnded = false; + buffer = ""; + while (!file->eos()) { + byte b = file->readByte(); + if (b == ' ' || b == '\n' || b == '\r') { + lineEnded = (b == '\n'); + break; + } + buffer += (char)b; + } + + if (buffer == "" && file->eos()) break; + if (!lineEnded) filename += " " + buffer; + else break; + } + + //parse timestamp + uint32 timestamp = buffer.asUint64(); + if (buffer == "" || timestamp == 0) break; + if (timestamps.contains(filename)) + timestamps[filename] = timestamp; + } + + delete file; + return timestamps; +} + +void DefaultSaveFileManager::saveTimestamps(Common::HashMap<Common::String, uint32> ×tamps) { + Common::DumpFile f; + Common::String filename = concatWithSavesPath(TIMESTAMPS_FILENAME); + if (!f.open(filename, true)) { + warning("DefaultSaveFileManager: failed to open '%s' file to save timestamps", filename.c_str()); + return; + } + + for (Common::HashMap<Common::String, uint32>::iterator i = timestamps.begin(); i != timestamps.end(); ++i) { + Common::String data = i->_key + Common::String::format(" %u\n", i->_value); + if (f.write(data.c_str(), data.size()) != data.size()) { + warning("DefaultSaveFileManager: failed to write timestamps data into '%s'", filename.c_str()); + return; + } + } + + f.flush(); + f.finalize(); + f.close(); +} + +Common::String DefaultSaveFileManager::concatWithSavesPath(Common::String name) { + DefaultSaveFileManager *manager = dynamic_cast<DefaultSaveFileManager *>(g_system->getSavefileManager()); + Common::String path = (manager ? manager->getSavePath() : ConfMan.get("savepath")); + if (path.size() > 0 && (path.lastChar() == '/' || path.lastChar() == '\\')) + return path + name; + + //simple heuristic to determine which path separator to use + int backslashes = 0; + for (uint32 i = 0; i < path.size(); ++i) + if (path[i] == '/') --backslashes; + else if (path[i] == '\\') ++backslashes; + + if (backslashes > 0) return path + '\\' + name; + return path + '/' + name; +} + +#endif // ifdef USE_LIBCURL + #endif // !defined(DISABLE_DEFAULT_SAVEFILEMANAGER) diff --git a/backends/saves/default/default-saves.h b/backends/saves/default/default-saves.h index 81f45f96b8..f2453810bf 100644 --- a/backends/saves/default/default-saves.h +++ b/backends/saves/default/default-saves.h @@ -27,6 +27,8 @@ #include "common/savefile.h" #include "common/str.h" #include "common/fs.h" +#include "common/hash-str.h" +#include <limits.h> /** * Provides a default savefile manager implementation for common platforms. @@ -36,11 +38,24 @@ public: DefaultSaveFileManager(); DefaultSaveFileManager(const Common::String &defaultSavepath); + virtual void updateSavefilesList(Common::StringArray &lockedFiles); virtual Common::StringArray listSavefiles(const Common::String &pattern); + virtual Common::InSaveFile *openRawFile(const Common::String &filename); virtual Common::InSaveFile *openForLoading(const Common::String &filename); virtual Common::OutSaveFile *openForSaving(const Common::String &filename, bool compress = true); virtual bool removeSavefile(const Common::String &filename); +#ifdef USE_LIBCURL + + static const uint32 INVALID_TIMESTAMP = UINT_MAX; + static const char *TIMESTAMPS_FILENAME; + + static Common::HashMap<Common::String, uint32> loadTimestamps(); + static void saveTimestamps(Common::HashMap<Common::String, uint32> ×tamps); + static Common::String concatWithSavesPath(Common::String name); + +#endif + protected: /** * Get the path to the savegame directory. @@ -54,6 +69,36 @@ protected: * Sets the internal error and error message accordingly. */ virtual void checkPath(const Common::FSNode &dir); + + /** + * Assure that the given save path is cached. + * + * @param savePathName String representation of save path to cache. + */ + void assureCached(const Common::String &savePathName); + + typedef Common::HashMap<Common::String, Common::FSNode, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> SaveFileCache; + + /** + * Cache of all the save files in the currently cached directory. + * + * Modify with caution because we only re-cache when the save path changed! + * This needs to be updated inside at least openForSaving and + * removeSavefile. + */ + SaveFileCache _saveFileCache; + + /** + * List of "locked" files. These cannot be used for saving/loading + * because CloudManager is downloading those. + */ + Common::StringArray _lockedFiles; + +private: + /** + * The currently cached directory. + */ + Common::String _cachedDirectory; }; #endif diff --git a/backends/saves/savefile.cpp b/backends/saves/savefile.cpp index b04c53d832..21f53b8de2 100644 --- a/backends/saves/savefile.cpp +++ b/backends/saves/savefile.cpp @@ -23,9 +23,37 @@ #include "common/util.h" #include "common/savefile.h" #include "common/str.h" +#if defined(USE_CLOUD) && defined(USE_LIBCURL) +#include "backends/cloud/cloudmanager.h" +#endif namespace Common { +OutSaveFile::OutSaveFile(WriteStream *w): _wrapped(w) {} + +OutSaveFile::~OutSaveFile() {} + +bool OutSaveFile::err() const { return _wrapped->err(); } + +void OutSaveFile::clearErr() { _wrapped->clearErr(); } + +void OutSaveFile::finalize() { + _wrapped->finalize(); +#if defined(USE_CLOUD) && defined(USE_LIBCURL) + CloudMan.syncSaves(); +#endif +} + +bool OutSaveFile::flush() { return _wrapped->flush(); } + +uint32 OutSaveFile::write(const void *dataPtr, uint32 dataSize) { + return _wrapped->write(dataPtr, dataSize); +} + +int32 OutSaveFile::pos() const { + return _wrapped->pos(); +} + bool SaveFileManager::copySavefile(const String &oldFilename, const String &newFilename) { InSaveFile *inFile = 0; OutSaveFile *outFile = 0; diff --git a/backends/taskbar/macosx/dockplugin/dockplugin.m b/backends/taskbar/macosx/dockplugin/dockplugin.m new file mode 100644 index 0000000000..95f961a39d --- /dev/null +++ b/backends/taskbar/macosx/dockplugin/dockplugin.m @@ -0,0 +1,125 @@ +/* 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 <Cocoa/Cocoa.h> + +@interface ScummVMDockTilePlugIn : NSObject <NSDockTilePlugIn> { + NSMenu *recentGamesMenu; +} +@end + +@interface StartGameMenuItem : NSMenuItem { + NSString *game; +} +- (IBAction) startGame; +- (NSMenuItem*)initWithGame:(NSString *)gameId description:(NSString*)desc icon:(NSString*)iconFile; +@end + +@implementation ScummVMDockTilePlugIn + +- (id)init { + self = [super init]; + if (self) { + recentGamesMenu = nil; + } + return self; +} + +- (void)dealloc { + [recentGamesMenu release]; + [super dealloc]; +} + + +- (void)setDockTile:(NSDockTile *)dockTile { +} + +- (NSMenu*)dockMenu { + // Get the list or recent games + CFPreferencesAppSynchronize(CFSTR("org.scummvm.scummvm")); + NSArray *array = CFPreferencesCopyAppValue(CFSTR("recentGames"), CFSTR("org.scummvm.scummvm")); + if (array == nil) + return nil; + + // Create the menu + if (recentGamesMenu == nil) + recentGamesMenu = [[NSMenu alloc] init]; + else + [recentGamesMenu removeAllItems]; + + NSEnumerator *enumerator = [array objectEnumerator]; + NSDictionary *recentGame; + while (recentGame = [enumerator nextObject]) { + NSString *gameId = [recentGame valueForKey:@"game"]; + NSString *desc = [recentGame valueForKey:@"description"]; + NSString *iconFile = [recentGame valueForKey:@"icon"]; + + StartGameMenuItem *menuItem = [[StartGameMenuItem alloc] initWithGame:gameId description:desc icon:iconFile]; + [recentGamesMenu addItem:menuItem]; + [menuItem release]; + } + + return recentGamesMenu; +} + +@end + +@implementation StartGameMenuItem + +- (NSMenuItem*)initWithGame:(NSString *)gameId description:(NSString*)desc icon:(NSString*)iconFile { + self = [super initWithTitle:(desc == nil ? gameId : desc) action:@selector(startGame) keyEquivalent:@""]; + [self setTarget:self]; + + if (iconFile != nil) { + NSImage *image = [[NSImage alloc] initWithContentsOfFile:iconFile]; + [self setImage:image]; + [image release]; + } + + game = gameId; + [game retain]; + + return self; +} + +- (void)dealloc { + [game release]; + [super dealloc]; +} + +- (IBAction) startGame { + NSLog(@"Starting Game %@...", game); + + NSString *scummVMPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"org.scummvm.scummvm"]; + if (scummVMPath == nil) { + NSLog(@"Cannot find ScummVM.app!"); + return; + } + // Start ScummVM.app with the game ID as argument + NSURL *url = [NSURL fileURLWithPath:scummVMPath]; + NSMutableDictionary *args = [[NSMutableDictionary alloc] init]; + [args setObject:[NSArray arrayWithObject:game] forKey:NSWorkspaceLaunchConfigurationArguments]; + [[NSWorkspace sharedWorkspace] launchApplicationAtURL:url options:NSWorkspaceLaunchDefault configuration:args error:nil]; + [args release]; +} + +@end diff --git a/backends/taskbar/macosx/macosx-taskbar.h b/backends/taskbar/macosx/macosx-taskbar.h index 5d5b9d02cd..145cbd4f3a 100644 --- a/backends/taskbar/macosx/macosx-taskbar.h +++ b/backends/taskbar/macosx/macosx-taskbar.h @@ -37,18 +37,19 @@ public: virtual void setProgressValue(int completed, int total); virtual void setProgressState(TaskbarProgressState state); virtual void setCount(int count); + virtual void addRecent(const Common::String &name, const Common::String &description); virtual void notifyError(); virtual void clearError(); - + private: Common::String getIconPath(const Common::String&); void initApplicationIconView(); void clearApplicationIconView(); - + void initOverlayIconView(); void clearOverlayIconView(); - + double _progress; }; diff --git a/backends/taskbar/macosx/macosx-taskbar.mm b/backends/taskbar/macosx/macosx-taskbar.mm index ae087dfb85..692fa84429 100644 --- a/backends/taskbar/macosx/macosx-taskbar.mm +++ b/backends/taskbar/macosx/macosx-taskbar.mm @@ -29,9 +29,6 @@ // NSDockTile was introduced with Mac OS X 10.5. // Try provide backward compatibility by avoiding NSDockTile symbols. -// TODO: Implement recent list, maybe as a custom menu on dock tile when app is not running -// See Dock Tile plug-in at https://developer.apple.com/library/mac/documentation/Carbon/Conceptual/customizing_docktile/CreatingaDockTilePlug-in/CreatingaDockTilePlug-in.html - #include "backends/taskbar/macosx/macosx-taskbar.h" #include "common/config-manager.h" #include "common/file.h" @@ -39,6 +36,9 @@ #include <AppKit/NSApplication.h> #include <AppKit/NSImage.h> #include <Foundation/NSString.h> +#include <Foundation/NSDictionary.h> +#include <Foundation/NSArray.h> +#include <Foundation/NSUserDefaults.h> #include <AppKit/NSImageView.h> #include <AppKit/NSColor.h> #include <AppKit/NSBezierPath.h> @@ -112,15 +112,15 @@ void MacOSXTaskbarManager::setOverlayIcon(const Common::String &name, const Comm [_dockTile performSelector:@selector(display)]; return; } - + Common::String path = getIconPath(name); if (path.empty()) return; - + initOverlayIconView(); CFStringRef imageFile = CFStringCreateWithCString(0, path.c_str(), kCFStringEncodingASCII); - NSImage* image = [[NSImage alloc] initWithContentsOfFile:(NSString *)imageFile]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:(NSString *)imageFile]; [_overlayIconView setImage:image]; [image release]; CFRelease(imageFile); @@ -136,7 +136,7 @@ void MacOSXTaskbarManager::setProgressValue(int completed, int total) { _progress = (double)completed / (double)total; else if (_progress < 0) _progress = 0.0; - + NSImage *mainIcon = [[NSApp applicationIconImage] copy]; double barSize = [mainIcon size].width; double progressSize = barSize * _progress; @@ -150,7 +150,7 @@ void MacOSXTaskbarManager::setProgressValue(int completed, int total) { initApplicationIconView(); [_applicationIconView setImage:mainIcon]; [mainIcon release]; - + [_dockTile performSelector:@selector(display)]; } @@ -207,32 +207,91 @@ Common::String MacOSXTaskbarManager::getIconPath(const Common::String& target) { // fallback to the extra path // // Icons can be either in a subfolder named "icons" or directly in the path - + Common::String iconsPath = ConfMan.get("iconspath"); Common::String extraPath = ConfMan.get("extrapath"); - + #define TRY_ICON_PATH(path) { \ Common::FSNode node((path)); \ if (node.exists()) \ return (path); \ } - + if (!iconsPath.empty()) { TRY_ICON_PATH(iconsPath + "/" + target + ".png"); TRY_ICON_PATH(iconsPath + "/" + ConfMan.get("gameid") + ".png"); TRY_ICON_PATH(iconsPath + "/icons/" + target + ".png"); TRY_ICON_PATH(iconsPath + "/icons/" + ConfMan.get("gameid") + ".png"); } - + if (!extraPath.empty()) { TRY_ICON_PATH(extraPath + "/" + target + ".png"); TRY_ICON_PATH(extraPath + "/" + ConfMan.get("gameid") + ".png"); TRY_ICON_PATH(extraPath + "/icons/" + target + ".png"); TRY_ICON_PATH(extraPath + "/icons/" + ConfMan.get("gameid") + ".png"); } - + return ""; } +void MacOSXTaskbarManager::addRecent(const Common::String &name, const Common::String &description) { + //warning("[MacOSXTaskbarManager::addRecent] Adding recent list entry: %s (%s)", name.c_str(), description.c_str()); + + if (_dockTile == nil) + return; + + // Store the game, description and icon in user preferences. + // The NSDockTilePlugin will retrieve them there to list them in the dock tile menu. + + CFStringRef gameName = CFStringCreateWithCString(0, name.c_str(), kCFStringEncodingASCII); + CFStringRef desc = CFStringCreateWithCString(0, description.c_str(), kCFStringEncodingASCII); + + // First build the dictionary for this game. + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + [dict setObject:(NSString *)gameName forKey:@"game"]; + [dict setObject:(NSString *)desc forKey:@"description"]; + + // Icon + Common::String iconPath = getIconPath(name); + if (!iconPath.empty()) { + CFStringRef icon = CFStringCreateWithCString(0, iconPath.c_str(), kCFStringEncodingASCII); + [dict setObject:(NSString *)icon forKey:@"icon"]; + CFRelease(icon); + } + + // Retrieve the current list of recent items and update it. + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSArray *oldArray = [defaults arrayForKey:@"recentGames"]; + if (oldArray == nil) { + [defaults setObject:[NSArray arrayWithObject:dict] forKey:@"recentGames"]; + } else { + NSMutableArray *newArray = [[NSMutableArray alloc] initWithArray:oldArray]; + // Insert the new game at the start + [newArray insertObject:dict atIndex:0]; + // If the game was already present in the array, remove it + for (unsigned int i = 1 ; i < [newArray count] ; ++i) { + NSDictionary *oldDict = [newArray objectAtIndex:i]; + if (oldDict == nil) + continue; + NSString *oldGame = [oldDict valueForKey:@"game"]; + if (oldGame != nil && [oldGame isEqualToString:(NSString*)gameName]) { + [newArray removeObjectAtIndex:i]; + break; + } + } + // And make sure we limit the size of the array to 5 games + if ([newArray count] > 5) + [newArray removeLastObject]; + [defaults setObject:newArray forKey:@"recentGames"]; + [newArray release]; + } + + // Finally release the dictionary + [dict release]; + CFRelease(gameName); + CFRelease(desc); +} + + #endif diff --git a/backends/taskbar/win32/win32-taskbar.cpp b/backends/taskbar/win32/win32-taskbar.cpp index f3339fb917..b2810e55b4 100644 --- a/backends/taskbar/win32/win32-taskbar.cpp +++ b/backends/taskbar/win32/win32-taskbar.cpp @@ -85,7 +85,7 @@ const PROPERTYKEY PKEY_Title = { /* fmtid = */ { 0xF29F85E0, 0x4FF9, 0x1068, { 0 Win32TaskbarManager::Win32TaskbarManager(SdlWindow *window) : _window(window), _taskbar(NULL), _count(0), _icon(NULL) { // Do nothing if not running on Windows 7 or later - if (!isWin7OrLater()) + if (!confirmWindowsVersion(10, 0) && !confirmWindowsVersion(6, 1)) return; CoInitialize(NULL); @@ -400,14 +400,14 @@ BOOL VerifyVersionInfoFunc(LPOSVERSIONINFOEXA lpVersionInformation, DWORD dwType return verifyVersionInfo(lpVersionInformation, dwTypeMask, dwlConditionMask); } -bool Win32TaskbarManager::isWin7OrLater() { +bool Win32TaskbarManager::confirmWindowsVersion(uint majorVersion, uint minorVersion) { OSVERSIONINFOEX versionInfo; DWORDLONG conditionMask = 0; ZeroMemory(&versionInfo, sizeof(OSVERSIONINFOEX)); versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); - versionInfo.dwMajorVersion = 6; - versionInfo.dwMinorVersion = 1; + versionInfo.dwMajorVersion = majorVersion; + versionInfo.dwMinorVersion = minorVersion; conditionMask = VerSetConditionMaskFunc(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL); conditionMask = VerSetConditionMaskFunc(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL); diff --git a/backends/taskbar/win32/win32-taskbar.h b/backends/taskbar/win32/win32-taskbar.h index a6d1b49213..a5c024b21b 100644 --- a/backends/taskbar/win32/win32-taskbar.h +++ b/backends/taskbar/win32/win32-taskbar.h @@ -64,7 +64,7 @@ private: Common::String getIconPath(Common::String target); // Helper functions - bool isWin7OrLater(); + bool confirmWindowsVersion(uint majorVersion, uint minorVersion); LPWSTR ansiToUnicode(const char *s); HWND getHwnd(); }; diff --git a/backends/updates/macosx/macosx-updates.h b/backends/updates/macosx/macosx-updates.h index fd2d1f46f5..6fb9af7712 100644 --- a/backends/updates/macosx/macosx-updates.h +++ b/backends/updates/macosx/macosx-updates.h @@ -39,8 +39,10 @@ public: virtual void setAutomaticallyChecksForUpdates(UpdateState state); virtual UpdateState getAutomaticallyChecksForUpdates(); - virtual void setUpdateCheckInterval(UpdateInterval interval); - virtual UpdateInterval getUpdateCheckInterval(); + virtual void setUpdateCheckInterval(int interval); + virtual int getUpdateCheckInterval(); + + virtual bool getLastUpdateCheckTimeAndDate(TimeDate &t); }; #endif diff --git a/backends/updates/macosx/macosx-updates.mm b/backends/updates/macosx/macosx-updates.mm index a94f1c21fd..64dcbf62ce 100644 --- a/backends/updates/macosx/macosx-updates.mm +++ b/backends/updates/macosx/macosx-updates.mm @@ -23,14 +23,18 @@ // Disable symbol overrides so that we can use system headers. #define FORBIDDEN_SYMBOL_ALLOW_ALL +#include "common/system.h" #include "backends/updates/macosx/macosx-updates.h" #ifdef USE_SPARKLE #include "common/translation.h" +#include "common/config-manager.h" #include <Cocoa/Cocoa.h> #include <Sparkle/Sparkle.h> +#include <AvailabilityMacros.h> + SUUpdater *sparkleUpdater; /** @@ -45,14 +49,22 @@ SUUpdater *sparkleUpdater; * */ MacOSXUpdateManager::MacOSXUpdateManager() { + NSBundle* mainBundle = [NSBundle mainBundle]; + + NSString *version = [mainBundle objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]; + if (!version || [version isEqualToString:@""]) { + warning("Running not in bundle, skipping Sparkle initialization"); + + sparkleUpdater = nullptr; + return; + } + NSMenuItem *menuItem = [[NSApp mainMenu] itemAtIndex:0]; NSMenu *applicationMenu = [menuItem submenu]; // Init Sparkle sparkleUpdater = [SUUpdater sharedUpdater]; - NSBundle* mainBundle = [NSBundle mainBundle]; - NSString* feedbackURL = [mainBundle objectForInfoDictionaryKey:@"SUFeedURL"]; // Set appcast URL @@ -74,11 +86,13 @@ MacOSXUpdateManager::MacOSXUpdateManager() { // Finally give up our references to the objects [menuItem release]; - // Enable automatic update checking once a day (alternatively use - // checkForUpdates() here to check for updates on every startup) - // TODO: Should be removed when an update settings gui is implemented - setAutomaticallyChecksForUpdates(kUpdateStateEnabled); - setUpdateCheckInterval(kUpdateIntervalOneDay); + if (!ConfMan.hasKey("updates_check") + || ConfMan.getInt("updates_check") == Common::UpdateManager::kUpdateIntervalNotSupported) { + setAutomaticallyChecksForUpdates(kUpdateStateDisabled); + } else { + setAutomaticallyChecksForUpdates(kUpdateStateEnabled); + setUpdateCheckInterval(normalizeInterval(ConfMan.getInt("updates_check"))); + } } MacOSXUpdateManager::~MacOSXUpdateManager() { @@ -86,31 +100,48 @@ MacOSXUpdateManager::~MacOSXUpdateManager() { } void MacOSXUpdateManager::checkForUpdates() { - [sparkleUpdater checkForUpdatesInBackground]; + if (sparkleUpdater == nullptr) + return; + + [sparkleUpdater checkForUpdates:nil]; } void MacOSXUpdateManager::setAutomaticallyChecksForUpdates(UpdateManager::UpdateState state) { if (state == kUpdateStateNotSupported) return; + if (sparkleUpdater == nullptr) + return; + [sparkleUpdater setAutomaticallyChecksForUpdates:(state == kUpdateStateEnabled ? YES : NO)]; } Common::UpdateManager::UpdateState MacOSXUpdateManager::getAutomaticallyChecksForUpdates() { + if (sparkleUpdater == nullptr) + return kUpdateStateDisabled; + if ([sparkleUpdater automaticallyChecksForUpdates]) return kUpdateStateEnabled; else return kUpdateStateDisabled; } -void MacOSXUpdateManager::setUpdateCheckInterval(UpdateInterval interval) { +void MacOSXUpdateManager::setUpdateCheckInterval(int interval) { + if (sparkleUpdater == nullptr) + return; + if (interval == kUpdateIntervalNotSupported) return; + interval = normalizeInterval(interval); + [sparkleUpdater setUpdateCheckInterval:(NSTimeInterval)interval]; } -Common::UpdateManager::UpdateInterval MacOSXUpdateManager::getUpdateCheckInterval() { +int MacOSXUpdateManager::getUpdateCheckInterval() { + if (sparkleUpdater == nullptr) + return kUpdateIntervalOneDay; + // This is kind of a hack but necessary, as the value stored by Sparkle // might have been changed outside of ScummVM (in which case we return the // default interval of one day) @@ -128,4 +159,30 @@ Common::UpdateManager::UpdateInterval MacOSXUpdateManager::getUpdateCheckInterva } } +bool MacOSXUpdateManager::getLastUpdateCheckTimeAndDate(TimeDate &t) { + if (sparkleUpdater == nullptr) + return false; + + NSDate *date = [sparkleUpdater lastUpdateCheckDate]; +#ifdef MAC_OS_X_VERSION_10_10 + NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *components = [gregorian components:(NSCalendarUnitDay | NSCalendarUnitWeekday) fromDate:date]; +#else + NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + NSDateComponents *components = [gregorian components:(NSDayCalendarUnit | NSWeekdayCalendarUnit) fromDate:date]; +#endif + + t.tm_wday = [components weekday]; + t.tm_year = [components year]; + t.tm_mon = [components month]; + t.tm_mday = [components day]; + t.tm_hour = [components hour]; + t.tm_min = [components minute]; + t.tm_sec = [components second]; + + [gregorian release]; + + return true; +} + #endif diff --git a/backends/updates/win32/win32-updates.cpp b/backends/updates/win32/win32-updates.cpp new file mode 100644 index 0000000000..aebeab531b --- /dev/null +++ b/backends/updates/win32/win32-updates.cpp @@ -0,0 +1,132 @@ +/* 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. + * + */ + +// Disable symbol overrides so that we can use system headers. +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "common/system.h" +#include "backends/updates/win32/win32-updates.h" + +#ifdef USE_SPARKLE +#include "common/translation.h" +#include "common/config-manager.h" + +#include <time.h> +#include <winsparkle.h> + +/** + * Sparkle is a software update framework for Mac OS X which uses appcasts for + * release information. Appcasts are RSS-like XML feeds which contain information + * about the most current version at the time. If a new version is available, the + * user is presented the release-notes/changes/fixes and is asked if he wants to + * update, and if yes the Sparkle framework downloads a signed update package + * from the server and automatically installs and restarts the software. + * More detailed information is available at the following address: + * http://sparkle.andymatuschak.org/ + * + * WinSparkle is a heavily (to the point of being its almost-port) inspired by the + * Sparkle framework originally by Andy Matuschak that became the de facto standard + * for software updates on OS X. + * More detailed information is available at the following address: + * https://winsparkle.org/ + * + */ +Win32UpdateManager::Win32UpdateManager() { + const char *appcastUrl = "https://www.scummvm.org/appcasts/macosx/release.xml"; + + win_sparkle_set_appcast_url(appcastUrl); + win_sparkle_init(); + + if (!ConfMan.hasKey("updates_check") + || ConfMan.getInt("updates_check") == Common::UpdateManager::kUpdateIntervalNotSupported) { + setAutomaticallyChecksForUpdates(kUpdateStateDisabled); + } else { + setAutomaticallyChecksForUpdates(kUpdateStateEnabled); + setUpdateCheckInterval(normalizeInterval(ConfMan.getInt("updates_check"))); + } +} + +Win32UpdateManager::~Win32UpdateManager() { + win_sparkle_cleanup(); +} + +void Win32UpdateManager::checkForUpdates() { + win_sparkle_check_update_with_ui(); +} + +void Win32UpdateManager::setAutomaticallyChecksForUpdates(UpdateManager::UpdateState state) { + if (state == kUpdateStateNotSupported) + return; + + win_sparkle_set_automatic_check_for_updates(state == kUpdateStateEnabled ? 1 : 0); +} + +Common::UpdateManager::UpdateState Win32UpdateManager::getAutomaticallyChecksForUpdates() { + if (win_sparkle_get_automatic_check_for_updates() == 1) + return kUpdateStateEnabled; + else + return kUpdateStateDisabled; +} + +void Win32UpdateManager::setUpdateCheckInterval(int interval) { + if (interval == kUpdateIntervalNotSupported) + return; + + interval = normalizeInterval(interval); + + win_sparkle_set_update_check_interval(interval); +} + +int Win32UpdateManager::getUpdateCheckInterval() { + // This is kind of a hack but necessary, as the value stored by Sparkle + // might have been changed outside of ScummVM (in which case we return the + // default interval of one day) + + int updateInterval = win_sparkle_get_update_check_interval(); + switch (updateInterval) { + case kUpdateIntervalOneDay: + case kUpdateIntervalOneWeek: + case kUpdateIntervalOneMonth: + return updateInterval; + + default: + // Return the default value (one day) + return kUpdateIntervalOneDay; + } +} + +bool Win32UpdateManager::getLastUpdateCheckTimeAndDate(TimeDate &t) { + time_t updateTime = win_sparkle_get_last_check_time(); + tm *ut = localtime(&updateTime); + + t.tm_wday = ut->tm_wday; + t.tm_year = ut->tm_year; + t.tm_mon = ut->tm_mon; + t.tm_mday = ut->tm_mday; + t.tm_hour = ut->tm_hour; + t.tm_min = ut->tm_min; + t.tm_sec = ut->tm_sec; + + return true; +} + +#endif diff --git a/backends/updates/win32/win32-updates.h b/backends/updates/win32/win32-updates.h new file mode 100644 index 0000000000..71ed9ef685 --- /dev/null +++ b/backends/updates/win32/win32-updates.h @@ -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 BACKENDS_UPDATES_WIN32_H +#define BACKENDS_UPDATES_WIN32_H + +#include "common/scummsys.h" + +#if defined(WIN32) && defined(USE_SPARKLE) + +#include "common/updates.h" + +class Win32UpdateManager : public Common::UpdateManager { +public: + Win32UpdateManager(); + virtual ~Win32UpdateManager(); + + virtual void checkForUpdates(); + + virtual void setAutomaticallyChecksForUpdates(UpdateState state); + virtual UpdateState getAutomaticallyChecksForUpdates(); + + virtual void setUpdateCheckInterval(int interval); + virtual int getUpdateCheckInterval(); + + virtual bool getLastUpdateCheckTimeAndDate(TimeDate &t); +}; + +#endif + +#endif // BACKENDS_UPDATES_WIN32_H |