Building a plug-in
Once you have a working interpreter running in GlkTerm, you can very quickly turn it into a working plugin. You will want to use Zoom version 1.1.2 or later to do this, as this version introduced the plug-in manager that makes it very easy to install new plug-ins.
Add a new target to your XCode project which is a Loadable bundle. Make sure that ZeroLink is turned off and set the wrapper extension to be ‘zoomplugin’. Remove the files that XCode generates for the project, and link to the ZoomPlugIns, GlkView, ZoomView and Cocoa frameworks (Cocoa is in /System/Frameworks, the rest are available from inside the Zoom application).
Next, add a copy files build phase with a destination of ‘Executables’ and add your glk interpreter product to it. This is why you need to pick a different name for your interpreter to your plugin: if they have the same name you’ll end up copying the interpreter over the plug-in bundle executable file.
Now, add a new class that derives from the ZoomGlkPlugIn class (defined in
+ (NSString*) pluginVersion
Returns a string indicating the version number of your plugin. You could just read the version number from your bundle’s Info.plist file here.
+ (NSString*) pluginDescription
Returns a string indicating a description of your plugin.
+ (BOOL) canRunPath: (NSString*) path
Returns YES if the file at the specified path looks like one that your plugin can run. Note that the file might not exist if Zoom is trying to work out if it has a plug-in that can handle a file available at the IFDB, so in general this should just look to see if the path extension is one that your interpreter can handle.
+ (BOOL) canLoadSavegames
Returns YES if your plug-in can load both a story file and a save game simultaneously. If this is YES, then Zoom will allow the user to double-click on saved games and have them immediately start up with the appropriate story file. If NO, then Zoom will open the story file but not restore the save game.
- (id) initWithFilename: (NSString*) gameFile
The designated initialiser for a plug-in. This should call the same function in the superclass, and [self setClientPath: ] with the path name of the interpreter to run.
- (ZoomStoryID*) idForStory
Returns a ZoomStoryID object with the IFID for the file represented by this object. In general, you’ll want to construct the ZoomStoryID object with initWithIdString: or initWithData:type:. The former lets you specify any string as your IFID, the latter will produce a MD5 ID.
You can incorporate some of the public-domain portions of babel to generate an IFID. The sample below shows how parts of the adrift.c file can be included to generate a valid ID. Unfortunately, it’s not possible to use the main babel functions as babel is licensed under the Creative Commons license, which is not compatible with Zoom’s licensing.
- (ZoomStory*) defaultMetadata
Returns a ZoomStory object containing the default metadata for the game represented by this object. You can ask Zoom to run the babel executable to get this information, via the ZoomBabel object. See the sample below for how to achieve this.
- (NSImage*) coverImage
Should return an NSImage object representing the image to use as the cover image in the iFiction window. If the game does not provide a cover image, then you should return an image containing the icon for your interpreter.
- (NSImage*) logo
Like coverImage, but this is the image displayed as the story starts up. You should return nil if you don’t want any startup image displayed.
Plist entries
Once you’ve written the class for your plugin, you will want to update the property list for your bundle to specify to Zoom the name of your class, which icon to use, further information about the interpreter, file associations, etc. You should set the following keys:
NSPrincipalClass
A string representing the name of the class you have declared to represent your plugin.
ZoomPlugin
A dictionary representing information about your plugin. The keys for this will be detailed later on.
CFBundleDocumentTypes
A standard document types dictionary (as for applications) describing the associations for the files that your plugin can handle.
UTExportedTypeDeclarations
A standard universal type declarations object describing the associations for the files your plugin can handle.
ZoomPlugin keys
These keys provide a description of your plug in that Zoom uses while installing it and to display information about it in the Plug-ins window. You should provide all of these keys to have Zoom recognise your plug in as being valid:
Author
That’s your name (ie, the person who wrote the plugin)
DisplayName
The ‘display name’ of the plug-in. This should be the name of the IF system that the plug-in implements: for example for SCARE, this is ‘Adrift’ as SCARE runs Adrift games.
InterpreterAuthor
Optional: the name of the person that wrote the interpreter that your plugin uses. If this isn’t present, Zoom will assume that the person who wrote the plug-in is also the person that wrote the interpreter.
InterpreterVersion
The version number of the interpreter that this plug-in implements.
Version
The version number of the plug-in: this is separate from the interpreter version number as it needs to be possible to fix bugs in a plug-in without there being a whole new version of the interpreter to go along with it.
Image
An icon for the plug-in. Typically you’ll put this in the Resources directory of your bundle: in this case, the value will be something like ‘../../Resources/SCARE.icns’.
ZoomVersion
This should contain the integer value ‘112′ at the moment.
Installing your plug-in
Once you’ve got everything put together and your plug-in compiles, you can try it out in Zoom by double-clicking the .zoomplugin file: Zoom will install your plug-in and restart. You should then be able to run files implemented for your interpreter using the Zoom front-end.
Sample plug-in bundle code
This code is the plug-in bundle code for the SCARE plug-in. It demonstrates how to declare a plug-in, and how to properly generate an IFID by using some of the public-domain code from babel.
//
// ZpScare.m
// Scare
//
// Created by Andrew Hunter on 07/10/2007.
// Copyright 2007 Andrew Hunter. All rights reserved.
//
#import "ZpScare.h"
#include <ZoomPlugIns/md5.h>
@implementation ZpScare
typedef int int32;
#define INVALID_STORY_FILE_RV 0
#define INVALID_USAGE_RV 0
#define INCOMPLETE_REPLY_RV 0
#define ASSERT_OUTPUT_SIZE(x) do { if (output_extent < (x)) return INVALID_USAGE_RV; } while (0)
/* VB RNG constants */
#define VB_RAND1 0x43FD43FD
#define VB_RAND2 0x00C39EC3
#define VB_RAND3 0x00FFFFFF
#define VB_INIT 0x00A09E86
static int32 vbr_state;
static unsigned char taf_translate (unsigned char c)
{
int32 r;
vbr_state = (vbr_state*VB_RAND1+VB_RAND2) & VB_RAND3;
r=UCHAR_MAX * (unsigned) vbr_state;
r/=((unsigned) VB_RAND3)+1;
return r^c;
}
static int32 get_story_file_IFID(void *story_file, int32 extent, char *output, int32 output_extent)
{
int adv;
unsigned char buf[4];
unsigned char *sf=(unsigned char *)story_file;
vbr_state=VB_INIT;
if (extent <12) return INVALID_STORY_FILE_RV;
buf[3]=0;
/* Burn the first 8 bytes of translation */
for(adv=0;adv<8;adv++) taf_translate(0);
/* Bytes 8-11 contain the Adrift version number in the formay N.NN */
buf[0]=taf_translate(sf[8]);
taf_translate(0);
buf[1]=taf_translate(sf[10]);
buf[2]=taf_translate(sf[11]);
adv=atoi((char *) buf);
ASSERT_OUTPUT_SIZE(12);
sprintf(output,"ADRIFT-%03d-",adv);
md5_state_t md5state;
unsigned char r[16];
md5_init(&md5state);
md5_append(&md5state, story_file, extent);
md5_finish(&md5state, r);
snprintf(output + strlen(output), output_extent - strlen(output), "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]);
return 1;
}
+ (NSString*) pluginVersion {
return [[NSBundle bundleForClass: [self class]] objectForInfoDictionaryKey: @"CFBundleVersion"];
}
+ (NSString*) pluginDescription {
return @"Zoom SCARE PlugIn";
}
+ (NSString*) pluginAuthor {
return @"Andrew Hunter";
}
+ (BOOL) canRunPath: (NSString*) path {
NSString* extn = [[path pathExtension] lowercaseString];
// We can run TADS 2 .gam files
if ([extn isEqualToString: @"taf"]) {
return YES;
}
// No blorb types are valid
// Otherwise, follow inherited behaviour (which may in the future include things like save game files)
return [super canRunPath: path];
}
+ (BOOL) canLoadSavegames {
return YES;
}
- (id) initWithFilename: (NSString*) gameFile {
// Initialise as usual
self = [super initWithFilename: gameFile];
if (self) {
// Set the client to be tads-2
[self setClientPath: [[NSBundle bundleForClass: [self class]] pathForAuxiliaryExecutable: @"scare-terp"]];
}
return self;
}
// = Metadata =
- (ZoomStoryID*) idForStory {
NSData* gameData = [self gameData];
char buffer[256];
if (get_story_file_IFID((void*)[gameData bytes], [gameData length], buffer, 256)) {
return [[[ZoomStoryID alloc] initWithIdString: [[[NSString alloc] initWithBytes: buffer
length: strlen(buffer)
encoding: NSISOLatin1StringEncoding] autorelease]]
autorelease];
}
// Generate an MD5-based ID
return [[[ZoomStoryID alloc] initWithData: gameData
type: @"ADRIFT"] autorelease];
}
- (ZoomBabel*) babel {
if (babel == nil) babel = [[ZoomBabel alloc] initWithFilename: [self gameFilename]];
return babel;
}
- (ZoomStory*) defaultMetadata {
// Try babel first
// Try babel first
ZoomStory* story = [[self babel] metadata];
if (story != nil) {
[story addID: [self idForStory]];
} else {
// Just use the default metadata-establishing routine
story = [ZoomStory defaultMetadataForFile: [self gameFilename]];
}
if ([story group] == nil || [@"" isEqualToString: [story group]] || [@"Z-Code" isEqualToString: [story group]]) {
[story setGroup: @"Adrift"];
}
return story;
}
- (NSImage*) coverImage {
// Try babel first
NSImage* image = [[self babel] coverImage];
if (image != nil) return image;
// Use the SCARE icon
return [[[NSImage alloc] initWithContentsOfFile: [[NSBundle mainBundle] pathForResource: @"SCARE"
ofType: @"icns"]] autorelease];
}
- (NSImage*) logo {
// Use a logo if babel reports one
NSImage* image = [[self babel] coverImage];
if (image != nil) return [self resizeLogo: image];
// Otherwise use nothing
return nil;
}
@end