ftp.nice.ch/pub/next/games/board/Risk.0.97.s.tar.gz#/RiskSource0.97/Risk/GameSetup.m

This is GameSetup.m in view mode; [Download] [Up]

//  GameSetup.m
// Part of Risk by Mike Ferris

#import "GameSetup.h"
#import "RiskController.h"
#import "Mover.h"
#import "ComputerPlayer.h"
#import "CardManager.h"
#import "DefaultManager.h"
#import "MapView.h"
#import <appkit/NXColorWell.h>
#import <appkit/Form.h>
#import <appkit/Matrix.h>
#import <appkit/ButtonCell.h>
#import "LanguageApp.h"
#import <appkit/Panel.h>
#import <appkit/Matrix.h>
#import <appkit/Text.h>
#import <appkit/NXImage.h>
#import <appkit/ScrollView.h>
#import <objc/Storage.h>
#import <appkit/PopUpList.h>
#import <objc/objc-load.h>

#import <stdlib.h>				/* for getenv() */
#import <strings.h>				/* for index() */
#import <sys/file.h>			/* for access() */
#import <sys/param.h>			/* for MAXPATHLEN */
#import <sys/dir.h>				/* for opendir(), etc. */

@implementation GameSetup

+ initialize
{
	if (self == [GameSetup class])  {
		[self setVersion:1];
	}
	return self;
}

- appDidInit:sender
{
	int i;
	
	compPlayersExist = NO;
	[setupPanel setFloatingPanel:YES];
	[self initCPMenus];
	for (i=0;i<6;i++)  {
		names[i]=NULL;
		computerPlayers[i]=nil;
	}
	[self defaultSetup:self];
	[self setCurrentFromPanel:self];
	newGame = YES;
	
	return self;
}

- free
{
	int i;
	
	for (i=0;i<6;i++)  {
		if (names[i]) free(names[i]);
	}
	return [super free];
}

- newGame:sender
{
	newGame = YES;
	[radio1 setEnabled:YES];
	[radio2 setEnabled:YES];
	[radio3 setEnabled:YES];
	[radio4 setEnabled:YES];
	[radio5 setEnabled:YES];
	[radio6 setEnabled:YES];
	[popup1 setEnabled:YES];
	[popup2 setEnabled:YES];
	[popup3 setEnabled:YES];
	[popup4 setEnabled:YES];
	[popup5 setEnabled:YES];
	[popup6 setEnabled:YES];
	[countryRadio setEnabled:YES];
	[armyRadio setEnabled:YES];
	[cardRadio setEnabled:YES];
	[fortifyRadio setEnabled:YES];
	if (compPlayersExist)  {
		[aboutMatrix setEnabled:YES];
	}  else  {
		[aboutMatrix setEnabled:NO];
	}
	[setupPanel makeKeyAndOrderFront:self];
    return self;
}

- changeSettings:sender
{
	newGame = NO;
	[radio1 setEnabled:NO];
	[radio2 setEnabled:NO];
	[radio3 setEnabled:NO];
	[radio4 setEnabled:NO];
	[radio5 setEnabled:NO];
	[radio6 setEnabled:NO];
	[popup1 setEnabled:NO];
	[popup2 setEnabled:NO];
	[popup3 setEnabled:NO];
	[popup4 setEnabled:NO];
	[popup5 setEnabled:NO];
	[popup6 setEnabled:NO];
	[countryRadio setEnabled:YES];
	[armyRadio setEnabled:YES];
	[cardRadio setEnabled:NO];
	[fortifyRadio setEnabled:YES];
	[setupPanel makeKeyAndOrderFront:self];
	if (compPlayersExist)  {
		[aboutMatrix setEnabled:YES];
	}  else  {
		[aboutMatrix setEnabled:NO];
	}
    return self;
}

