Everything on this page comes from two sources: the addresses in
cdi_merlin.stb and the offsets of the printable strings
in cdi_merlin. Intersecting them recovers the structure of a game whose
source has never been published.
The whole of Merlin's Apprentice is 83 symbols in one section, and reading them in address order is reading the program's layout:
0x00054 _cstart _stkcheck stacksiz freemem trapinit
0x002ea main
0x0073c InitChdInfo InitIconInfo InitBGInfo
0x00822 ChallDirEngine
0x01258 SOCE_InitObjects SOCE_InitAnimTyp SoundChallEngine
0x0210c FragChallEngine
0x04832 ArcadeChallEngine
0x066b0 InitAnimsStruct CodeChallEngine
0x080fa MorphChallEngine
0x08ca0 AlignChallEngine
0x09cb6 JumbleChallEngine
0x0aa2e PotionChallEngine
0x0c6de POCE_InitMain POCE_InitReact POCE_InitLevel
0x0c820 BOSS_Register* BOSS_Init BOSS_Execute BOSS_Get* BOSS_Set*
0x0dca4 OptionDirEngine
0x0dd00 BOSS_NVInit BOSS_NVPack BOSS_NVUnPack pack_bytes unpack_bytes
0x0e422 InitMainMenu InitGameToken InitDiffLevel InitAPList InitMenuBar
0x0e738 AllPlayEngine
0x0e92a DiffLevelScreen DL_*
0x0eeea GameTokenScreen GT_*
0x0fc5a MainMenuScreen MM_*
Eight *ChallEngine functions, a ChallDirEngine to choose between them, a
PotionChallEngine, and a BOSS_* block that runs the whole thing as a state
machine.
The disc's own abstract.txt promises "27 puzzles and 3 magic potions". The
executable contains exactly 30 identifiers ending in DD — the suffix the
FH_DD_Load / FH_DD_Save / FH_DD_Delete / FH_DD_Empty API uses for its
data records — and each cluster sits inside one engine's address range.
| string offset | engine (address range) | names |
|---|---|---|
0x018f6 |
SoundChallEngine (0x012fc–0x0210c) |
PondDD FlasksDD StalacDD |
0x023f0 |
FragChallEngine (0x0210c–0x04832) |
MirrorDD PlaqueDD OctagonDD TileDD |
0x04a0a |
ArcadeChallEngine (0x04832–0x066b0) |
SeedsDD LeavesDD BubblesDD SnowflakDD GemsDD DemonsDD |
0x06dac |
CodeChallEngine (0x066f6–0x080fa) |
GraveDD ParchDD TabletDD |
0x08496 |
MorphChallEngine (0x080fa–0x08ca0) |
WindowDD StarDD |
0x090a6 |
AlignChallEngine (0x08ca0–0x09cb6) |
PlanetDD DoorDD SphereDD |
0x0a164 |
JumbleChallEngine (0x09cb6–0x0aa2e) |
RavenDD LeafDD SnakeDD SkeltnDD SpiderDD QuartzDD |
0x0ac60 |
PotionChallEngine (0x0aa2e–0x0c6de) |
ForestDD LabratDD CavernDD |
3 + 4 + 6 + 3 + 2 + 3 + 6 = 27 puzzles, plus three potions named after the three levels — forest, laboratory, cavern — which are also the three level files on the disc. The abstract's numbers are exact, and every puzzle in the game now has a name and a type.
Two of the names are truncated to eight characters plus the suffix:
SnowflakDD for Snowflake and SkeltnDD for Skeleton, the latter by
dropping vowels rather than by cutting. StalacDD and ParchDD are shortened
the same way, from Stalactite and Parchment.
Two engines have their own initialiser prefixes: SOCE_ for
SoundChallEngine (SOCE_InitObjects, SOCE_InitAnimTyp), POCE_ for
PotionChallEngine (POCE_InitMain, POCE_InitReact, POCE_InitLevel), and
ARCE_ for ArcadeChallEngine — which appears only in the globals
(ARCE_NumSpritesLaunched, ARCE_NumSpritesLeft). The three engines that got
their own namespace are the three with the most moving parts.
ArcadeChallEngine is also the only one with sprite bookkeeping: NumSprites,
SpritesUsed, NumPaths, PathsUsed beside its two counters. Six of the 27
puzzles are action sequences.
Thirty-eight symbols share the BOSS_ prefix and they run everything above the
individual puzzle:
BOSS_Init BOSS_Execute
BOSS_Execute_Options BOSS_Execute_Dir BOSS_Execute_Chall
BOSS_Execute_Presentation BOSS_Play_Help
BOSS_RegisterStateMachine BOSS_RegisterMainMenu BOSS_RegisterTokenScreen
BOSS_RegisterDiffScreen BOSS_RegisterAllPlayScreen BOSS_RegisterMenuBar
with a paired accessor for every piece of game state — BOSS_GetDiffLevel /
BOSS_SetDiffLevel, BOSS_GetChallComplete / BOSS_SetChallComplete,
BOSS_GetChallVariation / BOSS_SetChallVariation, BOSS_GetCurrChallType /
BOSS_SetCurrChallType, BOSS_GetUndoAvailable / BOSS_SetUndoAvailable —
and three read-only ones: BOSS_GetLevelKey, BOSS_GetCompleteArray,
BOSS_GetMenuBar.
The state machine itself is table-driven. BOSS_StateTable,
BOSS_NextStateTable, CurrStateTableEntry, NextState and Attribute are
globals, and the tables sit in the initialised data at StateTable (−27,334)
and NextStateTable (−26,302) — 1,032 bytes apart. Its diagnostics
are still in:
BOSS: CurrentState = %d
BOSS: Returned from CurrentState = %d
BOSS_Execute: Software Error occurred in state %d
BOSS_Execute: Illegal event %d returned from state %d
BOSS_EXECUTE: Invalid Help ID
BOSS_GetCheatMode 0x0d260
BOSS_SetCheatMode 0x0d328
BOSS_CheatMode global at -25,186
A get, a set and a global, built with exactly the same pair of verbs as
BOSS_GetDiffLevel / BOSS_SetDiffLevel — so it is ordinary game state, read
and written through the same accessor discipline as the difficulty level.
What sets it is not established here. Soccer shipped a literal CHEAT MODE ON
string; Merlin has no such string, and nothing in the printable text names the
flag. The three functions and the global are the whole of the evidence, and
they are enough to say the mode exists.
The save format is packed by hand into NVRAM.
FH_NV_Load FH_NV_Save FH_NV_Delete the device layer
BOSS_NVInit BOSS_NVPack BOSS_NVUnPack the format
pack_bytes unpack_bytes the bit packer
/nvr/csd is the device path, appearing twice in the binary, and the save is
named MerlinsApprentice — the string at offset 0x2d7, which the global
BOSS_NVSaveName points at.
The layout is fully described by the globals, which is unusual and useful. Each field has an index and the packer has a width:
NV_VersionIndex NV_VersionSize NV_NumDifficultyBits
NV_DifficultyIndex NV_TokenSize NV_NumVariationBits
NV_VariationIndex NV_ChallVarSize
NV_CompleteIndex NV_TokenStateSize
NV_LastDirStateIndex
NV_ResumeStateIndex
NV_TokenStateIndex
NV_FirstTokenIndex
NV_NumBytes NV_SaveData
So a save record holds a version, a difficulty, a per-puzzle variation, a per-puzzle completion flag, the last directory state, a resume state, and a block of token state — bit-packed, with the field widths held in variables rather than baked into the code. The bits-per-variable setting is validated at startup, and the check has a shipped diagnostic that gives the whole design away:
BOSS_Init: NumBitsPerVar = %d. Should be 1,2,4, or 8. -- using 8.
The failure path also survives:
BOSS: NVRAM is out of date
BOSS: NVRAM is missing or invalid, removing
BOSS_NVVersion and NVVersion are the version constants the loader checks,
and a mismatch deletes the save. pack: number of bits to pack into = %d and pack: number of bits to unpack from = %d are the packer's own complaints.
GT_ConfirmErase and EraseMenuBar are the user-facing side of
FH_NV_Delete.
Three screens have their own symbol families, all built the same way — an
Init, an Update, a Term:
| screen | functions |
|---|---|
MainMenuScreen |
MM_InitScreen MM_UpdateScreen MM_TermScreen |
GameTokenScreen |
GT_InitScreen GT_UpdateScreen GT_DisplayText GT_SelectButton GT_ConfirmErase GT_TermScreen |
DiffLevelScreen |
DL_InitScreen DL_UpdateScreen DL_SetDifficulty DL_TermScreen |
plus AllPlayEngine, BOSS_RegisterAllPlayScreen, BOSS_AllPlayState,
InitAPList and an AllPlayState global — a mode in which every puzzle is
available, distinct from the normal progression. Three
menu bars are registered: ChallDirMenuBar, IndChallMenuBar,
DepChallMenuBar — independent and dependent challenges get different
toolbars, which is the game's progression rule made visible in a symbol name.
The generic screen machinery underneath is FH_SM_*: InitScreen,
DrawScreen, StartCycle, UpdateButtons, and eight
SetButtonEnable/GetButtonEnable-style accessors for enable, state, instance
and hot spot. Hit testing is SM_ButtonDetected and
SM_ComputeDetPicLineAddrs — a detection picture, a second bitmap whose
pixel value at the cursor gives the button index. FH_SS_InRect and
FH_SS_RectOverLap do the rectangle arithmetic.
PresentationEngine and BOSS_Execute_Presentation play the 89 animations
indexed by tcanim.toc through the Traffic Cop.
FH_GU_ShuffleSeq, FH_GU_SwapAddresses and FH_GU_Rotate are the game
utilities that make "each puzzle has multiple solutions" true — a shuffler, a
swapper and a rotator, which between them are most of what the 27 puzzles do to
their pieces.
WaitingToDisplayStar and all_challenges_complete are the two globals that
close the loop.