Click here to download the file.
/*
* MIT License
*
* Based on Project Blue Dream
* Originally reverse Engineered by Johnny L. de Alba (Arkonviox), 2020
* Additional reverse engineering by Moduvator, 2026
* Copyright (c) 2026. Novotrade International and Sega Games Co., Ltd.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Project SECCO
* Ecco 2 Password System
*
* Game: Ecco 2: The Tides of Time, US/Europe
* Platform: Sega Genesis/Mega Drive/Sega CD
*
* Summary:
* Reverse engineering of original game's password system
* Line by line approximation in C of the original 68000 code
*
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
// XOR Salt for Ecco 2
#define ECCO2_US_SALT 0x15b63c7a
#define CRCPOS 3 // Checksum character position in password
#define PWD_SIZE 8 // Password size
#define ASCII_OFFSET 65 // Offset to turn codes into ASCII
// Stored in Easy Flags
#define EASY_FLAG 0x01
#define FORCEEASY_FLAG 0x80
// Stored in Skill Points
#define FORCEHARD_FLAG 0x80
#define DEATHS_MASK 0x0F
#define POINTS_MASK 0x1F
#define PFIELD_MASK 0x7F
#define DEATHS_MAX 0x0F
// Packed password masks
#define PWDMASK_GLOBES 0x1F
#define PWDMASK_STAGEID 0x3F
#define PWDMASK_POINTS 0x0F
#define PWDMASK_TIME 0xFFFE
#define PWDMASK_HARD 0x10000
#define PWDMASK_EASY 0x8000
// Packed password offsets
#define PWDOFF_STAGEID 0x05
#define PWDOFF_POINTS 0x0B
#define PWDOFF_EASYF 0x0F
#define PWDOFF_HARDF 0x10
#define PWDOFF_TIME 0x10
// Maximum valid Stage ID
#define STAGEID_MAX 49
/* BEGIN Password Related Global Variables */
// Asterite's globes. Used as bitfield
/*
0b00000000000000000000111111111111
12 globe pairs, etc
*/
uint32_t gAsteriteGlobesLeft, gAsteriteGlobesRight;
uint16_t gGameTimer; // Frames rendered / 1024
uint8_t gSkillPoints; // Deaths or skill points plus Force Hard flag
uint8_t gEasyFlags; // Stores Force Easy and Easy Mode flags
int8_t gStageID; // Current Stage ID
uint8_t gCheatEn; // Cheats Enabled flag
/* END Password Related Global Variables */
// RAM 0xFFD35E Asterite related, function TBD
uint16_t gAsteriteUnk0[49];
// LUT for obfuscating password checksum (ROM 0x6E08)
uint8_t encodeLUT[] = {
0x16, 0x08, 0x01, 0x0F, 0x02, 0x07, 0x14, 0x09,
0x0D, 0x19, 0x17, 0x0A, 0x00, 0x0B, 0x04, 0x0C,
0x06, 0x0E, 0x05, 0x10, 0x13, 0x11, 0x03, 0x15,
0x12, 0x18
};
// LUT for de-obfuscating password checksum (0x6E22)
uint8_t decodeLUT[] = {
0x0C, 0x02, 0x04, 0x16, 0x0E, 0x12, 0x10, 0x05,
0x01, 0x07, 0x0B, 0x0D, 0x0F, 0x08, 0x11, 0x03,
0x13, 0x15, 0x18, 0x14, 0x06, 0x17, 0x00, 0x0A,
0x19, 0x09
};
// Asterite globes audit table (ROM 0x6E3C)
uint16_t gAsteriteTable[STAGEID_MAX] = {
0x01,0x0e,0x03,0x01,0x17,0x17,0x17,
0x17,0x01,0x01,0x01,0x01,0x01,0x01,
0x18,0x01,0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x18,0x18,0x18,0x18,
0x17,0x17,0x17,0x17,0x18,0x18,0x18,
0x18,0x18,0x18,0x18,0x0d,0x18,0x18
};
/*
ROM 0xD1A1A
Scrambles a given password via bit flipping
Arguments:
Stack: Raw password
Returns:
D0: Scrambled password
*/
uint32_t scramblePassword(uint32_t rawPassword) {
uint32_t hmask = 0x80000000;
uint32_t lmask = 0x40000000;
uint32_t key = 0x00000001;
uint32_t result = 0;
for (int i = 16; i > 0; i--) {
if ((rawPassword & hmask) != 0) {
result |= key;
}
if ((rawPassword & lmask) != 0) {
result |= hmask;
}
hmask = hmask >> 2;
lmask = lmask >> 2;
key = key << 2;
}
return result;
}
/*
ROM 0xD1A6C
Unscrambles a given password via bit flipping
Arguments:
Stack: Scrambled password
Returns:
D0: Unscrambled password
*/
uint32_t unscramblePassword(uint32_t rawPassword) {
uint32_t hmask = 0x80000000;
uint32_t lmask = 0x40000000;
uint32_t key = 0x00000001;
uint32_t result = 0;
for (int i = 16; i > 0; i--) {
if ((rawPassword & key) != 0) {
result |= hmask;
}
if ((rawPassword & hmask) != 0) {
result |= lmask;
}
hmask = hmask >> 2;
lmask = lmask >> 2;
key = key << 2;
}
return result;
}
/*
ROM 0xB0BA4
Counts number of Asterite globe pairs and returns it
Arguments:
NONE
Returns:
D0: Number of Asterite globe pairs or -1 if helixes mismatch
*/
int countAsteriteGlobePairs(void) {
int retval = -1;
uint32_t MASK = 0x80000000;
int16_t globeCount = 0;
if (gAsteriteGlobesLeft == gAsteriteGlobesRight) {
for (int i = 32; i > 0; i--) {
if ((gAsteriteGlobesLeft & MASK) != 0) {
globeCount++;
}
MASK = MASK >> 1;
}
retval = globeCount;
}
return retval;
}
/*
ROM 0xB0BD8
Sets global Asterite variables to number of globes given
Arguments:
Stack: Number of globe pairs
Returns:
NONE
*/
void setAsteriteGlobes(uint8_t numPairs) {
uint32_t MASK = 1;
gAsteriteGlobesLeft = 0;
for (int i = numPairs; i > 0; i--) {
gAsteriteGlobesLeft |= MASK;
MASK = MASK << 1;
}
gAsteriteGlobesRight = gAsteriteGlobesLeft;
return;
}
/*
ROM 0xB0BFC
Reinitialises Asterite to 1 globe pair
Also clears 49 words in RAM at 0xFFD35E
Not yet known what for but 49 is same as max stage ID
Arguments:
NONE
Returns:
NONE
*/
void clearAsteriteGlobes(void) {
// Function also clears another RAM location as follows:
uint16_t *ptAsterite = gAsteriteUnk0;
for (int i = 0; i < 49; i++) {
*ptAsterite = 0;
ptAsterite++;
}
gAsteriteGlobesLeft = gAsteriteGlobesRight = 1;
}
/*
ROM 0xD1BE6
Tests stage id and globe pairs from password against table
Said table also used to init Asterite when that stage is loaded
Arguments:
Stack: Stage ID, Globe Pairs
Returns:
D0: 1 if match, 0 if not match
*/
bool asteriteGlobeAudit(uint8_t stageID, uint16_t globePairs) {
switch (stageID) {
case 8: // Selection scr
case 26: // Vortex arrived
case 47: // The Pod
case 48: // Tmachine
return false;
break;
}
uint16_t expectedGlobes = gAsteriteTable[stageID];
return (expectedGlobes == globePairs);
}
/*
ROM 0xD1C40
Decodes a password supplied as a string
Sets global variables accordingly and returns Stage ID
Called by the Password Screen
Arguments:
Stack: Pointer to password string from the password screen
Returns:
D0: Stage ID or -1 on error
*/
int passwordChecker(char *srcString) {
int i;
uint8_t charCode;
uint8_t globePairs, stageID, skillPoints, forcedEasy, forcedHard;
uint16_t computedChecksum = 0;
uint16_t givenChecksum = 0;
uint32_t decodedPass = 0;
uint32_t multiplier = 1;
/* Build the password computedChecksum before everything else */
for (i = 0; i < PWD_SIZE; i++) {
if (i == CRCPOS) i++; // Skip 4th character, this is the computedChecksum
charCode = srcString[i];
computedChecksum += charCode;
}
computedChecksum = computedChecksum % 26;
charCode = srcString[CRCPOS];
givenChecksum = decodeLUT[charCode];
if (computedChecksum == givenChecksum) {
/* Checksum is valid, proceed to decode the password */
// Rebuild scrambled password from character codes
for (i = 0; i < PWD_SIZE; i++) {
if (i == CRCPOS) i++; // Skip 4th character
charCode = srcString[i];
decodedPass += (charCode * multiplier);
multiplier *= 26;
}
// Unscramble the password into its raw form now
decodedPass = unscramblePassword(decodedPass ^ ECCO2_US_SALT);
// Extract data from password into temp variables
globePairs = decodedPass & PWDMASK_GLOBES;
stageID = (decodedPass >> PWDOFF_STAGEID) & PWDMASK_STAGEID;
skillPoints = (decodedPass >> PWDOFF_POINTS) & PWDMASK_POINTS;
forcedEasy = (decodedPass >> PWDOFF_EASYF) & 1;
forcedHard = (decodedPass >> PWDOFF_HARDF) & 1;
if (stageID < STAGEID_MAX) {
// Check if Cheat Mode was enabled
if ((forcedHard == 0) || (forcedEasy == 0)) {
gCheatEn = 0;
} else {
gCheatEn = 1;
forcedEasy = 0;
gSkillPoints = 0;
gEasyFlags = 0;
forcedHard = 0;
_playSFX(25); // Emit the Wop of Shame
}
// Check stage ID against target Asterite globe pairs
if (asteriteGlobeAudit(stageID, globePairs)) {
if (forcedEasy != 0) {
// Set Forced Easy Mode
gEasyFlags = gEasyFlags | FORCEEASY_FLAG;
}
if (forcedHard == 0) {
gSkillPoints = skillPoints;
if (skillPoints > 3) {
gEasyFlags = gEasyFlags | EASY_FLAG;
}
} else {
gSkillPoints = skillPoints | FORCEHARD_FLAG;
}
gGameTimer = (decodedPass >> PWDOFF_TIME) & PWDMASK_TIME;
setAsteriteGlobes(globePairs);
return stageID;
}
clearAsteriteGlobes();
gGameTimer = 0;
gSkillPoints = 0x10;
gEasyFlags = 0;
_screenFadeOut();
}
}
return -1;
}
/*
ROM 0xD1ABE
Collects global status variables and packs them into a password
Called from level title screen subroutine
Arguments:
Stack: Pointer to password string
Returns:
D0: 0 on failure, 1 on success
*/
bool generatePassword(char *dstString) {
uint8_t i;
uint8_t skillPoints, forceEasy, forceHard, globesCount;
uint8_t checksum = 0;
char curChar;
uint32_t encodedPassword;
if (gStageID < 1)
return false;
globesCount = countAsteriteGlobePairs();
skillPoints = gSkillPoints & PFIELD_MASK;
if (skillPoints > DEATHS_MAX) skillPoints = DEATHS_MAX;
forceEasy = gEasyFlags >> 7;
if (gCheatEn != 0) forceEasy = 1;
forceHard = gSkillPoints >> 7;
if (gCheatEn != 0) forceHard = 1;
// Pack the data and send raw password to be scrambled
encodedPassword = scramblePassword (
((gGameTimer & PWDMASK_TIME ) << PWDOFF_TIME) |
(forceHard << PWDOFF_HARDF) |
(forceEasy << PWDOFF_EASYF) |
(skillPoints << PWDOFF_POINTS) |
((gStageID & PWDMASK_STAGEID) << PWDOFF_STAGEID) |
(globesCount & PWDMASK_GLOBES)
);
// XOR salt the password
encodedPassword ^= ECCO2_US_SALT;
// Turn password into character positions
for (i = 0; i < PWD_SIZE; i++) {
if (i == CRCPOS) i++; // Skip CRC character
curChar = encodedPassword % 26;
dstString[i] = curChar;
checksum += curChar;
encodedPassword /= 26;
}
checksum = checksum % 26;
dstString[CRCPOS] = encodeLUT[checksum];
dstString[PWD_SIZE] = '\0';
// Now turn character positions into ASCII
for (i = 0; i < PWD_SIZE; i++) {
curChar = dstString[i] + ASCII_OFFSET;
dstString[i] = curChar;
}
return true;
}
/*
Wrapper for passwordChecker(). Password screen already supplies string
as character positions. Here we need to convert it from ASCII first.
*/
uint32_t decodePassword(char *srcString) {
// Turn string into character codes
for (int i = 0; i < PWD_SIZE; i++) {
srcString[i] -= ASCII_OFFSET;
}
return passwordChecker(srcString);
}
int main(void) {
/*
*** DECODE A SAMPLE PASSWORD ***
Globe Pairs: 1
Stage ID: 17 - Fault Zone
Points/Deaths: 7
Force Easy: 0
Force Hard: 0
Time Elapsed: 0x1AA
*/
char samplePass[] = "STGDACCB"
decodePassword(samplePass);
// Generate a password for start of the game in Normal
char dstPwd[PWD_SIZE] = { 0 }; // Buffer to store password
setAsteriteGlobes(24); // Asterite has 24 pairs in Home Bay
gStageID = 14; // 14 - Home Bay
gEasyFlags |= EASY_FLAG; // Easy Mode 1, Force Easy 0
gSkillPoints = 12 & POINTS_MASK; // 12 skill points, Force Hard 0
gGameTimer = 0; // Game time 0
gCheatEn = 0; // No cheats
generatePassword(dstPwd);
puts(dstPwd);
return 0;
}