Skip to content

Latest commit

 

History

History
221 lines (179 loc) · 9.11 KB

File metadata and controls

221 lines (179 loc) · 9.11 KB

The game

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.

Section 0, in address order

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.

Eight engines, 27 puzzles, three potions

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.

BOSS — the shell

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

The cheat flag

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.

Saving

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.

Screens

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.

Presentation

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.