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;
}