From 31c6d2f9d502868970ccbe62d7cef36206cbc8b1 Mon Sep 17 00:00:00 2001 From: Stefan Sundin Date: Wed, 19 Apr 2023 05:48:26 -0700 Subject: [PATCH] Add protocol handler support This lets the user click a link in a web browser to very easily join a Quake 3 multiplayer game. As browser-based matchmaking websites become more popular, this makes it a lot more convenient and simple to play Quake 3 with others. The links have the following URI format: quake3://connect/example.com:27950. The format has been designed to be flexible to allow more types of links in the future and avoiding having to make a breaking change. At the moment, "connect" is the only supported command. --- code/qcommon/q_shared.h | 2 + code/sys/sys_local.h | 5 ++ code/sys/sys_main.c | 120 +++++++++++++++++++++++++++++++++++- code/sys/sys_osx.m | 43 +++++++++++++ make-macosx-app.sh | 16 +++++ misc/nsis/ioquake3.nsi.in | 36 +++++++++-- misc/setup/ioquake3.desktop | 3 +- 7 files changed, 217 insertions(+), 8 deletions(-) diff --git a/code/qcommon/q_shared.h b/code/qcommon/q_shared.h index 253e5e40..d7ede8af 100644 --- a/code/qcommon/q_shared.h +++ b/code/qcommon/q_shared.h @@ -40,6 +40,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define CINEMATICS_LOGO "foologo.roq" #define CINEMATICS_INTRO "intro.roq" // #define LEGACY_PROTOCOL // You probably don't need this for your standalone game +// #define PROTOCOL_HANDLER "foobar" #else #define PRODUCT_NAME "ioq3" #define BASEGAME "baseq3" @@ -56,6 +57,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #define CINEMATICS_LOGO "idlogo.RoQ" #define CINEMATICS_INTRO "intro.RoQ" #define LEGACY_PROTOCOL + #define PROTOCOL_HANDLER "quake3" #endif // Heartbeat for dpmaster protocol. You shouldn't change this unless you know what you're doing diff --git a/code/sys/sys_local.h b/code/sys/sys_local.h index cd7fbe7f..cc660a71 100644 --- a/code/sys/sys_local.h +++ b/code/sys/sys_local.h @@ -64,3 +64,8 @@ void Sys_AnsiColorPrint( const char *msg ); int Sys_PID( void ); qboolean Sys_PIDIsRunning( int pid ); + +#ifdef PROTOCOL_HANDLER +char *Sys_InitProtocolHandler( void ); +char *Sys_ParseProtocolUri( const char *uri ); +#endif diff --git a/code/sys/sys_main.c b/code/sys/sys_main.c index ae46c705..41bb54bd 100644 --- a/code/sys/sys_main.c +++ b/code/sys/sys_main.c @@ -642,6 +642,94 @@ void Sys_ParseArgs( int argc, char **argv ) } } +#ifdef PROTOCOL_HANDLER +/* +================= +Sys_InitProtocolHandler + +See sys_osx.m for macOS implementation. +================= +*/ +#ifndef __APPLE__ +char *Sys_InitProtocolHandler( void ) +{ + return NULL; +} +#endif + +/* +================= +Sys_ParseProtocolUri + +This parses a protocol URI, e.g. "quake3://connect/example.com:27950" +to a string that can be run in the console, or a null pointer if the +operation is invalid or unsupported. +At the moment only the "connect" command is supported. +================= +*/ +char *Sys_ParseProtocolUri( const char *uri ) +{ + // Both "quake3://" and "quake3:" can be used + if ( Q_strncmp( uri, PROTOCOL_HANDLER ":", strlen( PROTOCOL_HANDLER ":" ) ) ) + { + Com_Printf( "Sys_ParseProtocolUri: unsupported protocol.\n" ); + return NULL; + } + uri += strlen( PROTOCOL_HANDLER ":" ); + if ( !Q_strncmp( uri, "//", strlen( "//" ) ) ) + { + uri += strlen( "//" ); + } + Com_Printf( "Sys_ParseProtocolUri: %s\n", uri ); + + // At the moment, only "connect/hostname:port" is supported + if ( !Q_strncmp( uri, "connect/", strlen( "connect/" ) ) ) + { + int i, bufsize; + char *out; + + uri += strlen( "connect/" ); + if ( *uri == '\0' || *uri == '?' ) + { + Com_Printf( "Sys_ParseProtocolUri: missing argument.\n" ); + return NULL; + } + + // Check for any unsupported characters + // For safety reasons, the "hostname:port" part can only + // contain characters from: a-zA-Z0-9.:-[] + for ( i=0; uri[i] != '\0'; i++ ) + { + if ( uri[i] == '?' ) + { + // For forwards compatibility, any query string parameters are ignored (e.g. "?password=abcd") + // However, these are not passed on macOS, so it may be a bad idea to add them. + break; + } + + if ( isalpha( uri[i] ) == 0 && isdigit( uri[i] ) == 0 + && uri[i] != '.' && uri[i] != ':' && uri[i] != '-' + && uri[i] != '[' && uri[i] != ']' ) + { + Com_Printf( "Sys_ParseProtocolUri: hostname contains unsupported character.\n" ); + return NULL; + } + } + + bufsize = strlen( "+connect " ) + i + 1; + out = malloc( bufsize ); + strcpy( out, "+connect " ); + strncat( out, uri, i ); + return out; + } + else + { + Com_Printf( "Sys_ParseProtocolUri: unsupported command.\n" ); + return NULL; + } +} +#endif + #ifndef DEFAULT_BASEDIR # ifdef __APPLE__ # define DEFAULT_BASEDIR Sys_StripAppBundle(Sys_BinaryPath()) @@ -690,6 +778,9 @@ int main( int argc, char **argv ) { int i; char commandLine[ MAX_STRING_CHARS ] = { 0 }; +#ifdef PROTOCOL_HANDLER + char *protocolCommand = NULL; +#endif extern void Sys_LaunchAutoupdater(int argc, char **argv); Sys_LaunchAutoupdater(argc, argv); @@ -724,6 +815,10 @@ int main( int argc, char **argv ) Sys_PlatformInit( ); +#ifdef PROTOCOL_HANDLER + protocolCommand = Sys_InitProtocolHandler( ); +#endif + // Set the initial time base Sys_Milliseconds( ); @@ -740,7 +835,22 @@ int main( int argc, char **argv ) // Concatenate the command line for passing to Com_Init for( i = 1; i < argc; i++ ) { - const qboolean containsSpaces = strchr(argv[i], ' ') != NULL; + qboolean containsSpaces; + + // For security reasons we always detect --uri, even when PROTOCOL_HANDLER is undefined + // Any arguments after "--uri quake3://..." is ignored + if ( !strcmp( argv[i], "--uri" ) ) + { +#ifdef PROTOCOL_HANDLER + if ( argc > i+1 ) + { + protocolCommand = Sys_ParseProtocolUri( argv[i+1] ); + } +#endif + break; + } + + containsSpaces = strchr(argv[i], ' ') != NULL; if (containsSpaces) Q_strcat( commandLine, sizeof( commandLine ), "\"" ); @@ -752,6 +862,14 @@ int main( int argc, char **argv ) Q_strcat( commandLine, sizeof( commandLine ), " " ); } +#ifdef PROTOCOL_HANDLER + if ( protocolCommand != NULL ) + { + Q_strcat( commandLine, sizeof( commandLine ), protocolCommand ); + free( protocolCommand ); + } +#endif + CON_Init( ); Com_Init( commandLine ); NET_Init( ); diff --git a/code/sys/sys_osx.m b/code/sys/sys_osx.m index b0ecf6a5..d7415d32 100644 --- a/code/sys/sys_osx.m +++ b/code/sys/sys_osx.m @@ -34,6 +34,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #import #import +#ifdef PROTOCOL_HANDLER +char *protocolCommand = NULL; +#endif + /* ============== Sys_Dialog @@ -115,3 +119,42 @@ char *Sys_StripAppBundle( char *dir ) Q_strncpyz(cwd, Sys_Dirname(cwd), sizeof(cwd)); return cwd; } + +#ifdef PROTOCOL_HANDLER + +@interface AppDelegate : NSObject +@end + +@implementation AppDelegate + +- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent +{ + NSString *input = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + protocolCommand = Sys_ParseProtocolUri( input.UTF8String ); +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + [NSApp stop:nil]; +} + +@end + +char *Sys_InitProtocolHandler( void ) +{ + [NSApplication sharedApplication]; + + AppDelegate *appDelegate = [AppDelegate new]; + NSAppleEventManager *sharedAppleEventManager = [NSAppleEventManager new]; + [sharedAppleEventManager setEventHandler:appDelegate + andSelector:@selector(handleAppleEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + + [NSApp setDelegate:appDelegate]; + [NSApp run]; + + return protocolCommand; +} + +#endif diff --git a/make-macosx-app.sh b/make-macosx-app.sh index 276a21dd..860739b3 100755 --- a/make-macosx-app.sh +++ b/make-macosx-app.sh @@ -196,6 +196,7 @@ CONTENTS_FOLDER_PATH="${WRAPPER_NAME}/Contents" UNLOCALIZED_RESOURCES_FOLDER_PATH="${CONTENTS_FOLDER_PATH}/Resources" EXECUTABLE_FOLDER_PATH="${CONTENTS_FOLDER_PATH}/MacOS" EXECUTABLE_NAME="${PRODUCT_NAME}" +PROTOCOL_HANDLER="quake3" # loop through the architectures to build string lists for each universal binary for ARCH in $SEARCH_ARCHS; do @@ -379,6 +380,21 @@ if [ -n "${MACOSX_DEPLOYMENT_TARGET_PPC}" ] || [ -n "${MACOSX_DEPLOYMENT_TARGET_ " fi + if [ -n "${PROTOCOL_HANDLER}" ]; then + PLIST="${PLIST} + CFBundleURLTypes + + + CFBundleURLName + ${PRODUCT_NAME} + CFBundleURLSchemes + + ${PROTOCOL_HANDLER} + + + " + fi + PLIST="${PLIST} NSHumanReadableCopyright QUAKE III ARENA Copyright © 1999-2000 id Software, Inc. All rights reserved. diff --git a/misc/nsis/ioquake3.nsi.in b/misc/nsis/ioquake3.nsi.in index de029e64..7e2b07df 100644 --- a/misc/nsis/ioquake3.nsi.in +++ b/misc/nsis/ioquake3.nsi.in @@ -45,7 +45,7 @@ OutFile "ioquake3-XXXVERSIONXXX-XXXRELEASEXXX.x86.exe" !insertmacro MULTIUSER_PAGE_INSTALLMODE ;!insertmacro MUI_PAGE_LICENSE "../../COPYING.txt" -!define MUI_COMPONENTSPAGE_NODESC +!define MUI_COMPONENTSPAGE_SMALLDESC !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES @@ -71,7 +71,7 @@ Function un.onInit FunctionEnd ; The stuff to install -Section "ioquake3 (required)" +Section "ioquake3 (required)" ioquake3 SectionIn RO @@ -124,7 +124,7 @@ Section "ioquake3 (required)" SectionEnd ; Optional section (can be disabled by the user) -Section "Start Menu Shortcuts" +Section "Start Menu Shortcuts" StartMenuShortcuts CreateDirectory "$SMPROGRAMS\ioquake3" CreateShortCut "$SMPROGRAMS\ioquake3\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 @@ -132,7 +132,17 @@ Section "Start Menu Shortcuts" SectionEnd -Section "SDL2.dll" +Section "Protocol Handler" ProtocolHandler + + WriteRegStr SHCTX "Software\Classes\quake3" "CustomUrlApplication" "$INSTDIR\ioquake3.x86.exe" + WriteRegStr SHCTX "Software\Classes\quake3" "CustomUrlArguments" '"%1"' + WriteRegStr SHCTX "Software\Classes\quake3" "URL Protocol" "" + WriteRegStr SHCTX "Software\Classes\quake3\DefaultIcon" "" "$INSTDIR\ioquake3.x86.exe,0" + WriteRegStr SHCTX "Software\Classes\quake3\shell\open\command" "" '"$INSTDIR\ioquake3.x86.exe" --uri "%1"' + +SectionEnd + +Section "SDL2.dll" SDL SetOutPath $INSTDIR @@ -141,7 +151,7 @@ Section "SDL2.dll" SectionEnd !ifdef USE_OPENAL_DLOPEN -Section "OpenAL-Soft library" +Section "OpenAL-Soft library" OpenAL SetOutPath $INSTDIR @@ -151,7 +161,7 @@ SectionEnd !endif !ifdef USE_CURL_DLOPEN -Section "libcurl" +Section "libcurl" libcurl SetOutPath $INSTDIR @@ -169,6 +179,7 @@ Section "Uninstall" ; Remove registry keys DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\ioquake3" DeleteRegKey SHCTX "Software\ioquake3" + DeleteRegKey SHCTX "Software\Classes\quake3" ; Remove files and uninstaller Delete $INSTDIR\baseq3\cgamex86.dll @@ -220,3 +231,16 @@ Section "Uninstall" RMDir "$INSTDIR" SectionEnd + +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${ioquake3} "The game executables." + !insertmacro MUI_DESCRIPTION_TEXT ${StartMenuShortcuts} "Create shortcuts in the start menu." + !insertmacro MUI_DESCRIPTION_TEXT ${ProtocolHandler} "The protocol handler lets you connect to a game by clicking a link in a web browser." + !insertmacro MUI_DESCRIPTION_TEXT ${SDL} "SDL files." +!ifdef USE_OPENAL_DLOPEN + !insertmacro MUI_DESCRIPTION_TEXT ${OpenAL} "OpenAL files." +!endif +!ifdef USE_CURL_DLOPEN + !insertmacro MUI_DESCRIPTION_TEXT ${libcurl} "libcurl files." +!endif +!insertmacro MUI_FUNCTION_DESCRIPTION_END diff --git a/misc/setup/ioquake3.desktop b/misc/setup/ioquake3.desktop index 0ccff3ad..644d424c 100644 --- a/misc/setup/ioquake3.desktop +++ b/misc/setup/ioquake3.desktop @@ -1,9 +1,10 @@ [Desktop Entry] Name=ioquake3 -Exec=ioquake3 +Exec=ioquake3 --uri %u Icon=quake3 Type=Application Terminal=false Encoding=UTF-8 Categories=Game;ActionGame; +MimeType=x-scheme-handler/quake3; X-SuSE-translate=false