Skip to content

Commit

Permalink
Add support for 'flashpoint://' protocol
Browse files Browse the repository at this point in the history
CLIFp can register itself to respond to use of the above URL scheme in
order to start titles. Additionally, the new 'share' command will
generate links based on titles that use the protocol and adds them to
the clipboard so that users can
more easily share them.

Also implements an https redirect page to be hosted on GitHub that
redirects to a FP protocol link. This is useful so that users can still
share links on messageing platforms that don't allow for custom scheme
hyperlinks directly.
  • Loading branch information
oblivioncth committed Oct 25, 2023
1 parent 6964b13 commit 705072b
Show file tree
Hide file tree
Showing 21 changed files with 473 additions and 12 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/master-pull-request-merge-reaction.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ jobs:
uses: ./.github/workflows/build-clifp-linux.yml
secrets:
qt_ffynnon_cred: ${{ secrets.OBYBOT_FFYNNON_CREDS }}

update-redirect:
name: Update redirect page
if: github.event.pull_request.merged == true
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.page-deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup pages
uses: actions/configure-pages@v3
- name: Upload pages artifact
uses: actions/upload-pages-artifact@v1
with:
path: ${{ github.workspace }}/redirector
- name: Deploy pages artifact
id: page-deployment
uses: actions/deploy-pages@v1

create-release:
name: Create GitHub release
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ endif()

include(OB/FetchQx)
ob_fetch_qx(
REF "v0.5.3"
REF "06f396aa7add097041d97a54e98a7269dc57d106"
COMPONENTS
${CLIFP_QX_COMPONENTS}
)
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ If the application needs to use files from a Data Pack that pack will need to be

The applications and arguments that are used for each game/animation can be found within the Flashpoint database ([FP Install Dir]\Data\flashpoint.sqlite)

### Flashpoint Protocol
CLIFp supports the "flashpoint" protocol, which means it can launch titles through URL with a custom scheme, followed by a title's UUID, like this:

flashpoint://37e5c215-9c39-4a3d-9912-b4343a17027e

This makes it fast and easy to share games that you think your friends should try out.

To register CLIFp as the handler for these URLs, simply run:

CLIFp share -c

To easily create a share link if you don't already know the UUID of a game, you can use the **share** command with the **-t** switch followed by a title:

CLIFp share -t "Simple, Tasty Buttons"

This will create a share link for that title which will be displayed via a message box and automatically copied to the system clipboard.

If for whatever reason the service through which you wish to share a link does not support links with custom schemes, you can use the **-u** switch to generate a standard "https" link that utilizes a GitHub-hosted redirect page, enabling share links to be provided everywhere.

[!IMPORTANT]
You will want to disable the "Register As Protocol Handler" option in the default launcher or else it will replace CLIFp as the "flashpoint" protocol handler every time it's started (**NOTE: This option is not currently available in a released version of Flashpoint**).

## All Commands/Options
The recommended way to use all switches is to use their short form when the value for the switch has no spaces:

Expand Down Expand Up @@ -190,6 +212,29 @@ When using the **--exe** and **--param** switches all quotes that are part of th

See http://www.robvanderwoude.com/escapechars.php for more information.

--------------------------------------------------------------------------------

**share** - Generates a URL for starting a Flashpoint title that can be shared to other users.

Options:
- **-i | --id:** UUID of title to make a share link for
- **-t | --title:** Title to make a share link for
- **-T | --title-strict:** Same as **-t**, but only exact matches are considered
- **-s | --subtitle:** Name of additional-app under the title to make a share link for. Must be used with **-t**/**-T**
- **-S | --subtitle-strict:** Same as **-s**, but only exact matches are considered
- **-u | --universal:** Creates a standard HTTPS link that utilizes a redirect page. May be easier to share on some platforms.
- **-c | --configure:** Registers CLIFp as the default handler for "flashpoint" protocol links.
- **-C | --unconfigure:** Removes CLIFp as the default handler for "flashpoint" protocol links.
- **-h | --help | -?:** Prints command specific usage information

Requires:
**-i**, **-t** or **-c**

Notes:

- By default, the standard Flashpoint launcher is registered to handle share links; therefore, its "Register As Protocol Handler" option should likely be disabled if you intend to use CLIFp instead.
- See the **play** command notes for information regarding the **t**/**T** and **s**/**S** switches.

--------------------------------------------------------------------------------

**show** - Display a message or extra folder
Expand All @@ -203,7 +248,7 @@ Requires:
**-m** or **-e**

### Remarks
With any use of the **--title** option for the commands that support it the title must be entered verbatim as it appears within Flashpoint, as close matches are not checked (due to technical limitations). If two entries happen to share the title specified, a dialog window with more information will be displayed so that the intended title can be selected.
With any use of the **--title**/**--subtitle** options for the commands that support them, the provided title should match as closely as possible to how it appears within Flashpoint, as checks for close matches are limited due to technical restrictions. If more than one entry is found, a dialog window with more information will be displayed so that the intended title can be selected.

