Created
June 10, 2025 20:10
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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