- doneSetupAction:sender
{
	int ret, numPlaying=0;
	
	ret = [[sender selectedCell] tag];
	if (ret == 1)  {
		// OK button. Accept values.
		numPlaying = (([radio1 selectedCol] == S_NOTPLAYING)?0:1) + 
					 (([radio2 selectedCol] == S_NOTPLAYING)?0:1) + 
					 (([radio3 selectedCol] == S_NOTPLAYING)?0:1) + 
					 (([radio4 selectedCol] == S_NOTPLAYING)?0:1) + 
					 (([radio5 selectedCol] == S_NOTPLAYING)?0:1) + 
					 (([radio6 selectedCol] == S_NOTPLAYING)?0:1);
		if (numPlaying<=1)  {
			NXRunAlertPanel("Warning", "At least two players have to play.", 
							"OK", NULL, NULL);
		}  else  {
			[self setCurrentFromPanel:self];
			[setupPanel orderOut:self];
			[self writeDefaults];
			if (newGame)  {
				[theMover startNewGame];
				newGame = NO;
			}  else  {
				[theMover settingsChanged];
			}
		}
	}  else  {
		// Cancel button. revert.
		[self revertToCurrent:self];
		[setupPanel orderOut:self];
	}
	
    return self;
}

- setCurrentFromPanel:sender
{
	const char *temp;
	int i;
	const char *classname = NULL;
	id cpClass;
	
	// names
	for (i=0;i<6;i++)  {
		temp = [nameForm stringValueAt:i];
		if (names[i]!=NULL)  {
			free(names[i]);
		}
		names[i]=(char *)malloc(strlen(temp)+1);
		strcpy(names[i], temp);
	}
	// colors
	colors[0] = [well1 color];
	colors[1] = [well2 color];
	colors[2] = [well3 color];
	colors[3] = [well4 color];
	colors[4] = [well5 color];
	colors[5] = [well6 color];

	if (newGame)  {
		// strategies
		strategies[0] = [radio1 selectedCol];
		strategies[1] = [radio2 selectedCol];
		strategies[2] = [radio3 selectedCol];
		strategies[3] = [radio4 selectedCol];
		strategies[4] = [radio5 selectedCol];
		strategies[5] = [radio6 selectedCol];
		
		for (i=0;i<6;i++)  {
			if (computerPlayers[i] != nil)  {
				[computerPlayers[i] free];
			}
			if (strategies[i]==S_COMPUTER)  {
				if (!compPlayersExist)  {
					strategies[i]=S_HUMAN;
					continue;					
				}
				switch (i)  {
					case 0:
						classname = [popup1 title];
						break;
					case 1:
						classname = [popup2 title];
						break;
					case 2:
						classname = [popup3 title];
						break;
					case 3:
						classname = [popup4 title];
						break;
					case 4:
						classname = [popup5 title];
						break;
					case 5:
						classname = [popup6 title];
						break;
					default:
						break;
				}
				cpClass = objc_getClass(classname);
				computerPlayers[i] = [[cpClass allocFromZone:[self zone]] 
										initPlayerNum:i mover:theMover 
										gameSetup:self mapView:theMapView 
										cardManager:theCardManager];
				
			}
		}
		// card values
		cardRedemption = [cardRadio selectedRow];
	}
	// chosen or randomly selected countries
	countryDistribution = [countryRadio selectedRow];
	// initial armies per round
	switch ([armyRadio selectedRow])  {
		case A_BYONES:
			initialArmyPlacement=1;
			break;
		case A_BYTHREES:
			initialArmyPlacement=3;
			break;
		case A_BYFIVES:
			initialArmyPlacement=5;
			break;
		default:
			break;
	}
	// fortify rule
	fortifyRule = [fortifyRadio selectedRow];
	
	[self revertToCurrent:self];  // in case changes were made
	return self;
}

- revertToCurrent:sender
{
	int i;
	
	for (i=0;i<6;i++)  {
		[nameForm setStringValue:names[i] at:i];
	}
	[well1 setColor:colors[0]];
	[well2 setColor:colors[1]];
	[well3 setColor:colors[2]];
	[well4 setColor:colors[3]];
	[well5 setColor:colors[4]];
	[well6 setColor:colors[5]];
	[radio1 selectCellAt:0:strategies[0]];
	[radio2 selectCellAt:0:strategies[1]];
	[radio3 selectCellAt:0:strategies[2]];
	[radio4 selectCellAt:0:strategies[3]];
	[radio5 selectCellAt:0:strategies[4]];
	[radio6 selectCellAt:0:strategies[5]];
	[cardRadio selectCellAt:cardRedemption:0];
	[countryRadio selectCellAt:countryDistribution:0];
	switch (initialArmyPlacement)  {
		case 1:
			[armyRadio selectCellAt:A_BYONES:0];
			break;
		case 3:
			[armyRadio selectCellAt:A_BYTHREES:0];
			break;
		case 5:
			[armyRadio selectCellAt:A_BYFIVES:0];
			break;
		default:
			break;
	}
	[fortifyRadio selectCellAt:fortifyRule:0];

	return self;
}

- defaultSetup:sender
{
	const char *tempStr;
	float x[18];
	int trash, options[4];
	NXColor tempColor;
	char name1[20], name2[20], name3[20], name4[20], name5[20], name6[20];
	
	// set the color wells from the default manager
	tempStr = [theDefaultManager playerColors];
	trash = sscanf(tempStr, "%f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, "
					"%f, %f, %f, %f, %f, %f", 
					&(x[0]), &(x[1]), &(x[2]), 
					&(x[3]), &(x[4]), &(x[5]), 
					&(x[6]), &(x[7]), &(x[8]), 
					&(x[9]), &(x[10]), &(x[11]), 
					&(x[12]), &(x[13]), &(x[14]), 
					&(x[15]), &(x[16]), &(x[17]));
	tempColor = NXConvertRGBToColor(x[0], x[1], x[2]);
	[well1 setColor:tempColor];
	tempColor = NXConvertRGBToColor(x[3], x[4], x[5]);
	[well2 setColor:tempColor];
	tempColor = NXConvertRGBToColor(x[6], x[7], x[8]);
	[well3 setColor:tempColor];
	tempColor = NXConvertRGBToColor(x[9], x[10], x[11]);
	[well4 setColor:tempColor];
	tempColor = NXConvertRGBToColor(x[12], x[13], x[14]);
	[well5 setColor:tempColor];
	tempColor = NXConvertRGBToColor(x[15], x[16], x[17]);
	[well6 setColor:tempColor];
	
	// set the names from the default manager
	tempStr = [theDefaultManager playerNames];
	trash = sscanf(tempStr, "%[^,], %[^,], %[^,], %[^,], %[^,], %[^,]", 
					name1, name2, name3, name4, name5, name6);
	[nameForm setStringValue:name1 at:0];
	[nameForm setStringValue:name2 at:1];
	[nameForm setStringValue:name3 at:2];
	[nameForm setStringValue:name4 at:3];
	[nameForm setStringValue:name5 at:4];
	[nameForm setStringValue:name6 at:5];

	// set the options from the default manager
	tempStr = [theDefaultManager setupOptions];
	trash = sscanf(tempStr, "%d, %d, %d, %d", &(options[0]), &(options[1]), 
					&(options[2]), &(options[3]));
	[countryRadio selectCellAt:options[0]:0];
	[armyRadio selectCellAt:options[1]:0];
	[cardRadio selectCellAt:options[2]:0];
	[fortifyRadio selectCellAt:options[3]:0];

	// set the radio buttons absolutely	
	[radio1 selectCellAt:0:0];	
	[radio2 selectCellAt:0:0];	
	[radio3 selectCellAt:0:0];	
	[radio4 selectCellAt:0:0];	
	[radio5 selectCellAt:0:0];	
	[radio6 selectCellAt:0:0];	
	
	return self;
}

- writeDefaults
{
	char str[1000]="", buf[100];
	float r, g, b;
	int temp;
	
	// first do the colors
	NXConvertColorToRGB(colors[0], &r, &g, &b);
	sprintf(buf, "%f, %f, %f, ", r, g, b);  strcat(str, buf);
	NXConvertColorToRGB(colors[1], &r, &g, &b);
	sprintf(buf, "%f, %f, %f, ", r, g, b);  strcat(str, buf);
	NXConvertColorToRGB(colors[2], &r, &g, &b);
	sprintf(buf, "%f, %f, %f, ", r, g, b);  strcat(str, buf);
	NXConvertColorToRGB(colors[3], &r, &g, &b);
	sprintf(buf, "%f, %f, %f, ", r, g, b);  strcat(str, buf);
	NXConvertColorToRGB(colors[4], &r, &g, &b);
	sprintf(buf, "%f, %f, %f, ", r, g, b);  strcat(str, buf);
	NXConvertColorToRGB(colors[5], &r, &g, &b);
	sprintf(buf, "%f, %f, %f", r, g, b);  strcat(str, buf);
	[theDefaultManager setPlayerColors:str];
	
	strcpy(str, "");
	
	// now the names
	sprintf(str, "%s, %s, %s, %s, %s, %s", names[0], names[1], 
			names[2], names[3], names[4], names[5]);
	[theDefaultManager setPlayerNames:str];
	
	strcpy(str, "");
	
	// finally the options
	switch (initialArmyPlacement)  {
		case 1:
			temp=0;
			break;
		case 3:
			temp=1;
			break;
		case 5:
			temp=2;
			break;
		default:
			temp=0;
			break;
	}
	sprintf(str, "%d, %d, %d, %d", countryDistribution, temp, 
				cardRedemption, fortifyRule);
	[theDefaultManager setSetupOptions:str];
	
	return self;
}

