Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -0,0 +1,25 @@
package format
// DataType classifies entries.
type DataType byte
const (
// DataTypeEndOfMedia marks the last entry.
DataTypeEndOfMedia = DataType(0)
// DataTypeLowResVideo for low resolution (low compression) video.
DataTypeLowResVideo = DataType(0x21)
// DataTypeHighResVideo for high resolution (high compression) video.
DataTypeHighResVideo = DataType(0x79)
// DataTypeAudio marks an audio entry.
DataTypeAudio = DataType(2)
// DataTypeSubtitle control for subtitle entries.
DataTypeSubtitle = DataType(3)
// DataTypePalette describes a color palette.
DataTypePalette = DataType(4)
// DataTypePaletteReset is a zero-byte entry immediately before a Palette entry.
DataTypePaletteReset = DataType(0x4C)
// DataTypePaletteLookupList for high compression video.
DataTypePaletteLookupList = DataType(5)
// DataTypeControlDictionary for high compression video.
DataTypeControlDictionary = DataType(0x0D)
)
@@ -0,0 +1,305 @@
package format
import (
"bytes"
"io"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
"github.com/inkyblackness/hacked/ss1/serial"
)
// EntryBucketPriority describes the general priority of the bucket.
// Lower numbers are put before higher numbers if the bucket has the same timestamp.
type EntryBucketPriority int
// List of entry bucket priorities.
const (
EntryBucketPriorityAudio = 0
EntryBucketPriorityVideoControl = 1
EntryBucketPrioritySubtitleControl = 2
EntryBucketPrioritySubtitle = 3
EntryBucketPriorityFrame = 4
)
// EntryBucket is a set of entries that need to be together within the stream.
// This exists mainly to cover special cases of entries within a stream that have no valid timestamp.
type EntryBucket struct {
// Priority helps in merging content from multiple buckets.
Priority EntryBucketPriority
// Timestamp specifies at which time the bucket should be inserted into the stream.
// This is typically derived from one of the timestamps of the contained entries.
Timestamp Timestamp
// Entries is the list of entries within this bucket.
Entries []Entry
}
// Entry describes a timestamped block from a MOVI container.
type Entry struct {
// Timestamp marks the beginning time of the entry.
Timestamp Timestamp
// Data is a type specific information of a movie.
Data EntryData
}
// EntryData describes one entry in a MOVI container.
type EntryData interface {
// Type describes the content type of the data.
Type() DataType
// Bytes returns the raw bytes of the entry.
Bytes() []byte
}
// UnknownEntryData is an entry that is not know to this codebase.
type UnknownEntryData struct {
DataType DataType
Raw []byte
}
// UnknownEntryFrom decodes an entry from given data.
func UnknownEntryFrom(dataType DataType, r io.Reader, dataSize int) (UnknownEntryData, error) {
data := UnknownEntryData{
DataType: dataType,
Raw: make([]byte, dataSize),
}
coder := serial.NewDecoder(r)
coder.Code(data.Raw)
return data, coder.FirstError()
}
// Type describes the entry.
func (data UnknownEntryData) Type() DataType {
return data.DataType
}
// Bytes returns the raw bytes of the entry.
func (data UnknownEntryData) Bytes() []byte {
return data.Raw
}
// LowResVideoEntryData is a compressed low-resolution video frame.
type LowResVideoEntryData struct {
BoundingBox [4]uint16
Packed []byte
}
// LowResVideoEntryFrom decodes an entry from given data.
func LowResVideoEntryFrom(r io.Reader, dataSize int) (LowResVideoEntryData, error) {
var data LowResVideoEntryData
coder := serial.NewDecoder(r)
coder.Code(&data.BoundingBox)
data.Packed = make([]byte, dataSize-int(coder.CurPos()))
coder.Code(data.Packed)
return data, coder.FirstError()
}
// Type describes the entry.
func (data LowResVideoEntryData) Type() DataType {
return DataTypeLowResVideo
}
// Bytes returns the raw bytes of the entry.
func (data LowResVideoEntryData) Bytes() []byte {
buf := bytes.NewBuffer(nil)
coder := serial.NewEncoder(buf)
coder.Code(data.BoundingBox)
coder.Code(data.Packed)
return buf.Bytes()
}
// HighResVideoEntryData is a compressed high-resolution video frame.
type HighResVideoEntryData struct {
Bitstream []byte
Maskstream []byte
}
// HighResVideoEntryFrom decodes an entry from given data.
func HighResVideoEntryFrom(r io.Reader, dataSize int) (HighResVideoEntryData, error) {
var data HighResVideoEntryData
coder := serial.NewDecoder(r)
var maskstreamOffset uint16
coder.Code(&maskstreamOffset)
data.Bitstream = make([]byte, int(maskstreamOffset)-int(coder.CurPos()))
coder.Code(data.Bitstream)
data.Maskstream = make([]byte, dataSize-int(coder.CurPos()))
coder.Code(data.Maskstream)
return data, coder.FirstError()
}
// Type describes the entry.
func (data HighResVideoEntryData) Type() DataType {
return DataTypeHighResVideo
}
// Bytes returns the raw bytes of the entry.
func (data HighResVideoEntryData) Bytes() []byte {
buf := bytes.NewBuffer(nil)
coder := serial.NewEncoder(buf)
maskstreamOffset := uint16(len(data.Bitstream)) + 2
coder.Code(&maskstreamOffset)
coder.Code(data.Bitstream)
coder.Code(data.Maskstream)
return buf.Bytes()
}
// AudioEntryData is an entry with audio samples.
type AudioEntryData struct {
Samples []byte
}
// AudioEntryFrom decodes an entry from given data.
func AudioEntryFrom(r io.Reader, dataSize int) (AudioEntryData, error) {
var data AudioEntryData
data.Samples = make([]byte, dataSize)
coder := serial.NewDecoder(r)
coder.Code(data.Samples)
return data, coder.FirstError()
}
// Type describes the entry.
func (data AudioEntryData) Type() DataType {
return DataTypeAudio
}
// Bytes returns the raw bytes of the entry.
func (data AudioEntryData) Bytes() []byte {
return data.Samples
}
// SubtitleEntryData is an entry with Subtitle samples.
type SubtitleEntryData struct {
Control SubtitleControl
Text []byte
}
// SubtitleEntryFrom decodes an entry from given data.
func SubtitleEntryFrom(r io.Reader, dataSize int) (SubtitleEntryData, error) {
var data SubtitleEntryData
coder := serial.NewDecoder(r)
var header SubtitleHeader
coder.Code(&header)
data.Control = header.Control
if header.TextOffset > coder.CurPos() {
coder.Code(make([]byte, header.TextOffset-coder.CurPos()))
}
data.Text = make([]byte, dataSize-int(coder.CurPos()))
coder.Code(data.Text)
return data, coder.FirstError()
}
// Type describes the entry.
func (data SubtitleEntryData) Type() DataType {
return DataTypeSubtitle
}
// Bytes returns the raw bytes of the entry.
func (data SubtitleEntryData) Bytes() []byte {
header := SubtitleHeader{
Control: data.Control,
TextOffset: SubtitleDefaultTextOffset,
}
buf := bytes.NewBuffer(nil)
coder := serial.NewEncoder(buf)
coder.Code(&header)
coder.Code(make([]byte, int(header.TextOffset-coder.CurPos())))
coder.Code(data.Text)
return buf.Bytes()
}
// PaletteEntryData is an entry with a palette.
type PaletteEntryData struct {
Colors bitmap.Palette
}
// PaletteEntryFrom decodes an entry from given data.
func PaletteEntryFrom(r io.Reader) (PaletteEntryData, error) {
var data PaletteEntryData
coder := serial.NewDecoder(r)
coder.Code(&data.Colors)
return data, coder.FirstError()
}
// Type describes the entry.
func (data PaletteEntryData) Type() DataType {
return DataTypePalette
}
// Bytes returns the raw bytes of the entry.
func (data PaletteEntryData) Bytes() []byte {
buf := bytes.NewBuffer(nil)
coder := serial.NewEncoder(buf)
coder.Code(&data.Colors)
return buf.Bytes()
}
// PaletteResetEntryData marks a reset of the palette.
type PaletteResetEntryData struct {
}
// PaletteResetEntryFrom decodes an entry from given data.
func PaletteResetEntryFrom() (PaletteResetEntryData, error) {
return PaletteResetEntryData{}, nil
}
// Type describes the entry.
func (data PaletteResetEntryData) Type() DataType {
return DataTypePaletteReset
}
// Bytes serializes the entry.
func (data PaletteResetEntryData) Bytes() []byte {
return nil
}
// PaletteLookupEntryData contains a list of palette indices.
type PaletteLookupEntryData struct {
List []byte
}
// PaletteLookupEntryFrom decodes an entry from given data.
func PaletteLookupEntryFrom(r io.Reader, dataSize int) (PaletteLookupEntryData, error) {
var data PaletteLookupEntryData
data.List = make([]byte, dataSize)
coder := serial.NewDecoder(r)
coder.Code(data.List)
return data, coder.FirstError()
}
// Type describes the entry.
func (data PaletteLookupEntryData) Type() DataType {
return DataTypePaletteLookupList
}
// Bytes serializes the entry.
func (data PaletteLookupEntryData) Bytes() []byte {
return data.List
}
// ControlDictionaryEntryData contains control words for compression.
type ControlDictionaryEntryData struct {
Words []compression.ControlWord
}
// ControlDictionaryEntryFrom decodes an entry from given data.
func ControlDictionaryEntryFrom(r io.Reader, dataSize int) (ControlDictionaryEntryData, error) {
var data ControlDictionaryEntryData
coder := serial.NewDecoder(r)
b := make([]byte, dataSize)
coder.Code(b)
if coder.FirstError() != nil {
return data, coder.FirstError()
}
var err error
data.Words, err = compression.UnpackControlWords(b)
return data, err
}
// Type describes the entry.
func (data ControlDictionaryEntryData) Type() DataType {
return DataTypeControlDictionary
}
// Bytes serializes the entry.
func (data ControlDictionaryEntryData) Bytes() []byte {
return compression.PackControlWords(data.Words)
}
@@ -0,0 +1,73 @@
package format
import (
"fmt"
"math"
)
// Tag is the identifier key of a MOVI container.
const Tag string = "MOVI"
// HeaderSize specifies the length of a serialized Header in bytes.
const HeaderSize = 256
// Fix represents a fixed-point number, with 16 bits of fraction and 15 bits of integer number.
type Fix struct {
Fraction uint16
Number int16
}
// FixFromFloat returns a fixed-point number from given floating-point number.
func FixFromFloat(value float32) Fix {
number := int16(value)
return Fix{
Fraction: uint16(math.Abs(float64(value)-float64(number)) * 0x10000),
Number: number,
}
}
// ToFloat returns the equivalent floating-point number.
func (f Fix) ToFloat() float32 {
return float32(f.Number) + float32(f.Fraction)/0x10000
}
// Header is the first entry of a MOVI container.
type Header struct {
// Tag is the identifier key of a MOVI container
Tag [4]byte
// IndexEntryCount specifies how many index entries are in the index.
IndexEntryCount int32
// IndexSize specifies how long the index is.
IndexSize int32
// ContentSize specifies the length of the content.
ContentSize int32
// Duration is the length of the media in seconds.
Duration Fix
// VideoFrameRate gives a hint on number of frames per second.
VideoFrameRate Fix
// VideoWidth specifies the width of the video.
VideoWidth uint16
// VideoHeight specifies the height of the video.
VideoHeight uint16
// VideoBitsPerPixel specifies bit count for color information: 8, 15, 24.
VideoBitsPerPixel int16
// VideoPalettePresent specifies whether a palette is set.
VideoPalettePresent int16
// AudioChannelCount specifies whether there is audio, and how many channels to use. 0 = no audio, 1 = mono, 2 = stereo.
AudioChannelCount int16
// AudioBytesPerSample specifies how many bytes one audio sample has. 1 = 8-bit, 2 = 16-bit.
AudioBytesPerSample int16
// AudioSampleRate specifies the sample rate of the audio.
AudioSampleRate Fix
_ [216]byte
}
func (header *Header) String() (result string) {
result += fmt.Sprintf("Index: %d entries (%d bytes)\n", header.IndexEntryCount, header.IndexSize)
result += fmt.Sprintf("Content: %d bytes, %f seconds\n", header.ContentSize, header.Duration.ToFloat())
result += fmt.Sprintf("Video: %dx%d pixel @%f sample rate\n", header.VideoWidth, header.VideoHeight, header.AudioSampleRate.ToFloat())
return
}
@@ -0,0 +1,22 @@
package format_test
import (
"bytes"
"encoding/binary"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
)
func TestHeaderSerializesToProperLength(t *testing.T) {
source := bytes.NewReader(make([]byte, 0x200))
var header format.Header
_ = binary.Read(source, binary.LittleEndian, &header)
curPos, _ := source.Seek(0, io.SeekCurrent)
assert.Equal(t, int64(format.HeaderSize), curPos)
}
@@ -0,0 +1,26 @@
package format
// IndexTableEntrySize specifies how many bytes a serialized IndexTableEntry uses.
const IndexTableEntrySize = 8
const indexHeaderSizeIncrement = 0x0400
// IndexTableEntry describes one data entry of a MOVI container.
type IndexTableEntry struct {
TimestampFraction uint16
TimestampSecond byte
Type byte
DataOffset int32
}
// IndexTableSizeFor returns the number of bytes reserved for given amount of table entries.
func IndexTableSizeFor(entryCount int) int {
size := indexHeaderSizeIncrement
requiredSize := entryCount * IndexTableEntrySize
if requiredSize > size {
size *= requiredSize/size + 2
}
return size
}
@@ -0,0 +1,26 @@
package format_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
)
func TestIndexTableSizeFor_ExistingSizes(t *testing.T) {
// These sample values are always the minimum and maximum amount of index entries
// found for a given index size.
assert.Equal(t, 0x0400, format.IndexTableSizeFor(3))
assert.Equal(t, 0x0400, format.IndexTableSizeFor(127))
assert.Equal(t, 0x0C00, format.IndexTableSizeFor(130))
assert.Equal(t, 0x0C00, format.IndexTableSizeFor(218))
assert.Equal(t, 0x1C00, format.IndexTableSizeFor(738))
assert.Equal(t, 0x1C00, format.IndexTableSizeFor(755))
assert.Equal(t, 0x3400, format.IndexTableSizeFor(1475))
assert.Equal(t, 0x3400, format.IndexTableSizeFor(1523))
}
@@ -0,0 +1,33 @@
package format
import "github.com/inkyblackness/hacked/ss1/resource"
// SubtitleControl specifies how to interpret a subtitle entry.
type SubtitleControl uint32
// Subtitle constants are based on four-letter ASCII sequences.
const (
SubtitleArea = SubtitleControl(0x41455241)
SubtitleTextStd = SubtitleControl(0x20445453)
SubtitleTextFrn = SubtitleControl(0x204E5246)
SubtitleTextGer = SubtitleControl(0x20524547)
)
// String returns the string presentation of the control value.
func (ctrl SubtitleControl) String() string {
return string([]rune{rune((ctrl >> 0) & 0xFF), rune((ctrl >> 8) & 0xFF), rune((ctrl >> 16) & 0xFF), rune((ctrl >> 24) & 0xFF)})
}
// SubtitleControlForLanguage returns the corresponding subtitle control for given language.
func SubtitleControlForLanguage(lang resource.Language) SubtitleControl {
switch lang {
case resource.LangDefault:
return SubtitleTextStd
case resource.LangFrench:
return SubtitleTextFrn
case resource.LangGerman:
return SubtitleTextGer
default:
panic("unknown language")
}
}
@@ -0,0 +1,12 @@
package format
// SubtitleDefaultTextOffset is the typical offset, in bytes, to text content.
const SubtitleDefaultTextOffset = 16
// SubtitleHeader is the header structure of a subtitle data.
type SubtitleHeader struct {
// Control specifies how to interpret the string content
Control SubtitleControl
// TextOffset specifies the offset, in bytes from beginning of entry data, where the text starts.
TextOffset uint32
}
@@ -0,0 +1,86 @@
package format
import (
"math"
"time"
)
const fractionDivisor = 0x10000
// Timestamp represents a point in time using fixed resolution.
type Timestamp struct {
Second uint8
Fraction uint16
}
// TimestampLimit returns the highest possible timestamp value.
func TimestampLimit() Timestamp {
return Timestamp{
Second: math.MaxUint8,
Fraction: math.MaxUint16,
}
}
// TimestampFromSeconds creates a timestamp instance from given floating point value.
func TimestampFromSeconds(value float32) Timestamp {
second := uint8(value)
return Timestamp{
Second: second,
Fraction: uint16((value - float32(second)) * fractionDivisor),
}
}
// TimestampFromDuration creates a timestamp instance from given duration value.
func TimestampFromDuration(d time.Duration) Timestamp {
limit := TimestampLimit()
if d >= limit.ToDuration() {
return limit
}
return Timestamp{
Second: uint8(d / time.Second),
Fraction: uint16(((d - time.Second) * fractionDivisor) / time.Second),
}
}
// ToDuration returns the equivalent duration for this timestamp.
func (ts Timestamp) ToDuration() time.Duration {
return (time.Duration(ts.Second) * time.Second) + (time.Duration(ts.Fraction)*time.Second)/fractionDivisor
}
// IsAfter returns true if this timestamp is later than the given one.
func (ts Timestamp) IsAfter(other Timestamp) bool {
return (ts.Second > other.Second) || ((ts.Second == other.Second) && (ts.Fraction > other.Fraction))
}
// IsBefore returns true if this timestamp is before the given one.
func (ts Timestamp) IsBefore(other Timestamp) bool {
return (ts.Second < other.Second) || ((ts.Second == other.Second) && (ts.Fraction < other.Fraction))
}
// Plus returns a timestamp with the given one added to the current one.
// The result is saturated if the addition is larger than the timestamp can hold.
func (ts Timestamp) Plus(other Timestamp) Timestamp {
return TimestampFromDuration(ts.ToDuration() + other.ToDuration())
}
// Minus returns a timestamp with given one removed from the current one.
// The result is clipped to 0 if subtraction would result in a negative number.
func (ts Timestamp) Minus(other Timestamp) Timestamp {
otherLinear := other.toLinear()
tsLinear := ts.toLinear()
if otherLinear >= tsLinear {
return timestampFromLinear(0)
}
return timestampFromLinear(tsLinear - otherLinear)
}
func (ts Timestamp) toLinear() uint32 {
return uint32(ts.Second)<<16 + uint32(ts.Fraction)
}
func timestampFromLinear(value uint32) Timestamp {
return Timestamp{
Second: uint8(value >> 16),
Fraction: uint16(value),
}
}
@@ -0,0 +1,66 @@
package format_test
import (
"fmt"
"math"
"testing"
"github.com/stretchr/testify/assert"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
)
func TestTimestampFromSecond(t *testing.T) {
ts := format.TimestampFromSeconds(7.25)
assert.Equal(t, byte(7), ts.Second)
assert.Equal(t, uint16(0x4000), ts.Fraction)
}
func TestTimeToRawB(t *testing.T) {
ts := format.TimestampFromSeconds(255.75)
assert.Equal(t, byte(255), ts.Second)
assert.Equal(t, uint16(0xC000), ts.Fraction)
}
func TestDurationConversion(t *testing.T) {
for fraction := 0; fraction <= math.MaxUint16; fraction++ {
ts := format.Timestamp{Second: 0, Fraction: uint16(fraction)}
dur := ts.ToDuration()
ts2 := format.TimestampFromDuration(dur)
assert.Equal(t, ts, ts2, fmt.Sprintf("Mismatch for fraction %v", fraction))
}
}
func TestTimestampPlus(t *testing.T) {
tt := []struct {
a format.Timestamp
b format.Timestamp
expected format.Timestamp
}{
{
a: format.Timestamp{Second: 0, Fraction: 0},
b: format.Timestamp{Second: 1, Fraction: 0x8000},
expected: format.Timestamp{Second: 1, Fraction: 0x8000},
},
{
a: format.Timestamp{Second: 2, Fraction: 0x8000},
b: format.Timestamp{Second: 1, Fraction: 0x8000},
expected: format.Timestamp{Second: 4, Fraction: 0x0000},
},
{
a: format.Timestamp{Second: 0xFF, Fraction: 0x0000},
b: format.Timestamp{Second: 2, Fraction: 0x8000},
expected: format.Timestamp{Second: 0xFF, Fraction: 0xFFFF},
},
}
for index, tc := range tt {
tc := tc
t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) {
result := tc.a.Plus(tc.b)
assert.Equal(t, tc.expected, result)
})
}
}