## Other Features
CLIFp displays a system tray icon so that one can be sure it is still running. This icon also will display basic status messages when clicked on and features a context menu with an option to exit at any time.
Expand Down
2 changes: 2 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ set(CLIFP_SOURCE
command/c-prepare.cpp
command/c-run.h
command/c-run.cpp
command/c-share.h
command/c-share.cpp
command/c-show.cpp
command/c-show.h
command/title-command.h
Expand Down
2 changes: 1 addition & 1 deletion app/src/command/c-link.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// Project Includes
#include "command/title-command.h"

class QX_ERROR_TYPE(CLinkError, "CLinkError", 1212)
class QX_ERROR_TYPE(CLinkError, "CLinkError", 1213)
{
friend class CLink;
//-Class Enums-------------------------------------------------------------
Expand Down
41 changes: 39 additions & 2 deletions app/src/command/c-play.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@
#include "../task/t-message.h"
#include "../task/t-extra.h"

//===============================================================================================================
// CPlayError
//===============================================================================================================

//-Constructor-------------------------------------------------------------
//Private:
CPlayError::CPlayError(Type t, const QString& s) :
mType(t),
mSpecific(s)
{}

//-Instance Functions-------------------------------------------------------------
//Public:
bool CPlayError::isValid() const { return mType != NoError; }
QString CPlayError::specific() const { return mSpecific; }
CPlayError::Type CPlayError::type() const { return mType; }

//Private:
Qx::Severity CPlayError::deriveSeverity() const { return Qx::Critical; }
quint32 CPlayError::deriveValue() const { return mType; }
QString CPlayError::derivePrimary() const { return ERR_STRINGS.value(mType); }
QString CPlayError::deriveSecondary() const { return mSpecific; }

//===============================================================================================================
// CPLAY
//===============================================================================================================
Expand Down Expand Up @@ -262,15 +285,29 @@ Qx::Error CPlay::enqueueGame(const Fp::Game& game, const Fp::GameData& gameData,
}

//Protected:
QList<const QCommandLineOption*> CPlay::options() { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); }
QString CPlay::name() { return NAME; }

Qx::Error CPlay::perform()
{
// Get ID of title to start
// Get ID of title to start, prioritizing URL
QUuid titleId;
if(Qx::Error ide = getTitleId(titleId); ide.isValid())
if(mParser.isSet(CL_OPTION_URL))
{
QRegularExpressionMatch urlMatch = URL_REGEX.match(mParser.value(CL_OPTION_URL));
if(!urlMatch.hasMatch())
{
CPlayError err(CPlayError::InvalidUrl);
mCore.postError(NAME, err);
return err;
}

titleId = QUuid(urlMatch.captured(u"id"_s));
}
else if(Qx::Error ide = getTitleId(titleId); ide.isValid())
return ide;


Qx::Error errorStatus;

// Enqueue required tasks
Expand Down
56 changes: 56 additions & 0 deletions app/src/command/c-play.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,70 @@
// Project Includes
#include "command/title-command.h"

class QX_ERROR_TYPE(CPlayError, "CPlayError", 1212)
{
friend class CPlay;
//-Class Enums-------------------------------------------------------------
public:
enum Type
{
NoError = 0,
InvalidUrl = 1,
};

//-Class Variables-------------------------------------------------------------
private:
static inline const QHash<Type, QString> ERR_STRINGS{
{NoError, u""_s},
{InvalidUrl, u"The provided 'flashpoint://' scheme URL is invalid."_s}
};

//-Instance Variables-------------------------------------------------------------
private:
Type mType;
QString mSpecific;

//-Constructor-------------------------------------------------------------
private:
CPlayError(Type t = NoError, const QString& s = {});

//-Instance Functions-------------------------------------------------------------
public:
bool isValid() const;
Type type() const;
QString specific() const;

private:
Qx::Severity deriveSeverity() const override;
quint32 deriveValue() const override;
QString derivePrimary() const override;
QString deriveSecondary() const override;
};

class CPlay : public TitleCommand
{
//-Class Variables------------------------------------------------------------------------------------------------------
private:
// Status
static inline const QString STATUS_PLAY = u"Playing"_s;

// General
static inline const QRegularExpression URL_REGEX = QRegularExpression(
u"flashpoint:\\/\\/(?<id>[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})\\/?$"_s
);

// Command line option strings
static inline const QString CL_OPT_URL_S_NAME = u"u"_s;
static inline const QString CL_OPT_URL_L_NAME = u"url"_s;
static inline const QString CL_OPT_URL_DESC = u""_s;

// Command line options
static inline const QCommandLineOption CL_OPTION_URL{{CL_OPT_URL_S_NAME, CL_OPT_URL_L_NAME}, CL_OPT_URL_DESC, u"url"_s}; // Takes value
static inline const QList<const QCommandLineOption*> CL_OPTIONS_SPECIFIC{&CL_OPTION_URL};

// Logging - Messages
static inline const QString LOG_EVENT_ENQ_AUTO = u"Enqueuing automatic tasks..."_s;
static inline const QString LOG_EVENT_URL_ID = u"ID from URL: %1"_s;
static inline const QString LOG_EVENT_ID_MATCH_TITLE = u"ID matches main title: %1"_s;
static inline const QString LOG_EVENT_ID_MATCH_ADDAPP = u"ID matches additional app: %1 (Child of %2)"_s;
static inline const QString LOG_EVENT_QUEUE_CLEARED = u"Previous queue entries cleared due to auto task being a Message/Extra"_s;
Expand Down Expand Up @@ -45,6 +100,7 @@ class CPlay : public TitleCommand
Qx::Error enqueueGame(const Fp::Game& game, const Fp::GameData& gameData, Task::Stage taskStage);

protected:
QList<const QCommandLineOption*> options() override;
QString name() override;
Qx::Error perform() override;
};
Expand Down
113 changes: 113 additions & 0 deletions app/src/command/c-share.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Unit Include
#include "c-share.h"

// Qx Includes
#include <qx/core/qx-system.h>

// Project Includes
#include "task/t-message.h"
#include "utility.h"

//===============================================================================================================
// CShareError
//===============================================================================================================

//-Constructor-------------------------------------------------------------
//Private:
CShareError::CShareError(Type t, const QString& s) :
mType(t),
mSpecific(s)
{}

//-Instance Functions-------------------------------------------------------------
//Public:
bool CShareError::isValid() const { return mType != NoError; }
QString CShareError::specific() const { return mSpecific; }
CShareError::Type CShareError::type() const { return mType; }

//Private:
Qx::Severity CShareError::deriveSeverity() const { return Qx::Critical; }
quint32 CShareError::deriveValue() const { return mType; }
QString CShareError::derivePrimary() const { return ERR_STRINGS.value(mType); }
QString CShareError::deriveSecondary() const { return mSpecific; }

//===============================================================================================================
// CShare
//===============================================================================================================

//-Constructor-------------------------------------------------------------
//Public:
CShare::CShare(Core& coreRef) : TitleCommand(coreRef) {}

//-Instance Functions-------------------------------------------------------------
//Protected:
QList<const QCommandLineOption*> CShare::options() { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); }
QString CShare::name() { return NAME; }

Qx::Error CShare::perform()
{
// Prioritize scheme (un)registration
if(mParser.isSet(CL_OPTION_CONFIGURE))
{
mCore.logEvent(NAME, LOG_EVENT_REGISTRATION);

if(!Qx::setDefaultProtocolHandler(SCHEME, SCHEME_NAME, QDir::toNativeSeparators(CLIFP_PATH), {"play", "-u"}))
{
CShareError err(CShareError::RegistrationFailed);
mCore.postError(NAME, err);
return err;
}

// Enqueue success message task
TMessage* successMsg = new TMessage(&mCore);
successMsg->setStage(Task::Stage::Primary);
successMsg->setText(MSG_REGISTRATION_COMPLETE);
mCore.enqueueSingleTask(successMsg);

return CShareError();
}
else if(mParser.isSet(CL_OPTION_UNCONFIGURE))
{
mCore.logEvent(NAME, LOG_EVENT_UNREGISTRATION);

if(!Qx::removeDefaultProtocolHandler(SCHEME, SCHEME_NAME))
{
CShareError err(CShareError::UnregistrationFailed);
mCore.postError(NAME, err);
return err;
}

// Enqueue success message task
TMessage* successMsg = new TMessage(&mCore);
successMsg->setStage(Task::Stage::Primary);
successMsg->setText(MSG_UNREGISTRATION_COMPLETE);
mCore.enqueueSingleTask(successMsg);

return CShareError();
}

// Get ID of title to share
QUuid shareId;
if(Qx::Error ide = getTitleId(shareId); ide.isValid())
return ide;

mCore.setStatus(STATUS_SHARE, shareId.toString(QUuid::WithoutBraces));

// Generate URL
QString idStr = shareId.toString(QUuid::WithoutBraces);
QString shareUrl = mParser.isSet(CL_OPTION_UNIVERSAL) ? SCHEME_TEMPLATE_UNI.arg(idStr) : SCHEME_TEMPLATE_STD.arg(idStr);
mCore.logEvent(NAME, LOG_EVENT_URL.arg(shareUrl));

// Add URL to clipboard
mCore.requestClipboardUpdate(shareUrl);

// Enqueue message task
TMessage* urlMsg = new TMessage(&mCore);
urlMsg->setStage(Task::Stage::Primary);
urlMsg->setText(MSG_GENERATED_URL.arg(shareUrl));
urlMsg->setSelectable(true);
mCore.enqueueSingleTask(urlMsg);

// Return success
return Qx::Error();
}
Loading

0 comments on commit 705072b

Please sign in to comment.