Author Topic: Quick and dirty script to extract King's Quest 6 .bin files  (Read 1955 times)

0 Members and 1 Guest are viewing this topic.

Offline deckarep

Quick and dirty script to extract King's Quest 6 .bin files
« on: October 07, 2022, 05:47:05 PM »
Hello fellow SCI programming community,

I'm new around here but definitely not new to Sierra or their awesome games.

I just wanted to share a script that perhaps someone might find useful one day and I also bring a long a question.

1. Here's a link to a tool for extracting the first embedded character portrait bitmap of a KQ6 .bin file. The script was a super quick/dirty hack and doesn't yet properly parse the entire file which also includes synchronization data but it's a start. The code is written in the Go programming language. Perhaps others find it useful because I was not able to find anything that existed already from the community.

https://gist.github.com/deckarep/d09e98827945208db3bd4770f1256235

2. Is anyone aware of other Sierra games which had .bin files featuring character portraits other than KQ6? I only know of KQ6 which supported these files on the original Windows version of the game.

If anyone is curious why I'm bothering with this is because I'm putting together a fan-inspired project to build a playing card deck featuring the artwork of KQ6. I will likely do other Sierra games if there is more interest.

See my twitter @deckarep for more information and some screenshots!

Thanks!

-deckarep






Offline lskovlun

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #1 on: October 09, 2022, 01:17:19 AM »
Nice!

Offline doomlazer

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #2 on: October 09, 2022, 05:14:08 AM »
If you change curWidth from +2 to +6 and remove the break, it will export all the bitmaps.

Code: [Select]
curWidth := binary.LittleEndian.Uint16(b[dataOffset+6:])

This sets both width and bytesPerLine to what should be the width according to this from portrait.cpp.:

Code: [Select]
// bitmap-data follows, total of [animation count]
//   14 bytes bitmap header
//    -> 4 bytes unknown   // DL Edit: first for bytes are bytesPerLine Y and bytesPerLine X
//    -> 2 bytes height
//    -> 2 bytes width
//    -> 6 bytes unknown
//   height * width bitmap data

So +4 is height and +6 is width. I don't see how you can determine bytesPerLine other than setting it to width. The crazy part is, in the scummvm code further down, they seem to use the wrong offsets as well!

Code: [Select]
for (bitmapNr = 0; bitmapNr < _bitmaps.size(); bitmapNr++) {
PortraitBitmap &curBitmap = _bitmaps[bitmapNr];
curBitmap.width = data.getUint16LEAt(2);
curBitmap.height = data.getUint16LEAt(4);
bytesPerLine = data.getUint16LEAt(6);
if (bytesPerLine < curBitmap.width)
error("kPortrait: bytesPerLine larger than actual width");
curBitmap.extraBytesPerLine = bytesPerLine - curBitmap.width;
curBitmap.rawBitmap = data.subspan(14, curBitmap.width * curBitmap.height);
data += 14 + (curBitmap.height * bytesPerLine);
}

And what's the deal with if (bytesPerLine < curBitmap.width)? The message seems backwards.

Edit: In the scummvm code, the bytesPerLine and curWidth names are flipped, which explains why things still work in the ScummVM code.
« Last Edit: October 09, 2022, 06:06:27 PM by doomlazer »

Offline Kawa

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #3 on: October 09, 2022, 10:07:36 AM »
As far as I'm aware, KQ6 is the only Sierra game to have that kind of portrait.

Offline doomlazer

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #4 on: October 09, 2022, 03:35:02 PM »
I decided to take another look this morning and got things working properly - even skipping the pixels for bitmaps with some extraBytesPerLine:

Code: [Select]
package main

import (
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"io/ioutil"
"log"
"os"
"strings"
)

// Note: quick and dirty extraction script to pull out hires portraits from KQ6
// Usage: `go run main.go` in the directory of the .BIN files.
// Output: It will spit out all the relevant .png files

// ScummVM: https://github.com/scummvm/scummvm/blob/90f2ff2532ca71033b4393b9ce604c9b0e6cafa0/engines/sci/graphics/portrait.cpp
// REFERENCED: referenced: Portrait::init and Portrait::drawBitmap
// BUG: currently a bug exists where I can't properly loop through all bitmaps for a given picture...but I only wanted the first frame anyway...
// NOTE: None of the Rave, Lip-Sync stuff was implemented...don't need it.

type colors struct {
r byte
g byte
b byte
}

var portraits = []string{
"ALEX.BIN",
"ALLARIA.BIN",
//"ALLARIAD.BIN", // Redundant
"BEAST.BIN",
"BEAUTESS.BIN",
"BEAUTPEA.BIN",
"BOOKSH.BIN",
"BOOKWORM.BIN",
//"CALIPHID.BIN", // Redundant
"CALIPHIM.BIN",
"CASSIMA.BIN",
"CELESTE.BIN",
"FERRYM.BIN",
"GNOMES.BIN",
"GRAHAM.BIN",
"HEADDRU.BIN",
"JOLLO.BIN",
"LAMPSELL.BIN",
"PAWNSHOP.BIN",
"PRINCE.BIN",
"ROSELLA.BIN",
"SALADIN.BIN",
"SIGHT.BIN",
"SMELL.BIN",
//"SMELLNO.BIN", // Redundant
"SOUND.BIN",
//"SOUNDNO.BIN", // Redundant
"TASTE.BIN",
"TOUCH.BIN",
"VALANICE.BIN",
"VIZIER.BIN",
"WINGG.BIN",
}

func main() {
for _, fileName := range portraits {
processPortrait(fileName)
}
}

func processPortrait(fileName string) {
b, err := ioutil.ReadFile("ACTORS/" + fileName)
if err != nil {
log.Fatal("Couldn't open file with err!")
}

// Header

winHeader := string(b[0:3])
if winHeader != "WIN" {
log.Fatal("WIN Header not detected!")
}

// These ones commented out are kinda redundant.
//width := binary.LittleEndian.Uint16(b[3:])
//height := binary.LittleEndian.Uint16(b[5:])
bitmapSize := binary.LittleEndian.Uint16(b[7:])
//lipSyncIDCount := binary.LittleEndian.Uint16(b[11:])
portraitPaletteSize := binary.LittleEndian.Uint16(b[13:])

//fmt.Println(width, height, bitmapSize, lipSyncIDCount, portraitPaletteSize)

// Palette: starts at offset 17
var dataOffset = 17
palette := make([]colors, portraitPaletteSize)

var palSize uint16
var palNr uint16
for palSize < portraitPaletteSize {
palette[palNr].b = b[dataOffset]
dataOffset++
palette[palNr].g = b[dataOffset]
dataOffset++
palette[palNr].r = b[dataOffset]
dataOffset++

// ScummVM has these two lines hardcoded.
// _portraitPalette.colors[palNr].used = 1
// _portraitPalette.intensity[palNr] = 100

palNr += 1
palSize += 3
}

// Bitmap
var bitmapNr uint16
var bytesPerLine uint16

// Note: should only need FIRST bitmap in sequence.
for bitmapNr = 0; bitmapNr < bitmapSize; bitmapNr++ {
// Hmm width/height here redundant?
curWidth := binary.LittleEndian.Uint16(b[dataOffset+6:]) //changed from +2
curHeight := binary.LittleEndian.Uint16(b[dataOffset+4:])
bytesPerLine = binary.LittleEndian.Uint16(b[dataOffset+2:]) //changed from +6

if bytesPerLine > curWidth { //use correct sign
log.Fatal("bytesPerLine larger than actual width!")
}

extraBytesPerLine := curWidth - bytesPerLine //this was flipped
rawBitmap := b[dataOffset+14 : dataOffset+14+int(curWidth*curHeight)]

//fmt.Println("bitmapNr:", bitmapNr, "extraBytesPerLine:", extraBytesPerLine, "len(rawBitmap):", len(rawBitmap))
exportBitmap(fileName, bitmapNr, rawBitmap, palette, curWidth, curHeight, extraBytesPerLine, bytesPerLine) //send bytesPerLine

// Move dataOffset forward
dataOffset += int(14 + (curHeight * curWidth)) //changed from bytesPerLine

// BUG BOUNTY!!
// HACK just break on the first one...we have a bug to track down to get all files otherwise this code panics...don't care at the momment
// break, not needed
}
}

