Progress on documentation
@@ -27,7 +27,8 @@ Generate documentation following this exact structure:
|
||||
### 1. Header
|
||||
Format: `# Room ##: [Room Name]`
|
||||
|
||||
**Filename**: `kq4-##-human-readable-name.md`
|
||||
**Directory**: `rooms/kq4-##-human-readable-name/`
|
||||
**Filename inside directory**: `kq4-##-human-readable-name.md`
|
||||
|
||||
### 2. High-Level Summary
|
||||
A paragraph describing the room's purpose, key features, NPCs, and gameplay significance.
|
||||
@@ -187,21 +188,29 @@ Follow the format exactly:
|
||||
5. Scripts table
|
||||
6. Technical notes with state variables
|
||||
|
||||
### Step 7: Save File
|
||||
### Step 7: Generate Visual PNG
|
||||
|
||||
Save to: `rooms/kq4-###-human-readable-name.md`
|
||||
After saving the documentation, generate the room visual:
|
||||
1. Run: `./sci_pic_render <room_number> "<path_to_game>" rooms/kq4-###-human-readable-name/`
|
||||
2. This creates three PNG files:
|
||||
- `pic_###_visual.png` - Background visual
|
||||
- `pic_###_control.png` - Control (walkable areas)
|
||||
- `pic_###_priority.png` - Priority (z-ordering)
|
||||
3. Move the markdown file into the room directory: `mv rooms/kq4-###-human-readable-name.md rooms/kq4-###-human-readable-name/`
|
||||
|
||||
### Step 8: Update README.md
|
||||
|
||||
After saving the room documentation:
|
||||
1. Read the current README.md to find the Progress section
|
||||
2. Find the row for this room number
|
||||
3. Update the Status to `[DONE](./rooms/kq4-###-human-readable-name.md)`
|
||||
3. Update the Status to include embedded PNG and link:
|
||||
- With PNG: ` [DONE](./rooms/kq4-###-human-readable-name/kq4-###-human-readable-name.md)`
|
||||
- Without PNG (if render fails): `[DONE](./rooms/kq4-###-human-readable-name/kq4-###-human-readable-name.md)`
|
||||
4. Fill in the Room Description with a human-readable name (e.g., "Beach", "Fountain Pool", "Cave Entrance")
|
||||
|
||||
## Example Reference
|
||||
|
||||
See `rooms/055-seven-dwarfs-diamond-mine.md` for a complete example following this format.
|
||||
See `rooms/kq4-055-seven-dwarfs-diamond-mine/kq4-055-seven-dwarfs-diamond-mine.md` for a complete example following this format.
|
||||
|
||||
## Command Reference
|
||||
|
||||
@@ -322,5 +331,7 @@ Document all applied regions and check each for additional interactions.
|
||||
- Include full message text in quotes
|
||||
- Organize interactions logically by type
|
||||
- Be thorough but concise in descriptions
|
||||
- Filename must be: `kq4-##-human-readable-name.md` (e.g., `kq4-024-waterfall-and-pool.md`)
|
||||
- After completing the documentation, update README.md with the room description and link
|
||||
- Create directory: `rooms/kq4-##-human-readable-name/`
|
||||
- Save markdown as: `rooms/kq4-##-human-readable-name/kq4-##-human-readable-name.md`
|
||||
- Generate PNGs using sci_pic_render into the room directory
|
||||
- After completing the documentation, update README.md with the room description, embedded PNG thumbnail, and link
|
||||
|
||||
240
README.md
@@ -6,12 +6,15 @@ A comprehensive documentation of all game interactions extracted from the decomp
|
||||
|
||||
```
|
||||
/
|
||||
├── rooms/ # Room-specific interaction documentation
|
||||
│ ├── kq4-001.md # Beach (starting room)
|
||||
│ ├── kq4-002.md # Beach continuation
|
||||
│ ├── kq4-007.md # Fisherman's shack exterior
|
||||
│ ├── kq4-042.md # Fisherman's cottage interior
|
||||
│ └── ... # One file per room
|
||||
├── rooms/ # Room-specific documentation (one directory per room)
|
||||
│ ├── kq4-001-beach/ # Beach (starting room)
|
||||
│ │ ├── 001-beach.md # Room documentation
|
||||
│ │ ├── pic_001_visual.png # Background visual
|
||||
│ │ ├── pic_001_control.png # Control (walkable areas)
|
||||
│ │ └── pic_001_priority.png # Priority (z-ordering)
|
||||
│ ├── kq4-002-meadow/ # Meadow (with Satyr/Pan)
|
||||
│ ├── kq4-010-forest-path/ # Forest Path
|
||||
│ └── ... # One directory per room
|
||||
├── KQ4_v1.006.004_int0.000.502_SRC_(6)/
|
||||
│ └── src/ # Decompiled SCI scripts
|
||||
│ ├── Main.sc # Game engine & global handlers
|
||||
@@ -218,6 +221,39 @@ When adapting to a KQ5-style interface (walk, look, hand, talk, inventory):
|
||||
|
||||
Text messages referenced as `Print scriptNum messageNum` will be extracted from the game's RESOURCE files and added to each room's documentation.
|
||||
|
||||
## Generating Room Visuals
|
||||
|
||||
Each room directory contains rendered PNG images of the game's PIC resources:
|
||||
|
||||
### Using sci_pic_render
|
||||
|
||||
```bash
|
||||
./sci_pic_render <room_number> "<path_to_game>" <output_directory>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
./sci_pic_render 1 "King's Quest IV - The Perils of Rosella (1988)/KQ4" rooms/kq4-001-beach
|
||||
```
|
||||
|
||||
This generates three PNG files in the room directory:
|
||||
- `pic_###_visual.png` - The background visual
|
||||
- `pic_###_control.png` - Control (walkable areas)
|
||||
- `pic_###_priority.png` - Priority (z-ordering)
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
rooms/
|
||||
├── kq4-001-beach/
|
||||
│ ├── 001-beach.md # Room documentation
|
||||
│ ├── pic_001_visual.png # Background
|
||||
│ ├── pic_001_control.png # Walkable areas
|
||||
│ └── pic_001_priority.png # Z-ordering
|
||||
└── kq4-010-forest-path/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding room documentation:
|
||||
@@ -233,105 +269,105 @@ When adding room documentation:
|
||||
|
||||
| Room Number | Room Description | Status |
|
||||
|-------------|------------------|--------|
|
||||
| 001 | Beach | [DONE](./rooms/kq4-001-beach.md) |
|
||||
| 002 | Meadow (with Satyr/Pan) | [DONE](./rooms/kq4-002-meadow.md) |
|
||||
| 003 | Fountain Pool | [DONE](./rooms/kq4-003-fountain-pool.md) |
|
||||
| 004 | Ogre's Cottage Exterior | [DONE](./rooms/kq4-004-ogres-cottage.md) |
|
||||
| 005 | Forest Grove | [DONE](./rooms/kq4-005-forest-grove.md) |
|
||||
| 006 | Cave Entrance | [DONE](./rooms/kq4-006-cave-entrance.md) |
|
||||
| 007 | Fisherman's Shack Exterior | [DONE](./rooms/kq4-007-fishermans-shack.md) |
|
||||
| 008 | Back of Fisherman's Shack | [DONE](./rooms/kq4-008-back-of-fishermans-shack.md) |
|
||||
| 009 | Shady Wooded Area | [DONE](./rooms/kq4-009-shady-wooded-area.md) |
|
||||
| 010 | Forest Path | [DONE](./rooms/kq4-010-forest-path.md) |
|
||||
| 011 | Enchanted Grove | [DONE](./rooms/kq4-011-enchanted-grove.md) |
|
||||
| 012 | Haunted Forest | [DONE](./rooms/kq4-012-haunted-forest.md) |
|
||||
| 013 | Beach | [DONE](./rooms/kq4-013-beach.md) |
|
||||
| 014 | Green Meadow | [DONE](./rooms/kq4-014-green-meadow.md) |
|
||||
| 015 | The Frog Pond | [DONE](./rooms/kq4-015-frog-pond.md) |
|
||||
| 016 | Graveyard | [DONE](./rooms/kq4-016-graveyard.md) |
|
||||
| 017 | Spooky House Exterior | [DONE](./rooms/kq4-017-spooky-house-exterior.md) |
|
||||
| 018 | Cemetery | [DONE](./rooms/kq4-018-cemetery.md) |
|
||||
| 019 | Coastal Cliffs | [DONE](./rooms/kq4-019-coastal-cliffs.md) |
|
||||
| 020 | Meadow | [DONE](./rooms/kq4-020-meadow.md) |
|
||||
| 021 | Bridge Over Stream | [DONE](./rooms/kq4-021-bridge-over-stream.md) |
|
||||
| 022 | Gnome's Cottage | [DONE](./rooms/kq4-022-gnomes-cottage.md) |
|
||||
| 023 | Forest Path with Cottage | [DONE](./rooms/kq4-023-forest-path-with-cottage.md) |
|
||||
| 024 | Waterfall and Pool | [DONE](./rooms/kq4-024-waterfall-and-pool.md) |
|
||||
| 025 | Beach at River Delta | [DONE](./rooms/kq4-025-beach-at-river-delta.md) |
|
||||
| 026 | River Meadow | [DONE](./rooms/kq4-026-river-meadow.md) |
|
||||
| 027 | Forest Path | [DONE](./rooms/kq4-027-forest-path.md) |
|
||||
| 028 | Mine Entrance | [DONE](./rooms/kq4-028-mine-entrance.md) |
|
||||
| 029 | Dense Forest | [DONE](./rooms/kq4-029-dense-forest.md) |
|
||||
| 030 | Mountain Pass | [DONE](./rooms/kq4-030-mountain-pass.md) |
|
||||
| 031 | Open Ocean | [DONE](./rooms/kq4-031-open-ocean.md) |
|
||||
| 032 | Ocean Near Island | [DONE](./rooms/kq4-032-ocean-near-island.md) |
|
||||
| 033 | Enchanted Island Beach | [DONE](./rooms/kq4-033-enchanted-island-beach.md) |
|
||||
| 034 | Island Beach | [DONE](./rooms/kq4-034-island-beach.md) |
|
||||
| 035 | Island Beach | [DONE](./rooms/kq4-035-island-beach.md) |
|
||||
| 036 | Island Garden Pond | [DONE](./rooms/kq4-036-island-garden-pond.md) |
|
||||
| 037 | Fairy Island | [DONE](./rooms/kq4-037-fairy-island.md) |
|
||||
| 038 | Island Garden | [DONE](./rooms/kq4-038-island-garden.md) |
|
||||
| 039 | Island Beach | [DONE](./rooms/kq4-039-island-beach.md) |
|
||||
| 040 | Island Beach (East) | [DONE](./rooms/kq4-040-island-beach-east.md) |
|
||||
| 041 | Island Shore | [DONE](./rooms/kq4-041-island-shore.md) |
|
||||
| 042 | Fisherman's Cottage Interior | [DONE](./rooms/kq4-042-fishermans-shack-inside.md) |
|
||||
| 043 | Desert Island | [DONE](./rooms/kq4-043-desert-island.md) |
|
||||
| 044 | Inside Whale | [DONE](./rooms/kq4-044-inside-whale.md) |
|
||||
| 045 | Genesta's Bed Chamber | [DONE](./rooms/kq4-045-genestas-bed-chamber.md) |
|
||||
| 046 | Tower Stairway | [DONE](./rooms/kq4-046-tower-stairway.md) |
|
||||
| 047 | Genesta's Palace Entry Hall | [DONE](./rooms/kq4-047-genestas-palace-entry-hall.md) |
|
||||
| 048 | Ogre's Bedroom | [DONE](./rooms/kq4-048-ogres-bedroom.md) |
|
||||
| 049 | Ogre's Cottage | [DONE](./rooms/kq4-049-ogres-cottage.md) |
|
||||
| 050 | Ogress's Kitchen | [DONE](./rooms/kq4-050-ogress-kitchen.md) |
|
||||
| 051 | Ogre's Closet | [DONE](./rooms/kq4-051-ogres-closet.md) |
|
||||
| 001 | Beach |  [DONE](./rooms/kq4-001-beach/001-beach.md) |
|
||||
| 002 | Meadow (with Satyr/Pan) |  [DONE](./rooms/kq4-002-meadow/kq4-002-meadow.md) |
|
||||
| 003 | Fountain Pool |  [DONE](./rooms/kq4-003-fountain-pool/kq4-003-fountain-pool.md) |
|
||||
| 004 | Ogre's Cottage Exterior |  [DONE](./rooms/kq4-004-ogres-cottage/kq4-004-ogres-cottage.md) |
|
||||
| 005 | Forest Grove |  [DONE](./rooms/kq4-005-forest-grove/kq4-005-forest-grove.md) |
|
||||
| 006 | Cave Entrance |  [DONE](./rooms/kq4-006-cave-entrance/kq4-006-cave-entrance.md) |
|
||||
| 007 | Fisherman's Shack Exterior |  [DONE](./rooms/kq4-007-fishermans-shack/kq4-007-fishermans-shack.md) |
|
||||
| 008 | Back of Fisherman's Shack |  [DONE](./rooms/kq4-008-back-of-fishermans-shack/kq4-008-back-of-fishermans-shack.md) |
|
||||
| 009 | Shady Wooded Area |  [DONE](./rooms/kq4-009-shady-wooded-area/kq4-009-shady-wooded-area.md) |
|
||||
| 010 | Forest Path |  [DONE](./rooms/kq4-010-forest-path/kq4-010-forest-path.md) |
|
||||
| 011 | Enchanted Grove |  [DONE](./rooms/kq4-011-enchanted-grove/kq4-011-enchanted-grove.md) |
|
||||
| 012 | Haunted Forest |  [DONE](./rooms/kq4-012-haunted-forest/kq4-012-haunted-forest.md) |
|
||||
| 013 | Beach |  [DONE](./rooms/kq4-013-beach/kq4-013-beach.md) |
|
||||
| 014 | Green Meadow |  [DONE](./rooms/kq4-014-green-meadow/kq4-014-green-meadow.md) |
|
||||
| 015 | The Frog Pond |  [DONE](./rooms/kq4-015-frog-pond/kq4-015-frog-pond.md) |
|
||||
| 016 | Graveyard |  [DONE](./rooms/kq4-016-graveyard/kq4-016-graveyard.md) |
|
||||
| 017 | Spooky House Exterior |  [DONE](./rooms/kq4-017-spooky-house-exterior/kq4-017-spooky-house-exterior.md) |
|
||||
| 018 | Cemetery |  [DONE](./rooms/kq4-018-cemetery/kq4-018-cemetery.md) |
|
||||
| 019 | Coastal Cliffs |  [DONE](./rooms/kq4-019-coastal-cliffs/kq4-019-coastal-cliffs.md) |
|
||||
| 020 | Meadow |  [DONE](./rooms/kq4-020-meadow/kq4-020-meadow.md) |
|
||||
| 021 | Bridge Over Stream |  [DONE](./rooms/kq4-021-bridge-over-stream/kq4-021-bridge-over-stream.md) |
|
||||
| 022 | Gnome's Cottage |  [DONE](./rooms/kq4-022-gnomes-cottage/kq4-022-gnomes-cottage.md) |
|
||||
| 023 | Forest Path with Cottage |  [DONE](./rooms/kq4-023-forest-path-with-cottage/kq4-023-forest-path-with-cottage.md) |
|
||||
| 024 | Waterfall and Pool |  [DONE](./rooms/kq4-024-waterfall-and-pool/024-waterfall-and-pool.md) |
|
||||
| 025 | Beach at River Delta |  [DONE](./rooms/kq4-025-beach-at-river-delta/kq4-025-beach-at-river-delta.md) |
|
||||
| 026 | River Meadow |  [DONE](./rooms/kq4-026-river-meadow/kq4-026-river-meadow.md) |
|
||||
| 027 | Forest Path |  [DONE](./rooms/kq4-027-forest-path/kq4-027-forest-path.md) |
|
||||
| 028 | Mine Entrance |  [DONE](./rooms/kq4-028-mine-entrance/kq4-028-mine-entrance.md) |
|
||||
| 029 | Dense Forest |  [DONE](./rooms/kq4-029-dense-forest/kq4-029-dense-forest.md) |
|
||||
| 030 | Mountain Pass |  [DONE](./rooms/kq4-030-mountain-pass/kq4-030-mountain-pass.md) |
|
||||
| 031 | Open Ocean |  [DONE](./rooms/kq4-031-open-ocean/kq4-031-open-ocean.md) |
|
||||
| 032 | Ocean Near Island |  [DONE](./rooms/kq4-032-ocean-near-island/kq4-032-ocean-near-island.md) |
|
||||
| 033 | Enchanted Island Beach |  [DONE](./rooms/kq4-033-enchanted-island-beach/kq4-033-enchanted-island-beach.md) |
|
||||
| 034 | Island Beach |  [DONE](./rooms/kq4-034-island-beach/kq4-034-island-beach.md) |
|
||||
| 035 | Island Beach |  [DONE](./rooms/kq4-035-island-beach/kq4-035-island-beach.md) |
|
||||
| 036 | Island Garden Pond |  [DONE](./rooms/kq4-036-island-garden-pond/kq4-036-island-garden-pond.md) |
|
||||
| 037 | Fairy Island |  [DONE](./rooms/kq4-037-fairy-island/kq4-037-fairy-island.md) |
|
||||
| 038 | Island Garden |  [DONE](./rooms/kq4-038-island-garden/kq4-038-island-garden.md) |
|
||||
| 039 | Island Beach |  [DONE](./rooms/kq4-039-island-beach/kq4-039-island-beach.md) |
|
||||
| 040 | Island Beach (East) |  [DONE](./rooms/kq4-040-island-beach-east/kq4-040-island-beach-east.md) |
|
||||
| 041 | Island Shore |  [DONE](./rooms/kq4-041-island-shore/kq4-041-island-shore.md) |
|
||||
| 042 | Fisherman's Cottage Interior |  [DONE](./rooms/kq4-042-fishermans-shack-inside/kq4-042-fishermans-shack-inside.md) |
|
||||
| 043 | Desert Island |  [DONE](./rooms/kq4-043-desert-island/kq4-043-desert-island.md) |
|
||||
| 044 | Inside Whale |  [DONE](./rooms/kq4-044-inside-whale/kq4-044-inside-whale.md) |
|
||||
| 045 | Genesta's Bed Chamber |  [DONE](./rooms/kq4-045-genestas-bed-chamber/kq4-045-genestas-bed-chamber.md) |
|
||||
| 046 | Tower Stairway |  [DONE](./rooms/kq4-046-tower-stairway/kq4-046-tower-stairway.md) |
|
||||
| 047 | Genesta's Palace Entry Hall |  [DONE](./rooms/kq4-047-genestas-palace-entry-hall/kq4-047-genestas-palace-entry-hall.md) |
|
||||
| 048 | Ogre's Bedroom |  [DONE](./rooms/kq4-048-ogres-bedroom/kq4-048-ogres-bedroom.md) |
|
||||
| 049 | Ogre's Cottage |  [DONE](./rooms/kq4-049-ogres-cottage/kq4-049-ogres-cottage.md) |
|
||||
| 050 | Ogress's Kitchen |  [DONE](./rooms/kq4-050-ogress-kitchen/kq4-050-ogress-kitchen.md) |
|
||||
| 051 | Ogre's Closet |  [DONE](./rooms/kq4-051-ogres-closet/kq4-051-ogres-closet.md) |
|
||||
| 052 | | N/A (doesn't exist) |
|
||||
| 053 | Seven Dwarfs' Bedroom | [DONE](./rooms/kq4-053-seven-dwarfs-bedroom.md) |
|
||||
| 054 | Seven Dwarfs' Cottage | [DONE](./rooms/kq4-054-seven-dwarfs-cottage.md) |
|
||||
| 055 | Seven Dwarfs Diamond Mine | [DONE](./rooms/kq4-055-seven-dwarfs-diamond-mine.md) |
|
||||
| 056 | Seven Dwarfs' Diamond Mine (West) | [DONE](./rooms/kq4-056-seven-dwarfs-diamond-mine-west.md) |
|
||||
| 057 | Witches' Cave | [DONE](./rooms/kq4-057-witches-cave.md) |
|
||||
| 058 | Tower Organ Room | [DONE](./rooms/kq4-058-tower-organ-room.md) |
|
||||
| 059 | Baby Nursery | [DONE](./rooms/kq4-059-baby-nursery.md) |
|
||||
| 060 | Bedroom | [DONE](./rooms/kq4-060-bedroom.md) |
|
||||
| 061 | Tower Stairs | [DONE](./rooms/kq4-061-tower-stairs.md) |
|
||||
| 062 | Bedroom | [DONE](./rooms/kq4-062-bedroom.md) |
|
||||
| 063 | Attic | [DONE](./rooms/kq4-063-attic.md) |
|
||||
| 064 | Old Dining Room | [DONE](./rooms/kq4-064-old-dining-room.md) |
|
||||
| 065 | Old Kitchen | [DONE](./rooms/kq4-065-old-kitchen.md) |
|
||||
| 066 | Secret Tower | [DONE](./rooms/kq4-066-secret-tower.md) |
|
||||
| 067 | The Parlor | [DONE](./rooms/kq4-067-the-parlor.md) |
|
||||
| 068 | The Foyer | [DONE](./rooms/kq4-068-the-foyer.md) |
|
||||
| 069 | The Crypt | [DONE](./rooms/kq4-069-the-crypt.md) |
|
||||
| 070 | Waterfall Cave | [DONE](./rooms/kq4-070-waterfall-cave.md) |
|
||||
| 071 | Cave Entrance | [DONE](./rooms/kq4-071-cave-entrance.md) |
|
||||
| 072 | Dark Cave Passage | [DONE](./rooms/kq4-072-dark-cave-passage.md) |
|
||||
| 073 | Cave Exit | [DONE](./rooms/kq4-073-cave-exit.md) |
|
||||
| 074 | Troll Cave | [DONE](./rooms/kq4-074-troll-cave.md) |
|
||||
| 075 | Troll Cave Passage | [DONE](./rooms/kq4-075-troll-cave-passage.md) |
|
||||
| 076 | Dark Chasm | [DONE](./rooms/kq4-076-dark-chasm.md) |
|
||||
| 077 | Swamp | [DONE](./rooms/kq4-077-swamp.md) |
|
||||
| 078 | Swamp Island | [DONE](./rooms/kq4-078-swamp-island.md) |
|
||||
| 079 | Mountain Path to Dark Castle | [DONE](./rooms/kq4-079-mountain-path-to-dark-castle.md) |
|
||||
| 080 | Lolotte's Castle Entrance | [DONE](./rooms/kq4-080-lolottes-castle-entrance.md) |
|
||||
| 081 | Edgar's Tower Bedroom | [DONE](./rooms/kq4-081-edgars-tower-bedroom.md) |
|
||||
| 082 | Lolotte's Tower Bedroom | [DONE](./rooms/kq4-082-lolottes-tower-bedroom.md) |
|
||||
| 083 | Castle Dungeon Cell | [DONE](./rooms/kq4-083-castle-dungeon-cell.md) |
|
||||
| 084 | Cottage Front | [DONE](./rooms/kq4-084-cottage-front.md) |
|
||||
| 085 | Dark Tower Stairs | [DONE](./rooms/kq4-085-dark-tower-stairs.md) |
|
||||
| 086 | Dim Hallway (West End) | [DONE](./rooms/kq4-086-dim-hallway-west-end.md) |
|
||||
| 087 | East End of Hallway | [DONE](./rooms/kq4-087-east-end-of-hallway.md) |
|
||||
| 088 | Stone Tower Stairs | [DONE](./rooms/kq4-088-stone-tower-stairs.md) |
|
||||
| 089 | Castle Kitchen | [DONE](./rooms/kq4-089-castle-kitchen.md) |
|
||||
| 090 | West Tower Bottom | [DONE](./rooms/kq4-090-west-tower-bottom.md) |
|
||||
| 091 | Castle Dining Room | [DONE](./rooms/kq4-091-castle-dining-room.md) |
|
||||
| 092 | Lolotte's Throne Room | [DONE](./rooms/kq4-092-lolottes-throne-room.md) |
|
||||
| 093 | Bottom of East Tower | [DONE](./rooms/kq4-093-bottom-of-east-tower.md) |
|
||||
| 094 | Unicorn Stable | [DONE](./rooms/kq4-094-unicorn-stable.md) |
|
||||
| 095 | Fisherman's Pier | [DONE](./rooms/kq4-095-fishermans-pier.md) |
|
||||
| 053 | Seven Dwarfs' Bedroom |  [DONE](./rooms/kq4-053-seven-dwarfs-bedroom/kq4-053-seven-dwarfs-bedroom.md) |
|
||||
| 054 | Seven Dwarfs' Cottage |  [DONE](./rooms/kq4-054-seven-dwarfs-cottage/kq4-054-seven-dwarfs-cottage.md) |
|
||||
| 055 | Seven Dwarfs Diamond Mine |  [DONE](./rooms/kq4-055-seven-dwarfs-diamond-mine/055-seven-dwarfs-diamond-mine.md) |
|
||||
| 056 | Seven Dwarfs' Diamond Mine (West) |  [DONE](./rooms/kq4-056-seven-dwarfs-diamond-mine-west/kq4-056-seven-dwarfs-diamond-mine-west.md) |
|
||||
| 057 | Witches' Cave |  [DONE](./rooms/kq4-057-witch-cave/57-witch-cave.md) |
|
||||
| 058 | Tower Organ Room |  [DONE](./rooms/kq4-058-tower-organ-room/kq4-058-tower-organ-room.md) |
|
||||
| 059 | Baby Nursery |  [DONE](./rooms/kq4-059-baby-nursery/kq4-059-baby-nursery.md) |
|
||||
| 060 | Bedroom |  [DONE](./rooms/kq4-060-bedroom/kq4-060-bedroom.md) |
|
||||
| 061 | Tower Stairs |  [DONE](./rooms/kq4-061-tower-stairs/kq4-061-tower-stairs.md) |
|
||||
| 062 | Bedroom |  [DONE](./rooms/kq4-062-bedroom/kq4-062-bedroom.md) |
|
||||
| 063 | Attic |  [DONE](./rooms/kq4-063-attic/kq4-063-attic.md) |
|
||||
| 064 | Old Dining Room |  [DONE](./rooms/kq4-064-old-dining-room/kq4-064-old-dining-room.md) |
|
||||
| 065 | Old Kitchen |  [DONE](./rooms/kq4-065-old-kitchen/kq4-065-old-kitchen.md) |
|
||||
| 066 | Secret Tower |  [DONE](./rooms/kq4-066-secret-tower/kq4-066-secret-tower.md) |
|
||||
| 067 | The Parlor |  [DONE](./rooms/kq4-067-the-parlor/kq4-067-the-parlor.md) |
|
||||
| 068 | The Foyer |  [DONE](./rooms/kq4-068-the-foyer/kq4-068-the-foyer.md) |
|
||||
| 069 | The Crypt |  [DONE](./rooms/kq4-069-the-crypt/kq4-069-the-crypt.md) |
|
||||
| 070 | Waterfall Cave |  [DONE](./rooms/kq4-070-waterfall-cave/070-waterfall-cave.md) |
|
||||
| 071 | Cave Entrance |  [DONE](./rooms/kq4-071-cave-entrance/kq4-071-cave-entrance.md) |
|
||||
| 072 | Dark Cave Passage |  [DONE](./rooms/kq4-072-dark-cave-passage/kq4-072-dark-cave-passage.md) |
|
||||
| 073 | Cave Exit |  [DONE](./rooms/kq4-073-cave-exit/kq4-073-cave-exit.md) |
|
||||
| 074 | Troll Cave |  [DONE](./rooms/kq4-074-troll-cave/kq4-074-troll-cave.md) |
|
||||
| 075 | Troll Cave Passage |  [DONE](./rooms/kq4-075-troll-cave-passage/kq4-075-troll-cave-passage.md) |
|
||||
| 076 | Dark Chasm |  [DONE](./rooms/kq4-076-dark-chasm/kq4-076-dark-chasm.md) |
|
||||
| 077 | Swamp |  [DONE](./rooms/kq4-077-swamp/kq4-077-swamp.md) |
|
||||
| 078 | Swamp Island |  [DONE](./rooms/kq4-078-swamp-island/kq4-078-swamp-island.md) |
|
||||
| 079 | Mountain Path to Dark Castle |  [DONE](./rooms/kq4-079-mountain-path-to-dark-castle/kq4-079-mountain-path-to-dark-castle.md) |
|
||||
| 080 | Lolotte's Castle Entrance |  [DONE](./rooms/kq4-080-lolottes-castle-entrance/kq4-080-lolottes-castle-entrance.md) |
|
||||
| 081 | Edgar's Tower Bedroom |  [DONE](./rooms/kq4-081-edgars-tower-bedroom/kq4-081-edgars-tower-bedroom.md) |
|
||||
| 082 | Lolotte's Tower Bedroom |  [DONE](./rooms/kq4-082-lolottes-tower-bedroom/kq4-082-lolottes-tower-bedroom.md) |
|
||||
| 083 | Castle Dungeon Cell |  [DONE](./rooms/kq4-083-castle-dungeon-cell/kq4-083-castle-dungeon-cell.md) |
|
||||
| 084 | Cottage Front |  [DONE](./rooms/kq4-084-cottage-front/kq4-084-cottage-front.md) |
|
||||
| 085 | Dark Tower Stairs |  [DONE](./rooms/kq4-085-dark-tower-stairs/kq4-085-dark-tower-stairs.md) |
|
||||
| 086 | Dim Hallway (West End) |  [DONE](./rooms/kq4-086-dim-hallway-west-end/kq4-086-dim-hallway-west-end.md) |
|
||||
| 087 | East End of Hallway |  [DONE](./rooms/kq4-087-east-end-of-hallway/kq4-087-east-end-of-hallway.md) |
|
||||
| 088 | Stone Tower Stairs |  [DONE](./rooms/kq4-088-stone-tower-stairs/kq4-088-stone-tower-stairs.md) |
|
||||
| 089 | Castle Kitchen |  [DONE](./rooms/kq4-089-castle-kitchen/kq4-089-castle-kitchen.md) |
|
||||
| 090 | West Tower Bottom |  [DONE](./rooms/kq4-090-west-tower-bottom/kq4-090-west-tower-bottom.md) |
|
||||
| 091 | Castle Dining Room |  [DONE](./rooms/kq4-091-castle-dining-room/kq4-091-castle-dining-room.md) |
|
||||
| 092 | Lolotte's Throne Room |  [DONE](./rooms/kq4-092-lolottes-throne-room/kq4-092-lolottes-throne-room.md) |
|
||||
| 093 | Bottom of East Tower |  [DONE](./rooms/kq4-093-bottom-of-east-tower/kq4-093-bottom-of-east-tower.md) |
|
||||
| 094 | Unicorn Stable |  [DONE](./rooms/kq4-094-unicorn-stable/kq4-094-unicorn-stable.md) |
|
||||
| 095 | Fisherman's Pier |  [DONE](./rooms/kq4-095-fishermans-pier/kq4-095-fishermans-pier.md) |
|
||||
| 096 | | N/A (doesn't exist) |
|
||||
| 097 | | N/A (doesn't exist) |
|
||||
| 098 | Transitional Room | [DONE](./rooms/kq4-098-transitional-room.md) |
|
||||
| 099 | Transitional Room | [DONE](./rooms/kq4-099-transitional-room.md) |
|
||||
| 098 | Transitional Room | [DONE](./rooms/kq4-098-transitional-room/kq4-098-transitional-room.md) |
|
||||
| 099 | Transitional Room | [DONE](./rooms/kq4-099-transitional-room/kq4-099-transitional-room.md) |
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
BIN
__pycache__/extract_sci_pic.cpython-311.pyc
Normal file
BIN
__pycache__/extract_sci_text.cpython-311.pyc
Normal file
BIN
docs/rooms/pic_001_control.png
Normal file
|
After Width: | Height: | Size: 152 B |
BIN
docs/rooms/pic_001_priority.png
Normal file
|
After Width: | Height: | Size: 204 B |
BIN
docs/rooms/pic_001_visual.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/rooms/pic_002_control.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
docs/rooms/pic_002_lines_only.png
Normal file
|
After Width: | Height: | Size: 736 B |
BIN
docs/rooms/pic_002_priority.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
docs/rooms/pic_002_visual.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/rooms/pic_003_control.png
Normal file
|
After Width: | Height: | Size: 458 B |
BIN
docs/rooms/pic_003_priority.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
docs/rooms/pic_003_visual.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
docs/rooms/pic_005_control.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
docs/rooms/pic_005_priority.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
docs/rooms/pic_005_visual.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
docs/rooms/pic_040_control.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
docs/rooms/pic_040_priority.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/rooms/pic_040_visual.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
566
extract_sci_pic.py
Normal file
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SCI Picture Resource Extractor
|
||||
|
||||
Extracts and renders background images (PIC resources) from Sierra's Creative Interpreter (SCI) games.
|
||||
Supports SCI0 format (King's Quest IV).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional
|
||||
from collections import deque
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
PIC_OP_SET_COLOR = 0xf0
|
||||
PIC_OP_DISABLE_VISUAL = 0xf1
|
||||
PIC_OP_SET_PRIORITY = 0xf2
|
||||
PIC_OP_DISABLE_PRIORITY = 0xf3
|
||||
PIC_OP_RELATIVE_PATTERNS = 0xf4
|
||||
PIC_OP_RELATIVE_MEDIUM_LINES = 0xf5
|
||||
PIC_OP_RELATIVE_LONG_LINES = 0xf6
|
||||
PIC_OP_RELATIVE_SHORT_LINES = 0xf7
|
||||
PIC_OP_FILL = 0xf8
|
||||
PIC_OP_SET_PATTERN = 0xf9
|
||||
PIC_OP_ABSOLUTE_PATTERNS = 0xfa
|
||||
PIC_OP_SET_CONTROL = 0xfb
|
||||
PIC_OP_DISABLE_CONTROL = 0xfc
|
||||
PIC_OP_RELATIVE_MEDIUM_PATTERNS = 0xfd
|
||||
PIC_OP_OPX = 0xfe
|
||||
PIC_OP_END = 0xff
|
||||
|
||||
WIDTH, HEIGHT = 320, 190
|
||||
|
||||
PATTERN_FLAG_RECTANGLE = 0x10
|
||||
PATTERN_FLAG_USE_PATTERN = 0x20
|
||||
|
||||
CIRCLES = [
|
||||
[0x80],
|
||||
[0x4e, 0x40],
|
||||
[0x73, 0xef, 0xbe, 0x70],
|
||||
[0x38, 0x7c, 0xfe, 0xfe, 0xfe, 0x7c, 0x38, 0x00],
|
||||
[0x1c, 0x1f, 0xcf, 0xfb, 0xfe, 0xff, 0xbf, 0xef, 0xf9, 0xfc, 0x1c],
|
||||
[0x0e, 0x03, 0xf8, 0x7f, 0xc7, 0xfc, 0xff, 0xef, 0xfe, 0xff, 0xe7, 0xfc, 0x7f, 0xc3, 0xf8, 0x1f, 0x00],
|
||||
[0x0f, 0x80, 0xff, 0x87, 0xff, 0x1f, 0xfc, 0xff, 0xfb, 0xff, 0xef, 0xff, 0xbf, 0xfe, 0xff, 0xf9, 0xff, 0xc7, 0xff, 0x0f, 0xf8, 0x0f, 0x80],
|
||||
[0x07, 0xc0, 0x1f, 0xf0, 0x3f, 0xf8, 0x7f, 0xfc, 0x7f, 0xfc, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0xff, 0xfe, 0x7f, 0xfc, 0x7f, 0xfc, 0x3f, 0xf8, 0x1f, 0xf0, 0x07, 0xc0],
|
||||
]
|
||||
|
||||
JUNQ = [0x20, 0x94, 0x02, 0x24, 0x90, 0x82, 0xa4, 0xa2, 0x82, 0x09, 0x0a, 0x22, 0x12, 0x10, 0x42, 0x14, 0x91, 0x4a, 0x91, 0x11, 0x08, 0x12, 0x25, 0x10, 0x22, 0xa8, 0x14, 0x24, 0x00, 0x50, 0x24, 0x04]
|
||||
|
||||
JUNQINDEX = [0x00, 0x18, 0x30, 0xc4, 0xdc, 0x65, 0xeb, 0x48, 0x60, 0xbd, 0x89, 0x05, 0x0a, 0xf4, 0x7d, 0x7d, 0x85, 0xb0, 0x8e, 0x95, 0x1f, 0x22, 0x0d, 0xdf, 0x2a, 0x78, 0xd5, 0x73, 0x1c, 0xb4, 0x40, 0xa1, 0xb9, 0x3c, 0xca, 0x58, 0x92, 0x34, 0xcc, 0xce, 0xd7, 0x42, 0x90, 0x0f, 0x8b, 0x7f, 0x32, 0xed, 0x5c, 0x9d, 0xc8, 0x99, 0xad, 0x4e, 0x56, 0xa6, 0xf7, 0x68, 0xb7, 0x25, 0x82, 0x37, 0x3a, 0x51, 0x69, 0x26, 0x38, 0x52, 0x9e, 0x9a, 0x4f, 0xa7, 0x43, 0x10, 0x80, 0xee, 0x3d, 0x59, 0x35, 0xcf, 0x79, 0x74, 0xb5, 0xa2, 0xb1, 0x96, 0x23, 0xe0, 0xbe, 0x05, 0xf5, 0x6e, 0x19, 0xc5, 0x66, 0x49, 0xf0, 0xd1, 0x54, 0xa9, 0x70, 0x4b, 0xa4, 0xe2, 0xe6, 0xe5, 0xab, 0xe4, 0xd2, 0xaa, 0x4c, 0xe3, 0x06, 0x6f, 0xc6, 0x4a, 0xa4, 0x75, 0x97, 0xe1]
|
||||
|
||||
EGA_COLORS = [
|
||||
(0x00, 0x00, 0x00), (0x00, 0x00, 0xA0), (0x00, 0xA0, 0x00), (0x00, 0xA0, 0xA0),
|
||||
(0xA0, 0x00, 0x00), (0xA0, 0x00, 0xA0), (0xA0, 0x50, 0x00), (0xA0, 0xA0, 0xA0),
|
||||
(0x50, 0x50, 0x50), (0x50, 0x50, 0xFF), (0x00, 0xFF, 0x50), (0x50, 0xFF, 0xFF),
|
||||
(0xFF, 0x50, 0x50), (0xFF, 0x50, 0xFF), (0xFF, 0xFF, 0x50), (0xFF, 0xFF, 0xFF),
|
||||
]
|
||||
|
||||
DEFAULT_PALETTE = [
|
||||
(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7),
|
||||
(8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (8, 8),
|
||||
(8, 8), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (8, 8),
|
||||
(8, 8), (15, 9), (15, 10), (15, 11), (15, 12), (15, 13), (15, 14), (15, 15),
|
||||
(0, 8), (9, 1), (2, 10), (3, 11), (4, 12), (5, 13), (6, 14), (8, 8),
|
||||
]
|
||||
|
||||
|
||||
class PictureRenderer:
|
||||
VISUAL = 1
|
||||
PRIORITY = 2
|
||||
CONTROL = 4
|
||||
|
||||
def __init__(self):
|
||||
self.visual = np.full((HEIGHT, WIDTH), 0x0f, dtype=np.uint8)
|
||||
self.priority = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
||||
self.control = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
||||
self.aux = np.zeros((HEIGHT, WIDTH), dtype=np.uint8)
|
||||
self.palettes = [list(DEFAULT_PALETTE) for _ in range(4)]
|
||||
self.locked = [0] * 40
|
||||
self.reset_state()
|
||||
|
||||
def reset_state(self):
|
||||
self.palette_number = 0
|
||||
self.palette_offset = 0
|
||||
self.ega_color = (0, 0)
|
||||
self.current_priority = 0
|
||||
self.current_control = 0
|
||||
self.draw_enable = self.VISUAL
|
||||
self.palette_to_draw = 0
|
||||
self.pattern_size = 0
|
||||
self.pattern_nr = 0
|
||||
self.pattern_use_pattern = False
|
||||
self.pattern_is_rect = False
|
||||
|
||||
def is_white(self):
|
||||
return self.ega_color[0] == 15 and self.ega_color[1] == 15
|
||||
|
||||
def get_aux_set(self):
|
||||
aux = self.draw_enable
|
||||
if self.ega_color[0] == 15 and self.ega_color[1] == 15:
|
||||
aux &= ~self.VISUAL
|
||||
if self.current_priority == 0:
|
||||
aux &= ~self.PRIORITY
|
||||
if self.current_control == 0:
|
||||
aux &= ~self.CONTROL
|
||||
return aux
|
||||
|
||||
def plot_pixel(self, x: int, y: int):
|
||||
if 0 <= x < WIDTH and 0 <= y < HEIGHT:
|
||||
aux_set = self.get_aux_set()
|
||||
if self.draw_enable & self.VISUAL:
|
||||
# EGA dithering: color1 on odd pixels, color2 on even pixels
|
||||
color_idx = self.ega_color[0] if ((x ^ y) & 1) else self.ega_color[1]
|
||||
self.visual[y, x] = color_idx
|
||||
if self.draw_enable & self.PRIORITY:
|
||||
self.priority[y, x] = self.current_priority
|
||||
if self.draw_enable & self.CONTROL:
|
||||
self.control[y, x] = self.current_control
|
||||
self.aux[y, x] |= aux_set
|
||||
|
||||
def draw_line(self, x1: int, y1: int, x2: int, y2: int):
|
||||
dx = abs(x2 - x1)
|
||||
dy = abs(y2 - y1)
|
||||
sx = 1 if x1 < x2 else -1
|
||||
sy = 1 if y1 < y2 else -1
|
||||
err = dx - dy
|
||||
while True:
|
||||
self.plot_pixel(x1, y1)
|
||||
if x1 == x2 and y1 == y2:
|
||||
break
|
||||
e2 = 2 * err
|
||||
if e2 > -dy:
|
||||
err -= dy
|
||||
x1 += sx
|
||||
if e2 < dx:
|
||||
err += dx
|
||||
y1 += sy
|
||||
|
||||
def draw_pattern(self, x: int, y: int, size: int, pattern_nr: int, use_pattern: bool, is_rect: bool):
|
||||
wsize = size
|
||||
x_max, y_max = WIDTH - 1, HEIGHT - 1
|
||||
|
||||
if x < wsize:
|
||||
x = wsize
|
||||
if x + wsize > x_max:
|
||||
x = x_max - wsize
|
||||
if y < wsize:
|
||||
y = wsize
|
||||
if y + wsize > y_max:
|
||||
y = y_max - wsize
|
||||
|
||||
if pattern_nr >= len(JUNQINDEX):
|
||||
return
|
||||
|
||||
junqbit = JUNQINDEX[pattern_nr]
|
||||
|
||||
if is_rect:
|
||||
for ly in range(y - wsize, y + wsize + 1):
|
||||
for lx in range(x - wsize, x + wsize + 2):
|
||||
if use_pattern:
|
||||
if (JUNQ[junqbit >> 3] >> (7 - (junqbit & 7))) & 1:
|
||||
self.plot_pixel(lx, ly)
|
||||
junqbit = (junqbit + 1) & 0xFF
|
||||
else:
|
||||
self.plot_pixel(lx, ly)
|
||||
else:
|
||||
circlebit = 0
|
||||
circle_data = CIRCLES[size]
|
||||
circle_data_bits = len(circle_data) * 8
|
||||
for ly in range(y - wsize, y + wsize + 1):
|
||||
for lx in range(x - wsize, x + wsize + 2):
|
||||
circle_on = False
|
||||
if circlebit < circle_data_bits:
|
||||
circle_on = bool((circle_data[circlebit >> 3] >> (7 - (circlebit & 7))) & 1)
|
||||
if circle_on:
|
||||
if use_pattern:
|
||||
if (JUNQ[junqbit >> 3] >> (7 - (junqbit & 7))) & 1:
|
||||
self.plot_pixel(lx, ly)
|
||||
junqbit = (junqbit + 1) & 0xFF
|
||||
else:
|
||||
self.plot_pixel(lx, ly)
|
||||
circlebit += 1
|
||||
|
||||
def _fill_bounds(self, x: int, y: int, draw_enable: int) -> bool:
|
||||
"""Check if (x, y) is a fill boundary, using the given draw_enable flags.
|
||||
|
||||
Returns True if the pixel is a boundary (should NOT be filled).
|
||||
Matches SCICompanion's FILL_BOUNDS macro:
|
||||
(dwDrawEnable & aux) && !(visual_enabled && pixel_is_white)
|
||||
"""
|
||||
aux = self.aux[y, x]
|
||||
overlap = draw_enable & aux
|
||||
if overlap:
|
||||
# There IS something drawn here that overlaps with what we want to draw.
|
||||
# But if visual is enabled and the pixel is white, treat it as empty (not a boundary).
|
||||
if (draw_enable & self.VISUAL) and (self.visual[y, x] == 0x0f):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def _ok_to_fill(self, x: int, y: int, draw_enable: int) -> bool:
|
||||
if not (0 <= x < WIDTH and 0 <= y < HEIGHT):
|
||||
return False
|
||||
return not self._fill_bounds(x, y, draw_enable)
|
||||
|
||||
def flood_fill(self, start_x: int, start_y: int):
|
||||
if not (0 <= start_x < WIDTH and 0 <= start_y < HEIGHT):
|
||||
return
|
||||
|
||||
# Compute auxSet from the color/priority/control and draw_enable.
|
||||
# Then replace draw_enable with auxSet (matching SCICompanion behavior).
|
||||
aux_set = self.get_aux_set()
|
||||
draw_enable = aux_set
|
||||
|
||||
# Guard against filling with pure white (would hang/infinite loop).
|
||||
if self.is_white():
|
||||
return
|
||||
|
||||
# If no screen is being drawn to, bail.
|
||||
if draw_enable == 0:
|
||||
return
|
||||
|
||||
# Check if starting point is already a boundary.
|
||||
if self._fill_bounds(start_x, start_y, draw_enable):
|
||||
return
|
||||
|
||||
# Use a stack (LIFO) to match SCICompanion's qstore/qretrieve behavior.
|
||||
stack = [(start_x, start_y)]
|
||||
|
||||
while stack:
|
||||
x, y = stack.pop()
|
||||
|
||||
if not self._ok_to_fill(x, y, draw_enable):
|
||||
continue
|
||||
|
||||
# Plot the pixel
|
||||
if draw_enable & self.VISUAL:
|
||||
self.visual[y, x] = self.ega_color[0] if ((x ^ y) & 1) else self.ega_color[1]
|
||||
if draw_enable & self.PRIORITY:
|
||||
self.priority[y, x] = self.current_priority
|
||||
if draw_enable & self.CONTROL:
|
||||
self.control[y, x] = self.current_control
|
||||
self.aux[y, x] |= aux_set
|
||||
|
||||
if y > 0 and self._ok_to_fill(x, y - 1, draw_enable):
|
||||
stack.append((x, y - 1))
|
||||
if x > 0 and self._ok_to_fill(x - 1, y, draw_enable):
|
||||
stack.append((x - 1, y))
|
||||
if x < WIDTH - 1 and self._ok_to_fill(x + 1, y, draw_enable):
|
||||
stack.append((x + 1, y))
|
||||
if y < HEIGHT - 1 and self._ok_to_fill(x, y + 1, draw_enable):
|
||||
stack.append((x, y + 1))
|
||||
|
||||
def read_abs_coords(self, data: bytes, i: int) -> Tuple[int, int, int]:
|
||||
prefix = data[i]
|
||||
x = data[i + 1] | ((prefix & 0xF0) << 4)
|
||||
y = data[i + 2] | ((prefix & 0x0F) << 8)
|
||||
return x, y, i + 3
|
||||
|
||||
def render(self, data: bytes) -> Tuple[Image.Image, Image.Image, Image.Image]:
|
||||
self.visual.fill(0x0f)
|
||||
self.priority.fill(0)
|
||||
self.control.fill(0)
|
||||
self.aux.fill(0)
|
||||
self.palettes = [list(DEFAULT_PALETTE) for _ in range(4)]
|
||||
self.locked = [0] * 40
|
||||
self.reset_state()
|
||||
|
||||
i = 0
|
||||
x, y = 0, 0
|
||||
|
||||
while i < len(data):
|
||||
opcode = data[i]
|
||||
|
||||
if opcode >= 0xF0:
|
||||
i += 1
|
||||
|
||||
if opcode == PIC_OP_END:
|
||||
break
|
||||
|
||||
elif opcode == PIC_OP_SET_COLOR:
|
||||
color_code = data[i]
|
||||
i += 1
|
||||
# Palette number and offset within palette
|
||||
palette_num = color_code // 40
|
||||
palette_offset = color_code % 40
|
||||
# If palette_num is 0, use the global palette_to_draw instead
|
||||
palette_to_use = palette_num if palette_num != 0 else self.palette_to_draw
|
||||
# Store for UI/state tracking
|
||||
self.palette_number = palette_num
|
||||
self.palette_offset = palette_offset
|
||||
# Look up the actual color from the palette
|
||||
if self.locked[palette_offset]:
|
||||
self.ega_color = self.palettes[0][palette_offset]
|
||||
else:
|
||||
self.ega_color = self.palettes[palette_to_use][palette_offset]
|
||||
self.draw_enable |= self.VISUAL
|
||||
|
||||
elif opcode == PIC_OP_DISABLE_VISUAL:
|
||||
self.draw_enable &= ~self.VISUAL
|
||||
|
||||
elif opcode == PIC_OP_SET_PRIORITY:
|
||||
self.current_priority = data[i] & 0x0F
|
||||
i += 1
|
||||
self.draw_enable |= self.PRIORITY
|
||||
|
||||
elif opcode == PIC_OP_DISABLE_PRIORITY:
|
||||
self.draw_enable &= ~self.PRIORITY
|
||||
|
||||
elif opcode == PIC_OP_SET_CONTROL:
|
||||
self.current_control = data[i]
|
||||
i += 1
|
||||
self.draw_enable |= self.CONTROL
|
||||
|
||||
elif opcode == PIC_OP_DISABLE_CONTROL:
|
||||
self.draw_enable &= ~self.CONTROL
|
||||
|
||||
elif opcode == PIC_OP_RELATIVE_MEDIUM_LINES:
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
by = data[i]
|
||||
bx = data[i + 1]
|
||||
i += 2
|
||||
dy = (by & 0x7F) if not (by & 0x80) else -(by & 0x7F)
|
||||
dx = bx if bx < 128 else bx - 256
|
||||
self.draw_line(x, y, x + dx, y + dy)
|
||||
x += dx
|
||||
y += dy
|
||||
|
||||
elif opcode == PIC_OP_RELATIVE_LONG_LINES:
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
x2, y2, i = self.read_abs_coords(data, i)
|
||||
self.draw_line(x, y, x2, y2)
|
||||
x, y = x2, y2
|
||||
|
||||
elif opcode == PIC_OP_RELATIVE_SHORT_LINES:
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
b = data[i]
|
||||
i += 1
|
||||
dx = ((b >> 4) & 0x7) if not (b & 0x80) else -((b >> 4) & 0x7)
|
||||
dy = (b & 0x7) if not (b & 0x08) else -(b & 0x7)
|
||||
self.draw_line(x, y, x + dx, y + dy)
|
||||
x += dx
|
||||
y += dy
|
||||
|
||||
elif opcode == PIC_OP_FILL:
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
fx, fy, i = self.read_abs_coords(data, i)
|
||||
self.flood_fill(fx, fy)
|
||||
|
||||
elif opcode == PIC_OP_RELATIVE_PATTERNS:
|
||||
pattern_nr_byte = 0
|
||||
if self.pattern_use_pattern and i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = data[i]
|
||||
i += 1
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
self.draw_pattern(x, y, self.pattern_size, (pattern_nr_byte >> 1) & 0x7f, self.pattern_use_pattern, self.pattern_is_rect)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = 0
|
||||
if self.pattern_use_pattern and i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = data[i]
|
||||
i += 1
|
||||
# Relative short coords for pattern
|
||||
b = data[i]
|
||||
i += 1
|
||||
dx = ((b >> 4) & 0x7) if not (b & 0x80) else -((b >> 4) & 0x7)
|
||||
dy = (b & 0x7) if not (b & 0x08) else -(b & 0x7)
|
||||
x += dx
|
||||
y += dy
|
||||
self.draw_pattern(x, y, self.pattern_size, (pattern_nr_byte >> 1) & 0x7f, self.pattern_use_pattern, self.pattern_is_rect)
|
||||
|
||||
elif opcode == PIC_OP_ABSOLUTE_PATTERNS:
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = 0
|
||||
if self.pattern_use_pattern and i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = data[i]
|
||||
i += 1
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
self.draw_pattern(x, y, self.pattern_size, (pattern_nr_byte >> 1) & 0x7f, self.pattern_use_pattern, self.pattern_is_rect)
|
||||
|
||||
elif opcode == PIC_OP_RELATIVE_MEDIUM_PATTERNS:
|
||||
pattern_nr_byte = 0
|
||||
if self.pattern_use_pattern and i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = data[i]
|
||||
i += 1
|
||||
x, y, i = self.read_abs_coords(data, i)
|
||||
self.draw_pattern(x, y, self.pattern_size, (pattern_nr_byte >> 1) & 0x7f, self.pattern_use_pattern, self.pattern_is_rect)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = 0
|
||||
if self.pattern_use_pattern and i < len(data) and data[i] < 0xF0:
|
||||
pattern_nr_byte = data[i]
|
||||
i += 1
|
||||
by = data[i]
|
||||
bx = data[i + 1]
|
||||
i += 2
|
||||
dy = (by & 0x7F) if not (by & 0x80) else -(by & 0x7F)
|
||||
dx = bx if bx < 128 else bx - 256
|
||||
x += dx
|
||||
y += dy
|
||||
self.draw_pattern(x, y, self.pattern_size, (pattern_nr_byte >> 1) & 0x7f, self.pattern_use_pattern, self.pattern_is_rect)
|
||||
|
||||
elif opcode == PIC_OP_SET_PATTERN:
|
||||
if i < len(data) and data[i] < 0xF0:
|
||||
val = data[i]
|
||||
i += 1
|
||||
val &= 0x37 # Mask to valid bits as SCICompanion does
|
||||
self.pattern_size = val & 0x07
|
||||
self.pattern_is_rect = bool(val & PATTERN_FLAG_RECTANGLE)
|
||||
self.pattern_use_pattern = bool(val & PATTERN_FLAG_USE_PATTERN)
|
||||
|
||||
elif opcode == PIC_OP_OPX:
|
||||
if i >= len(data):
|
||||
break
|
||||
ext = data[i]
|
||||
i += 1
|
||||
if ext == 0x00:
|
||||
# PIC_OPX_SET_PALETTE_ENTRY: variable length pairs of (index, color)
|
||||
while i < len(data) and data[i] < 0xF0:
|
||||
b_index = data[i]
|
||||
b_color = data[i + 1]
|
||||
i += 2
|
||||
pal_num = b_index // 40
|
||||
pal_offset = b_index % 40
|
||||
if pal_num < 4:
|
||||
# color1 is high nibble, color2 is low nibble
|
||||
color1 = (b_color >> 4) & 0x0f
|
||||
color2 = b_color & 0x0f
|
||||
self.palettes[pal_num][pal_offset] = (color1, color2)
|
||||
if pal_num == 0:
|
||||
self.locked[pal_offset] = 0xff
|
||||
elif ext == 0x01:
|
||||
# PIC_OPX_SET_PALETTE: 1 byte palette number + 40 color bytes
|
||||
pal_num = data[i]
|
||||
i += 1
|
||||
for j in range(40):
|
||||
b_color = data[i]
|
||||
i += 1
|
||||
color1 = (b_color >> 4) & 0x0f
|
||||
color2 = b_color & 0x0f
|
||||
self.palettes[pal_num][j] = (color1, color2)
|
||||
# If this palette is the one currently in use, update egaColor
|
||||
if pal_num == self.palette_number:
|
||||
self.ega_color = self.palettes[pal_num][self.palette_offset]
|
||||
elif ext == 0x08:
|
||||
# PIC_OPX_SET_PRIORITY_TABLE: 14 bytes
|
||||
i += 14
|
||||
else:
|
||||
# Unknown extended opcode, try to skip gracefully
|
||||
pass
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
visual_img = Image.new('RGB', (WIDTH, HEIGHT))
|
||||
for py in range(HEIGHT):
|
||||
for px in range(WIDTH):
|
||||
visual_img.putpixel((px, py), EGA_COLORS[self.visual[py, px]])
|
||||
|
||||
priority_img = Image.fromarray(self.priority * 17, mode='L')
|
||||
control_img = Image.fromarray(self.control * 17, mode='L')
|
||||
|
||||
return visual_img, priority_img, control_img
|
||||
|
||||
|
||||
class SCIPictureExtractor:
|
||||
def __init__(self, game_dir: str):
|
||||
self.game_dir = Path(game_dir)
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"extract_sci_text",
|
||||
self.game_dir.parent.parent / "extract_sci_text.py"
|
||||
)
|
||||
if spec and spec.loader:
|
||||
self.extractor_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(self.extractor_module)
|
||||
self.extractor = self.extractor_module.SCIResourceExtractor(game_dir)
|
||||
else:
|
||||
raise ImportError("Could not load extract_sci_text module")
|
||||
|
||||
def extract_pic(self, pic_number: int, output_dir: str = ".") -> Optional[Tuple[str, str, str]]:
|
||||
resources = self.extractor.read_resource_map()
|
||||
pic_resources = [r for r in resources if r['type'] == 1 and r['number'] == pic_number]
|
||||
|
||||
if not pic_resources:
|
||||
print(f"PIC {pic_number} not found")
|
||||
return None
|
||||
|
||||
res = pic_resources[0]
|
||||
print(f"Extracting PIC {pic_number} from package {res['package']}...")
|
||||
|
||||
header = self.extractor.read_resource_header(res['package'], res['offset'])
|
||||
if not header:
|
||||
print(f"Failed to read header for PIC {pic_number}")
|
||||
return None
|
||||
|
||||
print(f" Compression: {header['method_name']}, Size: {header['decompressed_size']} bytes")
|
||||
|
||||
data = self.extractor.extract_resource_data(
|
||||
res['package'],
|
||||
res['offset'],
|
||||
header['compressed_size'],
|
||||
header['decompressed_size'],
|
||||
header['method']
|
||||
)
|
||||
|
||||
if not data:
|
||||
print(f"Failed to extract data for PIC {pic_number}")
|
||||
return None
|
||||
|
||||
renderer = PictureRenderer()
|
||||
visual_img, priority_img, control_img = renderer.render(data)
|
||||
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
base_name = f"pic_{pic_number:03d}"
|
||||
visual_path = output_path / f"{base_name}_visual.png"
|
||||
priority_path = output_path / f"{base_name}_priority.png"
|
||||
control_path = output_path / f"{base_name}_control.png"
|
||||
|
||||
visual_img.save(visual_path)
|
||||
priority_img.save(priority_path)
|
||||
control_img.save(control_path)
|
||||
|
||||
print(f" Saved: {visual_path}")
|
||||
|
||||
return (str(visual_path), str(priority_path), str(control_path))
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Extract SCI picture resources")
|
||||
parser.add_argument("--pic", type=int, help="Specific PIC number to extract")
|
||||
parser.add_argument("--output", "-o", default="pics", help="Output directory")
|
||||
parser.add_argument("--game-dir", "-g", default="King's Quest IV - The Perils of Rosella (1988)/KQ4",
|
||||
help="Game directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.game_dir):
|
||||
print(f"Error: Game directory not found: {args.game_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Extracting pictures from: {args.game_dir}")
|
||||
|
||||
extractor = SCIPictureExtractor(args.game_dir)
|
||||
|
||||
if args.pic is not None:
|
||||
extractor.extract_pic(args.pic, args.output)
|
||||
else:
|
||||
resources = extractor.extractor.read_resource_map()
|
||||
pic_resources = sorted(set(r['number'] for r in resources if r['type'] == 1))
|
||||
print(f"Found {len(pic_resources)} PIC resources")
|
||||
for pic_num in pic_resources:
|
||||
extractor.extract_pic(pic_num, args.output)
|
||||
|
||||
print(f"\nExtraction complete! Images saved to {args.output}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
rooms/kq4-001-beach/pic_001_control.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
rooms/kq4-001-beach/pic_001_priority.png
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
rooms/kq4-001-beach/pic_001_visual.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
rooms/kq4-002-meadow/pic_002_control.png
Normal file
|
After Width: | Height: | Size: 233 B |
BIN
rooms/kq4-002-meadow/pic_002_priority.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
rooms/kq4-002-meadow/pic_002_visual.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
rooms/kq4-003-fountain-pool/pic_003_control.png
Normal file
|
After Width: | Height: | Size: 882 B |
BIN
rooms/kq4-003-fountain-pool/pic_003_priority.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
rooms/kq4-003-fountain-pool/pic_003_visual.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
rooms/kq4-004-ogres-cottage/pic_004_control.png
Normal file
|
After Width: | Height: | Size: 395 B |
BIN
rooms/kq4-004-ogres-cottage/pic_004_priority.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
rooms/kq4-004-ogres-cottage/pic_004_visual.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
rooms/kq4-005-forest-grove/pic_005_control.png
Normal file
|
After Width: | Height: | Size: 950 B |
BIN
rooms/kq4-005-forest-grove/pic_005_priority.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
rooms/kq4-005-forest-grove/pic_005_visual.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
rooms/kq4-006-cave-entrance/pic_006_control.png
Normal file
|
After Width: | Height: | Size: 820 B |
BIN
rooms/kq4-006-cave-entrance/pic_006_priority.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
rooms/kq4-006-cave-entrance/pic_006_visual.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
rooms/kq4-007-fishermans-shack/pic_007_control.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
rooms/kq4-007-fishermans-shack/pic_007_priority.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
rooms/kq4-007-fishermans-shack/pic_007_visual.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
rooms/kq4-008-back-of-fishermans-shack/pic_008_control.png
Normal file
|
After Width: | Height: | Size: 327 B |
BIN
rooms/kq4-008-back-of-fishermans-shack/pic_008_priority.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
rooms/kq4-008-back-of-fishermans-shack/pic_008_visual.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
rooms/kq4-009-shady-wooded-area/pic_009_control.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
rooms/kq4-009-shady-wooded-area/pic_009_priority.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
rooms/kq4-009-shady-wooded-area/pic_009_visual.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
rooms/kq4-010-forest-path/pic_010_control.png
Normal file
|
After Width: | Height: | Size: 375 B |
BIN
rooms/kq4-010-forest-path/pic_010_priority.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
rooms/kq4-010-forest-path/pic_010_visual.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
rooms/kq4-011-enchanted-grove/pic_011_control.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
rooms/kq4-011-enchanted-grove/pic_011_priority.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
rooms/kq4-011-enchanted-grove/pic_011_visual.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
rooms/kq4-012-haunted-forest/pic_012_control.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
rooms/kq4-012-haunted-forest/pic_012_priority.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
rooms/kq4-012-haunted-forest/pic_012_visual.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
rooms/kq4-013-beach/pic_013_control.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
rooms/kq4-013-beach/pic_013_priority.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
rooms/kq4-013-beach/pic_013_visual.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
rooms/kq4-014-green-meadow/pic_014_control.png
Normal file
|
After Width: | Height: | Size: 433 B |
BIN
rooms/kq4-014-green-meadow/pic_014_priority.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
rooms/kq4-014-green-meadow/pic_014_visual.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
rooms/kq4-015-frog-pond/pic_015_control.png
Normal file
|
After Width: | Height: | Size: 764 B |
BIN
rooms/kq4-015-frog-pond/pic_015_priority.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
rooms/kq4-015-frog-pond/pic_015_visual.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
rooms/kq4-016-graveyard/pic_016_control.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
rooms/kq4-016-graveyard/pic_016_priority.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
rooms/kq4-016-graveyard/pic_016_visual.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
rooms/kq4-017-spooky-house-exterior/pic_017_control.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
rooms/kq4-017-spooky-house-exterior/pic_017_priority.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
rooms/kq4-017-spooky-house-exterior/pic_017_visual.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
rooms/kq4-018-cemetery/pic_018_control.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
rooms/kq4-018-cemetery/pic_018_priority.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
rooms/kq4-018-cemetery/pic_018_visual.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
rooms/kq4-019-coastal-cliffs/pic_019_control.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
rooms/kq4-019-coastal-cliffs/pic_019_priority.png
Normal file
|
After Width: | Height: | Size: 1023 B |
BIN
rooms/kq4-019-coastal-cliffs/pic_019_visual.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
rooms/kq4-020-meadow/pic_020_control.png
Normal file
|
After Width: | Height: | Size: 231 B |
BIN
rooms/kq4-020-meadow/pic_020_priority.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |