Start loading .fnt files. No display yet

This commit is contained in:
2019-01-02 06:16:15 +00:00
parent 568365be3a
commit 997e2076d1
6 changed files with 340 additions and 19 deletions

85
internal/data/i18n.go Normal file
View File

@@ -0,0 +1,85 @@
package data
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"strconv"
)
// WH40K has basic text internationalisation capabilities based on a <lang>.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:
// <number> "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
}
}

133
internal/fonts/fonts.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
}

29
internal/util/file.go Normal file
View File

@@ -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
}