- (int)numberOfPlayers
{
	return (((strategies[0] == S_NOTPLAYING)?0:1) + 
			((strategies[1] == S_NOTPLAYING)?0:1) + 
			((strategies[2] == S_NOTPLAYING)?0:1) + 
			((strategies[3] == S_NOTPLAYING)?0:1) + 
			((strategies[4] == S_NOTPLAYING)?0:1) + 
			((strategies[5] == S_NOTPLAYING)?0:1));

}

- (NXColor)colorOfPlayer:(int)playerNum
{
	return colors[playerNum];
}

- (int)strategyOfPlayer:(int)playerNum
{
	return strategies[playerNum];
}

- (const char *)nameOfPlayer:(int)playerNum
{
	return names[playerNum];
}

- (int)armiesForCardSet:(int)cardSetNum
{
	if (cardSetNum==1)  {
		return 4;
	}
	switch(cardRedemption)  {
		case C_CONST:
			return 5;
			break;
		case C_BYONES:
			return cardSetNum+3;
			break;
		case C_BYFIVES:
			if (cardSetNum<6)  {
				return (cardSetNum+1)*2;
			}  else  {
				return (cardSetNum-3)*5;
			}
			break;
	}
	return 0;
}

- (int)countryDistribution { return countryDistribution; }
- (int)initialArmyPlacement { return initialArmyPlacement; }
- (int)initialPlaceArmiesForPlayer:(int)p
{
	if (strategies[p] == S_NOTPLAYING)  {
		return 0;
	}
	switch ([self numberOfPlayers])  {
		case 2:
			return 60-[theMapView numCountriesForPlayer:p];
			break;
		case 3:
			return 36-[theMapView numCountriesForPlayer:p];
			break;
		case 4:
			return 30-[theMapView numCountriesForPlayer:p];
			break;
		case 5:
			return 25-[theMapView numCountriesForPlayer:p];
			break;
		case 6:
			return 21-[theMapView numCountriesForPlayer:p];
			break;
		default:
			return 0;
	}
}

- (int)fortifyRule  {  return fortifyRule;  }

- computerPlayerForPlayer:(int)p
{
	if ((p>=0) && (p<=5))  {
		return computerPlayers[p];
	}  else  {
		return nil;
	}
}

- playerConquered:(int)loser
{
	strategies[loser]=S_NOTPLAYING;
	[self revertToCurrent:self];
	
	return self;
}

- strategyPopupAction:sender
{
	return self;
}

- computeInitialArmies:sender
{
	int numPlayers=0;

	numPlayers += (([radio1 selectedCol]==S_NOTPLAYING)?0:1);
	numPlayers += (([radio2 selectedCol]==S_NOTPLAYING)?0:1);
	numPlayers += (([radio3 selectedCol]==S_NOTPLAYING)?0:1);
	numPlayers += (([radio4 selectedCol]==S_NOTPLAYING)?0:1);
	numPlayers += (([radio5 selectedCol]==S_NOTPLAYING)?0:1);
	numPlayers += (([radio6 selectedCol]==S_NOTPLAYING)?0:1);
	switch (numPlayers)  {
		case 2:
			[armiesTextField setIntValue:60];
			break;
		case 3:
			[armiesTextField setIntValue:35];
			break;
		case 4:
			[armiesTextField setIntValue:30];
			break;
		case 5:
			[armiesTextField setIntValue:25];
			break;
		case 6:
			[armiesTextField setIntValue:20];
			break;
		default:
			[armiesTextField setStringValue:"--"];
	}
	
	return self;
}

