Skip to content

SHP — props ("shop" files) ​

Prerequisite: The DFile container format and The image codec.

A SHP file holds props — the images drawn on top of a SET's background: doors, items you pick up, buttons you press, the watch and menu button in the UI band. If it moves, appears, disappears, or reacts to a click, it is almost certainly a prop.

Reference implementation: engine/src/df/shp.ts (decoding) and engine/src/runtime/props.ts (runtime).

Despite the name, SHP is not "ship." CyberFlix called these shop files. (And SET files, confusingly, are the rooms.)

The structure: groups → states → frames ​

A prop is organised in three levels:

  • A group is one prop, addressed by name ("door", "watch", "bag").
  • A state is a named look or animation for that prop. The ship-wide door prop, for instance, has 135 route-named states like "b59-hallb" — one per doorway it can represent.
  • Frames are the individual images of a state; playing them in sequence is the animation.

A state's play list — a step count and that many 1-based steps, each naming a group of frames — is what plays. In DreamFactory 4 (TI.EXE, and Timelapse's TL.EXE, which carries the same routine) a state with more than one step goes round for as long as the prop stands in it. Every displayed frame the engine adds one to every prop's step and wraps it to 0 at the state's step count before drawing:

0x418beb: inc  ax                    ; step + 1
0x418bed: mov  word [ebp + 0x20], ax
0x418bf1: cmp  word [ebp + 0x22], ax ; the state's step count (its +0x70)
0x418bf5: jg   0x418bfd
0x418bf7: mov  word [ebp + 0x20], 0  ; ...wrapped

Nothing holds. A door opens and stays open because its script then puts it in a one-step state — for count = 1 to 6 / forceupdate () and propview (me, "idleopen"), or a makeloop to an idle handler timed to the list's length — and a state left standing keeps playing: the gramophone's crank in the trunk, which no script ever names (the shop opener starts each prop in its first state), and the Turkish bath's running water. One knock-on is visible in the original: the deck map's open keeps rolling while the map flat is up, so exitmap's transfromflat () brings the room back with the map part-rolled, and only then does close () roll it up from fully open.

Dust's DF.EXE goes round the same way, with one difference: it takes the step in the prop's draw (0x41eab0 stores step + 1 and wraps it at the state's +0x70, 0x41eb67), so a prop that is hidden, or a world prop outside the open set, is not drawn and does not move (0x41e770, 0x41e7b5). Its scripts stop a view the same way TAOOT's do — a makeloop timed to the list's length that names the next state (FIGHT.PRP's drop and stop after 4, HUB.SET's 10-step fade and its idle after 10).

Those three levels are pointers, and pointers are what a block map can show. CUFF.SHP — the cufflink bag, its three props and their 46 pictures — with the group table hovered, so every container it addresses lights up:

Loading the map of cuff.shp…

Container 0 names two things ​

The header's tail is a script ref immediately followed by the file's own name — the same idiom a group container uses one level down (its script at +38, its name at +42):

OffsetField
2340i32 container ref: the shop's main script
2344the shop's own name ("house", "inven")
2360i32 group count
2364the group table, 16 bytes per entry

Offset 20 is not the main script: it reads 0 in 149 of the 207 shipped .shp/.prp and 1 in the other 58, never a pointer (compare #291 in set-v1.ts). Every shop's main script in the corpus is container 1, so no file tells the two readings apart; the offset is taken from both engines — TI.EXE's shop opener 0x415780:

0x41583b: lea  ecx, [ebx + 0x928]   ; the ref name
0x415843: call 0x435740             ;   -> shop->name
0x41584b: mov  ecx, [ebx + 0x924]   ; the main script
0x415853: mov  [esi + 8], ecx
0x41586b: call 0x4385f0             ; ...and open that container

and DF.EXE's 0x4135c0 carries the same two instructions at the same two offsets, so the field is the same in DreamFactory 1 and 4 (#325). A CST header's tail is identical — opencastfile's parser 0x40dac0 reads [ebx+0x924] and [ebx+0x928] the same way — which is what identified the cast's main script too.

Transparency: the prop only paints part of the screen ​

Unlike a SET background (which fills its rectangle), a prop is a cut-out — a door occupies only a door-shaped region, the rest is transparent. So SHP frames use the transparent variant of the image codec: alongside the pixels there is an opaque mask marking which pixels are real and which are see-through.

Each frame also stores its own draw offset in the header — where the cut-out sits relative to the prop's anchor point — and, as with hotspots, the offset is stored Y before X.

Colour: props borrow the room's palette ​

A prop is decoded palette-independently: the loader keeps it as indexed pixels plus the opaque mask, and does not bake in colours. The prop is colourised only at composite time, through the currently active SET's palette (a shared colour table — the clut/mixclut commands exist to manage it). That is how the same door art looks right in every room it appears in.

Placing a prop on screen ​

There are two placement worlds, and which one a prop is in decides how it is drawn:

Screen-space props (UI, inventory, cards) ​

Most props are positioned directly in screen pixels. The rule (validated against dragged inventory items, blackjack cards, and UI-band buttons) is:

screenPosition = propxy − storedFrameOffset

with a default anchor at (256, 192) — the centre of the 512×384 screen — for Titanic and Dust, whose never-placed props store offsets centred on it. Timelapse's store where they stand on its 640×480 screen, so its default is (0, 0) (propAnchor in chrome/src/games.ts). The propxy command moves a prop in this screen space. UI-band props live below y=264 (e.g. propxy(me, 256, 324)).

World-space props (things sitting in the 3D room) ​

A prop placed with propxyz has a 3D world coordinate instead. It is projected into the current view using the camera math from the SET doc — so an item on a bed appears at the right spot and shrinks with distance. propscale and propzclip tune its size and its visibility cut-off. Calling propxy on such a prop returns it to screen space — which is exactly what "picking it up" does.

A world prop is bound to its set with propset(name, set), so it only draws in the room it belongs to. The bag on the C73 bed is such a prop: projected, clickable, and picked up into the inventory.

How props behave: they have their own scripts ​

Each prop group has a script whose code handlers respond to events (mousedown, setcursor, initprop, …), joining the same event chain as everything else (see scripting). Two runtime facts:

  • Prop clicks are hit-tested front-to-back and pixel-accurately (using the opaque mask), so you can only click the visible part of a prop, and the nearest prop wins.
  • A prop script's unqualified calls resolve through its "shop main" script — the file's own top-level script. The bag's mousedown calls watchidle() / mapidle(), which are defined in the shop main, not in the bag; without that parent link the calls fail and the click silently dies.

Boot-loaded, session-scoped props ​

Some SHP files are loaded once at startup by the boot and live for the whole session, not per-set: house.shp (44 ship-wide props including the 135-state door), inven.shp (the inventory). That is why sendtoprop("door", …) from any room reaches a real, already-loaded prop. See BOOTFILE.

  • tools/dumpshp.ts — decode a SHP and export its states/frames as images.
  • the shop editor (/editors/shops.html) — the same structure in a browser page, with the frames drawn where the engine would put them, animation playback, and the names/offsets/degrees editable.

Next: click-through close-ups and cutscenes — MOV.

Docs licensed GPL-3.0 (the decoder is ported from DFET). Game data © CyberFlix — not included.