Skip to content

Instantly share code, notes, and snippets.

@cetteup
Created June 10, 2025 20:10
Show Gist options
  • Save cetteup/85b0c52a398b714a632ca60dfc350422 to your computer and use it in GitHub Desktop.
Save cetteup/85b0c52a398b714a632ca60dfc350422 to your computer and use it in GitHub Desktop.
Go-based parser example for GameSettings.bin used by Battlefield: Bad Company 2
package main
import (
"bytes"
"encoding/binary"
"io"
"log"
"os"
"path/filepath"
"golang.org/x/sys/windows"
)
func main() {
log.SetFlags(log.Lshortfile)
log.SetOutput(os.Stdout)
// Get path to documents folder (in a dynamic way, as users can reconfigure the actual path)
path, err := windows.KnownFolderPath(windows.FOLDERID_Documents, windows.KF_FLAG_DEFAULT)
if err != nil {
log.Fatalf("Failed to determine user home dir: %v", err)
}
// Append remaining path elements
path = filepath.Join(path, "BFBC2", "GameSettings.bin")
// Read GameSettings.bin file
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("Failed to read settings file: %v", err)
}
b := newBuffer(data)
// Check header (first 12 bytes, seem to be static)
header := b.Next(12)
if !bytes.Equal(header, []byte{0x08, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x0d,0x00, 0x00, 0x00}) {
log.Fatalf("Invalid file header: % x", header)
}
// Read file content size
length, err := b.ReadUint32()
if err != nil {
log.Fatalf("Failed to read file content length: %v", err)
} else if int(length) != b.Len() {
log.Fatalf("Indidicated file content length does not match actual length: %v", err)
}
// File content is split into sections
// Each section starts with a 4-byte integer indicating the number of contained key-value pairs
// Sections can and will be empty
for b.Len() >= 4 {
n, err2 := b.ReadUint32()
if err2 != nil {
log.Fatalf("Failed to read section length: %v", err2)
}
// Read all key-value pairs in the section
for i := 0; i < int(n); i++ {
// Every key-value pair is prefixed with what seems to be a value type indicator
// 1 = float
// 2 = integer (signed)
// 3 = string
kind, err3 := b.ReadUint32()
if err3 != nil {
log.Fatalf("Failed to read value type: %v", err2)
}
key, err3 := b.ReadPascalString()
if err3 != nil {
log.Fatalf("Failed to read key: %v", err2)
}
// Value is always stored as a pascal string regardless of the indicated type
value, err3 := b.ReadPascalString()
if err3 != nil {
log.Fatalf("Failed to read value: %v", err2)
}
log.Println("Read key", key, "of type", kind, "with value", value)
}
}
}
type buffer struct {
bytes.Buffer
}
func newBuffer(buf []byte) *buffer {
return &buffer{
Buffer: *bytes.NewBuffer(buf),
}
}
func (b *buffer) ReadUint32() (uint32, error) {
p := make([]byte, 4)
if n, err := b.Read(p); err != nil {
return 0, err
} else if n != 4 {
return 0, io.ErrUnexpectedEOF
}
return binary.LittleEndian.Uint32(p), nil
}
func (b *buffer) ReadPascalString() (string, error) {
// Read pascal string length
l, err := b.ReadUint32()
if err != nil {
return "", err
}
// Read string and trailing nil byte
p := make([]byte, l)
n, err := b.Read(p)
if err != nil {
return "", err
} else if n != int(l) {
return "", io.ErrUnexpectedEOF
}
// Return string without trailing nil byte
return string(p[:len(p)-1]), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment