diff --git a/CK1PATCH.EXE b/CK1PATCH.EXE new file mode 100755 index 0000000..61e0bff Binary files /dev/null and b/CK1PATCH.EXE differ diff --git a/CWSDPMI.EXE b/CWSDPMI.EXE new file mode 100644 index 0000000..98e7f16 Binary files /dev/null and b/CWSDPMI.EXE differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d56016a --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +Commander Keen Randomiser +Ⓒ 2020 David Gow + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.TXT b/README.TXT new file mode 100644 index 0000000..ed6bc83 --- /dev/null +++ b/README.TXT @@ -0,0 +1,104 @@ +=========================== +Commander Keen 1 Randomiser +=========================== + +Author: David Gow +Version: 1.0 +Website: https://davidgow.net/keen/randomiser.html + + +About +----- + +The Keen 1 Randomiser lets you play Keen1 with a twist: items, levels, and +enemies are all randomised. + +The randomiser takes a copy of Keen 1, and rewrites the level files to give you +a new experience. It also creates a CK1PATCH-compatible patch file which loads +the replacement files and patches the random number seed in. + +What does it change? +-------------------- + +- Entrances to all levels on the world map are randomised. +- Positions of key items (spaceship parts and pogo stick) are randomised. +- Enemy positions are shuffled (within a level). +- Point items (lollies) are shuffled within a level. +- Door/Keycard colours are randomised (cosmetic only). +- Some block colours are randomised. +- Yorp statues now hint at item locations. +- You can see the random number seed on the Status Screen (press Space) + +Note that there are some things which don't change: +- Level layouts remain the same. +- Raygun ammo remains where it is in the original game. +- There is always a Pogo Stick at the bottom of the Vorticon Commander's Castle +- The Garg statue remains unhelpful. +- The world map looks identical (but the levels within are different) + +How do I use it? +---------------- + +Just run RNDKEEN1.BAT: a differently-randomised version of the game will greet +you each time you play. + +If you want to replay the same game (or have several people play the same +randomised version), you can pass a random number seed to the randomiser: +C:\RANDKEEN> RNDKEEN1 /SEED 12345 +(Replace 12345 with the seed you want to play.) + +You can check what seed you're running from the Status Screen by pressing SPACE. + +If you just want to generate the game, not play it, you can run RANDKEEN.EXE: +this will generate the randomised maps and patch file. Note you'll still need +to rename LEVEL81.CK1 and LEVEL90.CK1 to RNDLV81.CK1 and RNDLV90.CK1 +respectively for the world map and ending screen to work (this is usually done +by the RNDKEEN1.BAT file). + +RNDKEEN1.BAT and RANDKEEN.EXE accept some extra command-line parameters: +- /? -- Show a help screen listing these parameters. +- /SEED -- set the random number seed to +- /DEBUG -- show additional debug information (can contain spoilers!) +- /NOLEVELNAMES -- Make Yorp hints use level numbers instead of names + +The Keen 1 Randomiser requires a 386 or better to run, and probably needs a +megabyte or so of memory. + +Hints! +------ + +Playing Keen 1 Randomiser is, for the most part, very similar to playing Keen1, +but there are a few things to be wary of. + +- You'll probably need to play through levels which are not necessary to win + the original game. Familiarise yourself with them. +- The Pogo Stick is much harder to get: you'll probably need to complete a + lot of levels without it! +- Ship Parts and the Pogo Stick will always be either in their original + locations, or right next to the exit. You can't miss them! +- Raygun Ammo is where you remember it from the original game, but because + you'll be playing levels in a different order, you'll probably find ammo + hard to find. Use it wisely (and get good at dodging!) +- Shrine levels may contain Yorp statues, which have hints. You'll want to know + the level names[1] to take advantage of these. (Or, if you know the level + numbers, you can use the /NOLEVELNAMES switch.) +- There's always a Pogo Stick in the Vorticon Commander's dungeon. + +[1]: http://www.shikadi.net/keenwiki/Keen_1_Levels + +Building the Source Code +------------------------ + +If you want to modify Keen 1 Randomiser (or just see how it works), the source +code is included in this archive (randkeen.c), and is also available on GitHub: + +https://github.com/sulix/randkeen + +It's normally compiled with DJGPP, but can also be built for non-DOS platforms +(just compile randkeen.c with your favourite compiler: it should be reasonably +standard C99). + +Make sure that CK1PATCH and CWSDPMI are copied to the Keen1 directory, as well +as the RNDKEEN1.BAT and RANDKEEN.EXE files. There's an included shell script +which compiles RANDKEEN.EXE and does this, though it has some hardcoded paths +in it. diff --git a/RNDKEEN1.BAT b/RNDKEEN1.BAT new file mode 100644 index 0000000..48dfe66 --- /dev/null +++ b/RNDKEEN1.BAT @@ -0,0 +1,5 @@ +@ECHO OFF +COPY LEVEL81.CK1 RNDLV81.CK1 +COPY LEVEL90.CK1 RNDLV90.CK1 +RANDKEEN %1 %2 %3 %4 %5 +CK1PATCH RNDKEEN1.PAT diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ddc158f --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +OUTDIR=rndkeen1/ + +/usr/local/djgpp/bin/i586-pc-msdosdjgpp-gcc -o RANDKEEN.EXE randkeen.c +cp RNDKEEN1.BAT $OUTDIR +cp RANDKEEN.EXE $OUTDIR +cp CWSDPMI.EXE $OUTDIR +cp CK1PATCH.EXE $OUTDIR diff --git a/randkeen.c b/randkeen.c new file mode 100644 index 0000000..eff78c8 --- /dev/null +++ b/randkeen.c @@ -0,0 +1,670 @@ +/* + * Keen 1 Randomiser, by David Gow + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + + +// Options +int opt_seed = 0; +bool opt_useLevelNames = true; +bool opt_debug = false; + +void debugf(const char *format, ...) +{ + if (!opt_debug) + return; + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +uint16_t fread_u16(FILE *f) +{ + uint16_t u; + fread(&u,2,1,f); + return u; +} + +uint32_t fread_u32(FILE *f) +{ + uint32_t u; + fread(&u, 4, 1, f); + return u; +} + +void fwrite_u16(FILE *f, uint16_t val) +{ + fwrite(&val, 2, 1, f); +} + +void fwrite_u32(FILE *f, uint16_t val) +{ + fwrite(&val, 4, 1, f); +} + +typedef struct VorticonsMap +{ + uint16_t w; + uint16_t h; + uint16_t *planes[2]; +} VorticonsMap; + +#define VORT_MAP_RLE_TAG 0xFEFE + +uint16_t *VORT_DecompressRLE(FILE *f, uint32_t decompSize) +{ + uint16_t *data = malloc(decompSize); + + for(uint16_t *ptr = data; decompSize;) + { + uint16_t val = fread_u16(f); + if (val == VORT_MAP_RLE_TAG) + { + uint16_t length = fread_u16(f); + val = fread_u16(f); + while (length--) + { + *(ptr++) = val; + decompSize -= 2; + } + } + else + { + *(ptr++) = val; + decompSize -= 2; + } + } + return data; +} + +void VORT_CompressRLE(uint16_t* data, uint32_t dataSize, FILE *f) +{ + uint16_t currentVal = 0xFFFF; + uint16_t currentValCount = 0; + while (dataSize) + { + uint16_t val = *(data++); + if (val != currentVal) + { + if (currentValCount > 3) + { + fwrite_u16(f, VORT_MAP_RLE_TAG); + fwrite_u16(f, currentValCount); + fwrite_u16(f, currentVal); + } + else + { + for (int i = 0; i < currentValCount; ++i) + { + fwrite_u16(f, currentVal); + } + } + currentVal = val; + currentValCount = 1; + } + else + { + currentValCount++; + } + dataSize -= 2; + } + if (currentValCount) + { + if (currentValCount > 3) + { + fwrite_u16(f, VORT_MAP_RLE_TAG); + fwrite_u16(f, currentValCount); + fwrite_u16(f, currentVal); + } + else + { + for (int i = 0; i < currentValCount; ++i) + { + fwrite_u16(f, currentVal); + } + } + } +} + + +VorticonsMap VORT_LoadMap(FILE *f) +{ + uint32_t decompSize = fread_u32(f); + uint16_t *mapData = VORT_DecompressRLE(f, decompSize); + VorticonsMap vMap; + vMap.w = mapData[0]; + vMap.h = mapData[1]; + uint16_t mapPlaneSize = mapData[7]; + vMap.planes[0] = mapData + 16; + vMap.planes[1] = mapData + 16 + (mapPlaneSize/2); + + return vMap; +} + +uint16_t VORT_GetTileAtPos(VorticonsMap *vMap, int x, int y, int plane) +{ + return vMap->planes[plane][y * vMap->w + x]; +} + +void VORT_SetTileAtPos(VorticonsMap *vMap, int x, int y, int plane, uint16_t tile) +{ + vMap->planes[plane][y * vMap->w + x] = tile; +} + +void VORT_SaveMap(VorticonsMap *vMap, FILE *f) +{ + uint16_t planeSize = (vMap->w * vMap->h * 2 + 15) & ~15; + uint32_t dataLen = planeSize*2 + 32; + uint16_t *data = calloc(dataLen,1); + data[0] = vMap->w; + data[1] = vMap->h; + data[2] = 2; // Number of planes. + data[7] = planeSize; + + memcpy(&data[16], vMap->planes[0], planeSize); + memcpy(&data[16 + (planeSize / 2)], vMap->planes[1], planeSize); + + fwrite_u32(f, dataLen); + VORT_CompressRLE(data, dataLen, f); +} + + +bool VORT_FindTile(VorticonsMap *vMap, uint16_t tile, int plane, int *x, int *y) +{ + for (int _y = *y; _y < vMap->h; ++_y) + { + for (int _x = (_y == *y)?*x:0; _x < vMap->w; ++_x) + { + if (VORT_GetTileAtPos(vMap, _x, _y, plane) == tile) + { + *x = _x; + *y = _y; + return true; + } + } + } + return false; +} + +void VORT_ReplaceTiles(VorticonsMap *vMap, uint16_t tileFrom, uint16_t tileTo, int plane) +{ + int x = 0; + int y = 0; + while (VORT_FindTile(vMap, tileFrom, plane, &x, &y)) + { + debugf("Found tile %d at (%d, %d)\n", tileFrom, x, y); + VORT_SetTileAtPos(vMap, x, y, plane, tileTo); + } +} + +void PermuteArray(int *array, int count) +{ + for (int i = 0; i < count - 2; ++i) + { + int j = (rand() % (count - i)) + i; + if (array[i] != array[j]) + { + array[i] ^= array[j]; + array[j] ^= array[i]; + array[i] ^= array[j]; + } + } + +} + +#define K1_T_GREYSKY 143 +#define K1_T_POGOSTICK 176 +#define K1_T_JOYSTICK 221 +#define K1_T_BATTERY 237 +#define K1_T_EVERCLEAR 245 +#define K1_T_VACUUM 241 +#define K1_T_EXITSIGN1 167 +#define K1_T_EXITSIGN2 168 + +// NOTE: We don't shuffle the position of the Pogo Stick in Level 16, as it may +// be required to complete the level, and hence the game. +int slotsPerLevel[16] = {1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1}; +int itemsPerSlot[20] = {K1_T_POGOSTICK, K1_T_JOYSTICK, K1_T_BATTERY, K1_T_EVERCLEAR, K1_T_VACUUM, + K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, + K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY, + K1_T_GREYSKY, K1_T_GREYSKY, K1_T_GREYSKY}; + +void K1_SetSpecialItem(VorticonsMap *vMap, uint16_t item, int slot) +{ + // There are two types of special item slots: + // - A place where a special item normally goes + // - The place below the "exit" sign in a level + + if (item != K1_T_GREYSKY) + debugf("\tItem %d in slot %d\n"); + + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t tile = VORT_GetTileAtPos(vMap, x, y, 0); + if (tile == K1_T_POGOSTICK || + (tile >= K1_T_JOYSTICK && tile < K1_T_JOYSTICK+4) || + (tile >= K1_T_BATTERY && tile < K1_T_EVERCLEAR+4) || + (tile == K1_T_EXITSIGN1 || tile == K1_T_EXITSIGN2)) + { + if (!slot--) + { + // If it's the exit sign... + if (tile == K1_T_EXITSIGN1 || tile == K1_T_EXITSIGN2) + { + y++; + uint16_t underTile = VORT_GetTileAtPos(vMap, x, y, 0); + // Check that there's a free space below the exit sign. + if (underTile != K1_T_GREYSKY) + { + y--; + slot++; + continue; + } + } + VORT_SetTileAtPos(vMap, x, y, 0, item); + } + } + } + } +} + +int mapLocations[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16}; + +void K1_ShuffleLevelEntries(VorticonsMap *worldMap) +{ + + PermuteArray(mapLocations, 16); + + for (int y = 0; y < worldMap->h; ++y) + { + for (int x = 0; x < worldMap->w; ++x) + { + uint16_t entry = VORT_GetTileAtPos(worldMap, x, y, 1); + if (!entry) + continue; + int level = entry & 0x7F; + if (level > 16) + continue; + debugf("Changing level %d (entry %x) ->", level, entry); + level = mapLocations[level-1]; + entry = (entry & ~0x7F) | level; + debugf(" level %d (entry %x)\n", level, entry); + VORT_SetTileAtPos(worldMap, x, y, 1, entry); + } + } +} + +// Enemy heights in tiles. We need to adjust enemy positions based on this. +int enemyHeights[5] = {2, 2, 3, 1, 2}; + +void K1_ShuffleEnemies(VorticonsMap *vMap) +{ + int totalEnemies = 0; + int enemyCount[5] = {0}; + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t sprite = VORT_GetTileAtPos(vMap, x, y, 1); + if (sprite >= 1 && sprite <= 5) + { + enemyCount[sprite-1]++; + totalEnemies++; + } + } + } + + + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t sprite = VORT_GetTileAtPos(vMap, x, y, 1); + if (sprite >= 1 && sprite <= 5) + { + int enemyIdx = rand() % totalEnemies; + int enemyType = 0; + while (enemyIdx > enemyCount[enemyType]) + { + enemyIdx -= enemyCount[enemyType++]; + } + int oldEnemyType = VORT_GetTileAtPos(vMap, x, y, 1) - 1; + VORT_SetTileAtPos(vMap, x, y, 1, 0); + // Avoid having the enemy stuck in the map. + // Note that we only raise enemies off the ground, otherwise + // we'd end up processing the enemy twice (and dividing by 0 as + // a result) + int ny = y; + if (enemyHeights[enemyType] > enemyHeights[oldEnemyType]) + { + ny -= enemyHeights[enemyType] - enemyHeights[oldEnemyType]; + debugf("Enemy %d (%d,%d) of height %d <-=-> Enemy %d (%d, %d) (height %d)\n", oldEnemyType, x, y, enemyHeights[oldEnemyType], enemyType, x, ny, enemyHeights[enemyType]); + } + VORT_SetTileAtPos(vMap, x, ny, 1, 1 + enemyType); + enemyCount[enemyType]--; + totalEnemies--; + } + } + } +} + +void K1_ShuffleLollies(VorticonsMap *vMap) +{ + int totalLollies = 0; + int lollyCount[5] = {0}; + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t tile = VORT_GetTileAtPos(vMap, x, y, 0); + if (tile >= 201 && tile <= 205) + { + lollyCount[tile-201]++; + totalLollies++; + } + } + } + + + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t tile = VORT_GetTileAtPos(vMap, x, y, 0); + if (tile >= 201 && tile <= 205) + { + int lollyIdx = rand() % totalLollies; + int lollyType = 0; + while (lollyIdx > lollyCount[lollyType]) + { + lollyIdx -= lollyCount[lollyType++]; + } + VORT_SetTileAtPos(vMap, x, y, 0, 201 + lollyType); + lollyCount[lollyType]--; + totalLollies--; + } + } + } +} + +void K1_MungeBlockColours(VorticonsMap *vMap) +{ + int blockMask = rand()&3; + + int blockToJumpThruMatrix[] = {1,3,0,2}; + int blockToJumpThruInvert[] = {2,0,3,1}; + + debugf("MungeBlockColours: mask = %d\n", blockMask); + + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t tile = VORT_GetTileAtPos(vMap, x, y, 0); + // Solid blocks + if (tile >= 331 && tile <= 334) + { + tile = ((tile - 331) ^ blockMask) + 331; + VORT_SetTileAtPos(vMap, x, y, 0, tile); + } + // Jump-thru blocks + if ((tile >= 178 && tile <= 181)) + { + int colour = blockToJumpThruInvert[tile - 178]; + colour ^= blockMask; + tile = 178 + blockToJumpThruMatrix[colour]; + VORT_SetTileAtPos(vMap, x, y, 0, tile); + } + } + } +} + +void K1_MungeKeys(VorticonsMap *vMap) +{ + int keyMask = rand()&3; + + for (int y = 0; y < vMap->h; ++y) + { + for (int x = 0; x < vMap->w; ++x) + { + uint16_t tile = VORT_GetTileAtPos(vMap, x, y, 0); + // Keys + if (tile >= 190 && tile <= 193) + { + tile = ((tile - 190) ^ keyMask) + 190; + VORT_SetTileAtPos(vMap, x, y, 0, tile); + } + // Doors + if ((tile >= 173 && tile <= 174) || (tile >= 195 && tile <= 200)) + { + int topOrBot = 1 - (tile & 1); + int colour = (tile <= 174)?0:((tile - 193) >> 1); + colour ^= keyMask; + tile = ((colour == 0)?(173):(195+(colour-1)*2)) + topOrBot; + VORT_SetTileAtPos(vMap, x, y, 0, tile); + } + } + } +} + +// We remove the GARG scream for now, as there's just not enough +// room to fit it in the existing message space. +// A future version can patch it in elsewhere. +int hintLevels[] = {2, 6, 9, 10, 12, 15, -1}; +int currentHint = 0; + +// Level names +const char *levelNames[] = { + NULL, + "Border Town", + "1st Red Rock Shrine", + "Treasury", + "Capital City", + "Pogo Shrine", + "2nd Red Rock Shrine", + "Emerald City", + "Ice City", + "3rd Red Rock Shrine", + "1st Ice Shrine", + "4th Red Rock Shrine", + "5th Red Rock Shrine", + "Red Maze City", + "Secret City", + "2nd Ice Shrine", + "Commander's Castle" +}; + +int hintTiles[] = {K1_T_POGOSTICK, K1_T_JOYSTICK, K1_T_BATTERY, K1_T_VACUUM, K1_T_EVERCLEAR, -1}; +const char *hintTileNames[] = {"Pogo Stick", "Joystick", "Battery", "Vacuum", "Everclear", 0}; + +bool WriteHintHeaderToPatch(FILE *f) +{ + if (hintLevels[currentHint] == -1) + return false; + + if (hintLevels[currentHint] != 11) + fprintf(f, "%%level.hint %d\nA Yorpy Mind\nThought Bellows:\n", hintLevels[currentHint]); + else + fprintf(f, "%%level.hint %d\nA Gargish Scream\nEchoes In Your\nHead:\n", hintLevels[currentHint]); + + currentHint++; + return true; +} + +void WriteTileHintToPatch(FILE *f, int tile, int level) +{ + int hintId = 0; + while (tile != hintTiles[hintId] && hintTiles[hintId] != -1) + hintId++; + if (hintTiles[hintId] == -1) + return; + + if (!WriteHintHeaderToPatch(f)) + return; + + if (opt_useLevelNames) + fprintf(f, "The %s is\nfound in the\n%s\n\n", hintTileNames[hintId], levelNames[level]); + else + fprintf(f, "The %s is\nfound in level %d\n\n", hintTileNames[hintId], level); +} + +void WriteMapHintToPatch(FILE *f, int oldMap, int newMap) +{ + if (oldMap == newMap) + return; + if (!WriteHintHeaderToPatch(f)) + return; + + if (opt_useLevelNames) + fprintf(f, "%s\nrests where the\n%s\nonce was...\n\n", levelNames[newMap], levelNames[oldMap]); + else + fprintf(f, "Level %d\nrests where\nlevel %d once\nwas...\n\n", newMap, oldMap); +} + +void WritePatchHeader(FILE *f) +{ + fprintf(f, "%%ext ck1\n"); + fprintf(f, "%%version 1.31\n\n"); + + // Load levels from RNDLV??.CK1 + fprintf(f, "%%patch $14D9C \"RNDLV\"\n"); + fprintf(f, "%%patch $14DA3 \"RNDLV\"\n\n"); + + // Show the seed in the status screen + fprintf(f, "%%patch $14E60 \" RANDOM SEED \"\n"); + fprintf(f, "%%patch $0FA7\t$B8 $%04XW\n", (opt_seed >> 16) & 0xFFFF); + fprintf(f, "\t\t$BA $%04XW\n", opt_seed & 0xFFFF); + fprintf(f, "\t\t$90 $90 $90 $90 $90 $90\n\n"); + +} + +void WritePatchFooter(FILE *f) +{ + fprintf(f, "%%end\n"); +} + +void PrintBanner() +{ + printf("Keen 1 Randomiser\n"); + printf("\tv1.00\n"); + printf("\tBy David Gow \n\n"); +} + +void PrintOptions() +{ + printf("Available options:\n"); + printf("\t/SEED -- set the random number seed.\n"); + printf("\t/? -- show this message\n"); + printf("\t/DEBUG -- show debug messages.\n"); + printf("\t/NOLEVELNAMES -- use level numbers instead of names in hints.\n"); +} + +void ParseOptions(int argc, char **argv) +{ + for (int i = 1; i < argc; ++i) + { + if (!strcasecmp(argv[i], "/seed")) + { + opt_seed = atoi(argv[++i]); + } + else if (!strcasecmp(argv[i], "/nolevelnames")) + { + opt_useLevelNames = false; + } + else if (!strcasecmp(argv[i], "/debug")) + { + opt_debug = true; + } + else if (!strcasecmp(argv[i], "/?")) + { + PrintOptions(); + exit(0); + } + else + { + printf("Unknown argument \"%s\"\n", argv[i]); + PrintOptions(); + exit(1); + } + } +} + +int main(int argc, char **argv) +{ + PrintBanner(); + // Default random seed. + srand(time(0)); + // Clamp to 16-bit for ease of readability, and because I'm + // not sure srand() actually accepts a 32-bit seed on all archs. + opt_seed = rand() & 0xFFFF; + ParseOptions(argc, argv); + printf("Random seed: %d\n", opt_seed); + srand(opt_seed); + int curItemSlot = 0; + + + PermuteArray(itemsPerSlot, 20); + + FILE *patchFile = fopen("RNDKEEN1.PAT", "w"); + WritePatchHeader(patchFile); + + // World map + FILE *f = fopen("LEVEL80.CK1", "rb"); + VorticonsMap wm = VORT_LoadMap(f); + fclose(f); + K1_ShuffleLevelEntries(&wm); + f = fopen("RNDLV80.CK1", "wb"); + VORT_SaveMap(&wm, f); + fclose(f); + + for (int level = 1; level <= 16; level++) + { + char fname[16]; + sprintf(fname, "LEVEL%02d.CK1", level); + debugf("Processing level %d (%s)\n", level, fname); + FILE *f = fopen(fname, "rb"); + VorticonsMap vm = VORT_LoadMap(f); + fclose(f); + K1_MungeKeys(&vm); + K1_MungeBlockColours(&vm); + K1_ShuffleLollies(&vm); + K1_ShuffleEnemies(&vm); + while (slotsPerLevel[level-1]--) + { + WriteTileHintToPatch(patchFile, itemsPerSlot[curItemSlot], level); + if (itemsPerSlot[curItemSlot] != K1_T_GREYSKY) + WriteMapHintToPatch(patchFile, level, mapLocations[level-1]); + K1_SetSpecialItem(&vm, itemsPerSlot[curItemSlot++], slotsPerLevel[level-1]); + } + sprintf(fname, "RNDLV%02d.CK1", level); + f = fopen(fname, "wb"); + VORT_SaveMap(&vm, f); + fclose(f); + } + + WritePatchFooter(patchFile); + fclose(patchFile); + return 0; +}