From 997e2076d1528420034fe8c5361a724215824f75 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 2 Jan 2019 06:16:15 +0000 Subject: [PATCH] Start loading .fnt files. No display yet --- cmd/loader/main.go | 24 ++++++++ cmd/view-menu/main.go | 64 +++++++++++++------ internal/data/i18n.go | 85 +++++++++++++++++++++++++ internal/fonts/fonts.go | 133 ++++++++++++++++++++++++++++++++++++++++ internal/menus/menus.go | 24 ++++++++ internal/util/file.go | 29 +++++++++ 6 files changed, 340 insertions(+), 19 deletions(-) create mode 100644 internal/data/i18n.go create mode 100644 internal/fonts/fonts.go create mode 100644 internal/util/file.go diff --git a/cmd/loader/main.go b/cmd/loader/main.go index 06c3bc5..881fe38 100644 --- a/cmd/loader/main.go +++ b/cmd/loader/main.go @@ -8,6 +8,7 @@ import ( "strings" "ur.gs/ordoor/internal/data" + "ur.gs/ordoor/internal/fonts" "ur.gs/ordoor/internal/maps" "ur.gs/ordoor/internal/menus" "ur.gs/ordoor/internal/sets" @@ -31,6 +32,7 @@ func main() { loadMapsFrom("MultiMaps") loadSets() loadMenus() + loadFonts() } func loadData() { @@ -38,6 +40,7 @@ func loadData() { accountingPath := filepath.Join(dataPath, "Accounting.dat") genericDataPath := filepath.Join(dataPath, "GenericData.dat") aniObDefPath := filepath.Join(dataPath, "AniObDef.dat") + i18nPath := filepath.Join(dataPath, data.I18nFile) log.Printf("Loading %s...", accountingPath) accounting, err := data.LoadAccounting(accountingPath) @@ -62,6 +65,14 @@ func loadData() { } log.Printf("%s: %+v", genericDataPath, genericData) + + log.Printf("Loading %s...", i18nPath) + i18n, err := data.LoadI18n(i18nPath) + if err != nil { + log.Fatalf("Failed to parse %s: %s", i18nPath, err) + } + + log.Printf("%s: len=%v", i18nPath, i18n.Len()) } func loadObj() { @@ -165,3 +176,16 @@ func displayRecord(record *menus.Record, depth int) { displayRecord(child, depth+1) } } + +func loadFonts() { + fontsPath := filepath.Join(*gamePath, "Fonts") + + fonts, err := fonts.LoadFonts(fontsPath) + if err != nil { + log.Fatalf("Failed to parse %s/*.fnt as fonts: %v", fontsPath, err) + } + + for _, font := range fonts { + fmt.Printf(" * `%s`: obj=%v entries=%v\n", font.Name, font.ObjectFile, font.Entries()) + } +} diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index 9e35f57..815581e 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -12,6 +12,7 @@ import ( "ur.gs/ordoor/internal/conv" "ur.gs/ordoor/internal/data" + "ur.gs/ordoor/internal/fonts" "ur.gs/ordoor/internal/menus" "ur.gs/ordoor/internal/ui" ) @@ -25,6 +26,10 @@ type env struct { menu *menus.Menu objects []*conv.Object batch *pixel.Batch + + fonts []*fonts.Font + fontObjs []*conv.Object + fontBatch *pixel.Batch } type state struct { @@ -36,6 +41,26 @@ type state struct { winBounds pixel.Rect } +func loadObjects(names ...string) ([]*conv.Object, *pixel.Batch) { + var raw []*data.Object + + for _, name := range names { + objFile := filepath.Join(filepath.Dir(*menuFile), name) + obj, err := data.LoadObject(objFile) + if err != nil { + log.Fatalf("Failed to load %s: %v", name, err) + } + obj.Name = name + + raw = append(raw, obj) + } + + objects, spritesheet := conv.ConvertObjects(raw) + batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet) + + return objects, batch +} + func main() { flag.Parse() @@ -49,22 +74,27 @@ func main() { log.Fatalf("Couldn't load menu file %s: %v", *menuFile, err) } - rawObjs := []*data.Object{} - for _, name := range menu.ObjectFiles { - objFile := filepath.Join(filepath.Dir(*menuFile), name) - obj, err := data.LoadObject(objFile) - if err != nil { - log.Fatalf("Failed to load %s: %v", name, err) - } - obj.Name = name - - rawObjs = append(rawObjs, obj) + if i18n, err := data.LoadI18n(filepath.Join(*gamePath, "Data", data.I18nFile)); err != nil { + log.Printf("Failed to load i18n data, skipping internationalization: %v", err) + } else { + menu.Internationalize(i18n) } - objects, spritesheet := conv.ConvertObjects(rawObjs) - batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet) + var loadedFonts []*fonts.Font + for _, name := range menu.FontNames { + font, err := fonts.LoadFont(filepath.Join(*gamePath, "Fonts", name+".fnt")) + if err != nil { + log.Fatalf("Failed to load font %v: %v", name, err) + } + loadedFonts = append(loadedFonts, font) + } - env := &env{objects: objects, menu: menu, batch: batch} + menuObjs, menuBatch := loadObjects(menu.ObjectFiles...) + + env := &env{ + menu: menu, objects: menuObjs, batch: menuBatch, + fonts: loadedFonts, // TODO: load the objects and start displaying text + } // The main thread now belongs to pixelgl pixelgl.Run(env.run) @@ -131,10 +161,6 @@ func (s *state) present(pWin *pixelgl.Window) { } func (s *state) drawRecord(record *menus.Record, target pixel.Target) { - if !record.Active { - return - } - // Draw this record if it's valid to do so. FIXME: lots to learn if record.SpriteId >= 0 { x := float64(record.X) @@ -149,8 +175,8 @@ func (s *state) drawRecord(record *menus.Record, target pixel.Target) { } log.Printf( - "Drawing id=%v type=%v spriteid=%v x=%v y=%v", - record.Id, record.Type, record.SpriteId, x, y, + "Drawing id=%v type=%v spriteid=%v x=%v y=%v desc=%q parent=%p", + record.Id, record.Type, record.SpriteId, record.X, record.Y, record.Desc, record.Parent, ) // FIXME: Need to handle multiple objects diff --git a/internal/data/i18n.go b/internal/data/i18n.go new file mode 100644 index 0000000..f9a03c0 --- /dev/null +++ b/internal/data/i18n.go @@ -0,0 +1,85 @@ +package data + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "strconv" +) + +// WH40K has basic text internationalisation capabilities based on a .dta +// file that maps string IDs to messages +type I18n struct { + Name string + mapping map[int]string +} + +// FIXME: this should be put into the config file maybe, or detected from a list +// of possibilities? +const ( + I18nFile = "USEng.dta" +) + +func LoadI18n(filename string) (*I18n, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + + defer f.Close() + + out := &I18n{ + Name: filepath.Base(filename), + mapping: make(map[int]string), + } + + scanner := bufio.NewScanner(f) + for i := 0; scanner.Scan(); i++ { + // Remove comments from lines + line := bytes.TrimSpace(bytes.SplitN(scanner.Bytes(), []byte("//"), 2)[0]) + + // Ignore empty lines + if len(line) == 0 { + continue + } + + // Lines are expected to be in this format: + // "text that may include \" or not" + parts := bytes.SplitN(line, []byte(" "), 2) + if len(parts) != 2 { + return nil, fmt.Errorf("Bad line in %v at %v: %q", filename, i, scanner.Text()) + } + + num, err := strconv.Atoi(string(parts[0])) + if err != nil { + return nil, err + } + + // Cut off the leading and trailing quote characters of the string + val := parts[1] + val = val[1 : len(val)-1] + + // TODO: Replace certain escape characters with their literals? + + out.mapping[num] = string(val) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return out, nil +} + +func (n *I18n) Len() int { + return len(n.mapping) +} + +// Puts the internationalized string into `out` if `in` matches a known ID +func (n *I18n) Replace(in int, out *string) { + if str, ok := n.mapping[in]; ok { + *out = str + } +} diff --git a/internal/fonts/fonts.go b/internal/fonts/fonts.go new file mode 100644 index 0000000..7c3967d --- /dev/null +++ b/internal/fonts/fonts.go @@ -0,0 +1,133 @@ +package fonts + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + + "ur.gs/ordoor/internal/util" + "ur.gs/ordoor/internal/util/asciiscan" +) + +type Font struct { + Name string + // Contains the sprite data for the font. FIXME: load this? + ObjectFile string + + // Maps ASCII bytes to a sprite offset in the ObjectFile + mapping map[int]int +} + +func (f *Font) Entries() int { + return len(f.mapping) +} + +// Returns the offsets required to display a given string, returning an error if +// some of the runes in the string are unknown to the font +func (f *Font) Indices(s string) ([]int, error) { + out := make([]int, 0, len(s)) + + for i, b := range []byte(s) { + + offset, ok := f.mapping[int(b)] + if !ok { + return nil, fmt.Errorf("Unknown codepoint %v at offset %v in string %s", b, i, s) + } + + out = append(out, offset) + } + + return out, nil +} + +func LoadFont(filename string) (*Font, error) { + scanner, err := asciiscan.New(filename) + if err != nil { + return nil, err + } + + defer scanner.Close() + + // First, load the object file name + objFile, err := scanner.ConsumeString() + if err != nil { + return nil, err + } + + out := &Font{ + Name: filepath.Base(filename), + ObjectFile: objFile, + mapping: make(map[int]int), + } + + for { + str, err := scanner.ConsumeString() + if err != nil { + return nil, err + } + + parseErr := fmt.Errorf("Invalid entry in %v: %q", filename, str) + fields := strings.Fields(str) + + switch fields[0] { + case "done": + goto out + case "r": // A range of codepoints + if len(fields) < 5 { + return nil, parseErr + } + + cpStart, _ := strconv.Atoi(fields[1]) + cpEnd, _ := strconv.Atoi(fields[2]) + idxStart, _ := strconv.Atoi(fields[3]) + idxEnd, _ := strconv.Atoi(fields[4]) + size := idxEnd - idxStart + + // FIXME: I'd love this to be an error but several .fnt files do it + if cpEnd-cpStart != size { + fmt.Printf("WARNING: %v has mismatched codepoints and indices: %q\n", filename, str) + } + + for offset := 0; offset < size; offset++ { + out.mapping[cpStart+offset] = idxStart + offset + } + case "v": // A single codepoint, 4 fields + if len(fields) < 3 { + return nil, parseErr + } + + cp, _ := strconv.Atoi(fields[1]) + idx, _ := strconv.Atoi(fields[2]) + + out.mapping[cp] = idx + default: + return nil, parseErr + } + } + +out: + return out, nil +} + +func LoadFonts(dir string) (map[string]*Font, error) { + files, err := util.DirByExt(dir, ".fnt") + if err != nil { + return nil, err + } + + out := make(map[string]*Font, len(files)) + + for _, file := range files { + abs := filepath.Join(dir, file) + base := filepath.Base(file) + font, err := LoadFont(abs) + if err != nil { + return nil, fmt.Errorf("%s: %v", abs, err) + } + + out[base] = font + } + + return out, nil +} diff --git a/internal/menus/menus.go b/internal/menus/menus.go index c8507aa..4145d88 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -20,6 +20,7 @@ type Record struct { SpriteId int X int Y int + Desc string // FIXME: turn these into first-class data properties map[string]string @@ -174,7 +175,30 @@ func setProperty(r *Record, k, v string) { r.X = vInt case "Y-CORD": r.Y = vInt + case "DESC": + r.Desc = v default: r.properties[k] = v } } + +type Replacer interface { + Replace(int, *string) +} + +func (r *Record) Internationalize(replacer Replacer) { + id, err := strconv.Atoi(r.Desc) + if err == nil { + replacer.Replace(id, &r.Desc) + } + + for _, child := range r.Children { + child.Internationalize(replacer) + } +} + +func (m *Menu) Internationalize(replacer Replacer) { + for _, record := range m.Records { + record.Internationalize(replacer) + } +} diff --git a/internal/util/file.go b/internal/util/file.go new file mode 100644 index 0000000..11f6d47 --- /dev/null +++ b/internal/util/file.go @@ -0,0 +1,29 @@ +package util + +import ( + "io/ioutil" + "path/filepath" + "strings" +) + +// DirByExt returns entries in a directory with the specified extension +func DirByExt(dir, ext string) ([]string, error) { + fis, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + out := make([]string, 0, len(fis)) + + for _, fi := range fis { + relname := fi.Name() + extname := filepath.Ext(relname) + + // Skip anything that doesn't match the extension + if strings.EqualFold(extname, ext) { + out = append(out, relname) + } + } + + return out, nil +}