func exportBitmap(fileName string, nr uint16, bitmap []byte, palette []colors, width uint16, height uint16, extraBytesPerLine uint16, bytesPerLine uint16) { //add bytesPerLine
myImg := image.NewRGBA(image.Rect(0, 0, int(bytesPerLine), int(height))) //changed width to bytesPerLine

dataOffset := 0
reducedBitmap := bitmap[0 : width*height]

for y := 0; y < int(height); y++ {
for x := 0; x < int(bytesPerLine); x++ { //changed width to bytesPerLine
c := palette[reducedBitmap[dataOffset]]
myImg.SetRGBA(x, y, color.RGBA{
R: c.r,
G: c.g,
B: c.b,
A: 255,
})

dataOffset += 1
}
dataOffset += int(extraBytesPerLine)
}

newName := strings.Replace(fileName, ".BIN", "", -1)
out, err := os.Create(fmt.Sprintf(newName+"_%d.png", nr))
if err != nil {
log.Fatal(err)
}
err = png.Encode(out, myImg)
if err != nil {
log.Fatal(err)
}

err = out.Close()
if err != nil {
log.Fatal(err)
}
}

Edit: the names were better the other way I guess
« Last Edit: October 11, 2022, 12:46:30 AM by doomlazer »

Offline lskovlun

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #5 on: October 09, 2022, 03:40:11 PM »
As far as I'm aware, KQ6 is the only Sierra game to have that kind of portrait.
Yeah, RAVE was a licensed product:
https://patents.justia.com/assignee/bright-star-technology-inc

Offline Collector

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #6 on: October 09, 2022, 04:27:53 PM »
But isn't it the reason that Sierra bought Bright Star?
KQII Remake Pic

Offline lskovlun

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #7 on: October 09, 2022, 04:45:34 PM »
But isn't it the reason that Sierra bought Bright Star?
I'm not sure about the timing. KQ6 was the only SCI game that used Rave, but a number of Bright Star games (which one must assume used Rave in one capacity or another) now came under the Sierra imprint.

There's also more about Rave here. The first part deals with integration with HyperCard, the second part explains the part ("the RAVE driver") that KQ6 actually uses:
https://www.callapple.org/documentation/bright-stars-talking-heads-behind-the-scenes-with-hyperanimator/
(dated 1989)

Offline deckarep

Re: Quick and dirty script to extract King's Quest 6 .bin files
« Reply #8 on: October 09, 2022, 07:46:35 PM »
Hey thanks everyone who saw this and replied!

Special thanks to @Doomlazer for improving the script...I had a feeling my code was buggy and I honestly rage-wrote it because I just wanted to get back to my project and figured the ScummVM code was accurate since it all works well in the game.

Follow up edit: I confirmed Doomlazer's latest code exports all Bitmap assets! I also updated the gist to reflect the modified version.

I also started building an AGI engine a few months back: https://github.com/deckarep/zig-agi

And I also took some decent notes on using the ScummVM debugger if others find it helpful: https://deckarep.github.io/sci-hacking/

Thanks for allowing me into this community.

@deckarep
« Last Edit: October 09, 2022, 08:04:34 PM by deckarep »


SMF 2.0.19 | SMF © 2021, Simple Machines
Simple Audio Video Embedder

Page created in 0.045 seconds with 22 queries.