Files
bd2/go/internal/gamedata/database.go
T
2026-09-21 09:53:46 +08:00

80 lines
2.6 KiB
Go

package gamedata
import (
"archive/zip"
"bytes"
"crypto/sha1"
"fmt"
"io"
"path/filepath"
"strings"
"bd2server/internal/dbcrypt"
)
// DatabaseName maps a logical client DB name to its GameData archive entry.
// Version 1 is the current 2.34.13 DB schema generation.
func DatabaseName(logical string) (string, error) {
if logical == "" || strings.ContainsAny(logical, `/\\.`) {
return "", fmt.Errorf("gamedata: invalid logical database name %q", logical)
}
digest := sha1.Sum([]byte(logical + "_v1"))
return fmt.Sprintf("%X", digest), nil
}
// ReadDatabase extracts and decrypts one SQLite database from the validated
// common-dbdata.bin. The returned bytes are an ordinary SQLite file.
func ReadDatabase(root, version, logical string) ([]byte, error) {
name, err := DatabaseName(logical)
if err != nil {
return nil, err
}
return readEntry(root, version, name, logical)
}
// questDatabaseEntry is the member in the 2.34.13 GameData archive that
// contains the shared QuestTable* SQLite database, including QuestTable21.
// Quest data is not stored in a per-map "pack21" database.
const questDatabaseEntry = "9F251C63BC72551C681EE75D328FA090D56E444B"
// ReadQuestDatabase extracts the shared quest database. The member name is
// opaque in the client archive, so callers must not derive it from a map ID.
func ReadQuestDatabase(root, version string) ([]byte, error) {
return readEntry(root, version, questDatabaseEntry, "quest tables")
}
func readEntry(root, version, name, label string) ([]byte, error) {
archivePath := filepath.Join(root, version, "release", ArchiveName)
reader, err := zip.OpenReader(archivePath)
if err != nil {
return nil, fmt.Errorf("gamedata: open database archive: %w", err)
}
defer reader.Close()
for _, entry := range reader.File {
if !strings.EqualFold(entry.Name, name) {
continue
}
stream, err := entry.Open()
if err != nil {
return nil, fmt.Errorf("gamedata: open database %s: %w", label, err)
}
encrypted, readErr := io.ReadAll(stream)
closeErr := stream.Close()
if readErr != nil {
return nil, fmt.Errorf("gamedata: read database %s: %w", label, readErr)
}
if closeErr != nil {
return nil, fmt.Errorf("gamedata: close database %s: %w", label, closeErr)
}
plain, err := dbcrypt.DecryptPages(encrypted)
if err != nil {
return nil, fmt.Errorf("gamedata: decrypt database %s: %w", label, err)
}
if !bytes.HasPrefix(plain, dbcrypt.Header) {
return nil, fmt.Errorf("gamedata: database %s has no SQLite header after decryption", label)
}
return plain, nil
}
return nil, fmt.Errorf("gamedata: database %s entry %s not found", label, name)
}