- aboutAction:sender
{
	int tag = [[sender selectedCell] tag];
	id theRadio=nil, thePopup=nil;
	char str[100];
	id image=nil;
	NXStream *stream;
	
	switch (tag)  {
		case 0:
			theRadio=radio1;
			thePopup=popup1;
			break;
		case 1:
			theRadio=radio2;
			thePopup=popup2;
			break;
		case 2:
			theRadio=radio3;
			thePopup=popup3;
			break;
		case 3:
			theRadio=radio4;
			thePopup=popup4;
			break;
		case 4:
			theRadio=radio5;
			thePopup=popup5;
			break;
		case 5:
			theRadio=radio6;
			thePopup=popup6;
			break;
	}
	switch ([theRadio selectedCol])  {
		case 0:
			// not playing
			[aboutIconButton setImage:nil];
			sprintf(str, "%d Not Playing", tag +1);
			[aboutNameField setStringValue:str];
			sprintf(str, "%s/NotPlaying.rtf", [NXApp applicationDirectory]);
			stream = NXMapFile(str, NX_READONLY);
			if (stream!=NULL)  {
				[[aboutScroll docView] readRichText:stream];
				NXCloseMemory(stream, NX_FREEBUFFER);
			}  else  {
				[[aboutScroll docView] setText:""];
			}
			[aboutScroll display];
			break;
		case 1:
			// human player
			[aboutIconButton setIcon:"Human"];
			sprintf(str, "%d Human Player", tag +1);
			[aboutNameField setStringValue:str];
			sprintf(str, "%s/Human.rtf", [NXApp applicationDirectory]);
			stream = NXMapFile(str, NX_READONLY);
			if (stream!=NULL)  {
				[[aboutScroll docView] readRichText:stream];
				NXCloseMemory(stream, NX_FREEBUFFER);
			}  else  {
				[[aboutScroll docView] setText:""];
			}
			[aboutScroll display];
			break;
		case 2:
			// computer player
				if (compPlayersExist)  {
				sprintf(str, "%s/%s.cp/%s.tiff", [NXApp applicationDirectory],
								[thePopup title], [thePopup title]);
				image = [[NXImage allocFromZone:[self zone]] initFromFile:str];
				if (image != nil)  {
					[aboutIconButton setImage:image];
				}
				sprintf(str, "%d %s.cp", tag +1, [thePopup title]);
				[aboutNameField setStringValue:str];
				sprintf(str, "%s/%s.cp/%s.rtf", [NXApp applicationDirectory],
								[thePopup title], [thePopup title]);
				stream = NXMapFile(str, NX_READONLY);
				if (stream!=NULL)  {
					[[aboutScroll docView] readRichText:stream];
					NXCloseMemory(stream, NX_FREEBUFFER);
				}  else  {
					[[aboutScroll docView] setText:""];
				}
			}  else  {
				[aboutIconButton setImage:nil];
				sprintf(str, "%d No Computer Players", tag +1);
				[aboutNameField setStringValue:str];
				[[aboutScroll docView] setText:""];
			}
			[aboutScroll display];
			break;
	}
	[aboutPanel makeKeyAndOrderFront:self];
	
	[NXApp runModalFor:aboutPanel];
	
	[aboutPanel orderOut:self];
	
	if (image != nil)  {
		[image free];
	}
	return self;
}

- aboutStopAction:sender
{
	[NXApp stopModal];
	return self;
}

#define MAX_CPNAME_SIZE 31
// load all the computer players.  computer player directories reside in the
// app package.  They are named with the name of the class and a ".cp" 
// extension.  Inside the Classname.cp directory there is at least a
// Classname.o file containing the subclass of ComputerPlayer which implements
// the computer player.  It may also contain other files needed by the 
// computer player implementation.
- initCPMenus
{
	id  cpNameStorage = [[Storage allocFromZone:[self zone]] initCount:0
							elementSize:MAX_CPNAME_SIZE 
							description:"[MAX_CPNAME_SIZEc]"];
	id  tempStorage = [[Storage allocFromZone:[self zone]] initCount:0
							elementSize:MAXPATHLEN 
							description:"[MAXPATHLENc]"];
	BOOL keepTrying=YES;
    DIR *dir;
    struct direct *de;
    NXStream *nxstderr;
    char path[MAXPATHLEN], temp[MAXPATHLEN];
    char *filenames[] = {path, NULL};
	int i, numstrings;
	NXAtom dirname;

    nxstderr = NXOpenFile(2, NX_WRITEONLY);

	dirname = [NXApp applicationDirectory];
    dir = opendir(dirname);
    if (dir == NULL) {
		return self;
    }

    while ((de = readdir(dir)) != NULL)
	{
		BOOL ok;
	
		// Ignore '.'-files (not really necessary, I guess)
		if (de->d_name[0] == '.')
			continue;

		// Check that the file has the right extension
		if (de->d_namlen < 4 ||
				strcmp(&de->d_name[de->d_namlen-3], ".cp") != 0)
			continue;
		
		// refuse to load if the name matches a module already loaded
		numstrings = [cpNameStorage count];
		ok = YES;
		strcpy(path, de->d_name);
		*(rindex(path, '.')) = '\0';

		for (i=0; i< numstrings; i++)
		{
			if (!strcmp(path, [cpNameStorage elementAt:i]))
			{
				ok = NO;
				break;
			}
		}
		if (!ok) continue;

		// OK, all is well -- go load the little bugger
		sprintf(temp, "%s.o", path);
		sprintf(path, "%s/%s/%s", dirname, de->d_name, temp);
		if (objc_loadModules(filenames, nxstderr, NULL, NULL, NULL) == 1)  {
			// Ugh, failed.  Put the class name in tempStorage in case
			// it can't be loaded because it's a subclass of another
			// CP who hasn't been loaded yet.
			[tempStorage addElement:path];
		}  else  {
			// it loaded so add it to the list.
			// Smash out the '.' in ".../FooView.o"
			*(rindex(path, '.')) = '\0';
			[cpNameStorage addElement: rindex(path, '/') + 1];
		}
    }

    closedir(dir);
	
	// now loop and keep trying to load the ones in tempStorage.  Keep trying
	// as long as at least one of the failed ones succeeds each time through
	// the loop
	
	while (keepTrying)  {
		keepTrying=NO;
		for (i=[tempStorage count]-1; i>=0; i--)  {
			strcpy(path, (char *)[tempStorage elementAt:i]);
			if (objc_loadModules(filenames, nxstderr, NULL, NULL, NULL)!=1)  {
				// it loaded so add it to the list.
				// Smash out the '.' in ".../FooView.o"
				*(rindex(path, '.')) = '\0';
				[cpNameStorage addElement: rindex(path, '/') + 1];
				// since one loaded successfully, set the flag
				keepTrying=YES;
				// and delete it from the tempStorage
				[tempStorage removeElementAt:i];
			}
		}
	}

    NXClose(nxstderr);
	
	// now cpNameStorage contains a list of all the menu strings.
	// we must add them to the menus in the panel.
	numstrings = [cpNameStorage count];
	if (numstrings > 0)  {
		[[[popup1 target] removeItemAt:0] free];
		[[[popup2 target] removeItemAt:0] free];
		[[[popup3 target] removeItemAt:0] free];
		[[[popup4 target] removeItemAt:0] free];
		[[[popup5 target] removeItemAt:0] free];
		[[[popup6 target] removeItemAt:0] free];
		[popup1 setTitle:[cpNameStorage elementAt:0]];
		[popup2 setTitle:[cpNameStorage elementAt:0]];
		[popup3 setTitle:[cpNameStorage elementAt:0]];
		[popup4 setTitle:[cpNameStorage elementAt:0]];
		[popup5 setTitle:[cpNameStorage elementAt:0]];
		[popup6 setTitle:[cpNameStorage elementAt:0]];
		compPlayersExist = YES;
	}
	for (i=0;i<numstrings;i++)  {
		[[popup1 target] addItem:[cpNameStorage elementAt:i]];
		[[popup2 target] addItem:[cpNameStorage elementAt:i]];
		[[popup3 target] addItem:[cpNameStorage elementAt:i]];
		[[popup4 target] addItem:[cpNameStorage elementAt:i]];
		[[popup5 target] addItem:[cpNameStorage elementAt:i]];
		[[popup6 target] addItem:[cpNameStorage elementAt:i]];
	}
	[cpNameStorage free];
	[tempStorage free];
	
    return self;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.