Start loading .fnt files. No display yet
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
@@ -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 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)
|
||||
}
|
||||
|
||||
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 %s: %v", name, err)
|
||||
log.Fatalf("Failed to load font %v: %v", name, err)
|
||||
}
|
||||
obj.Name = name
|
||||
|
||||
rawObjs = append(rawObjs, obj)
|
||||
loadedFonts = append(loadedFonts, font)
|
||||
}
|
||||
|
||||
objects, spritesheet := conv.ConvertObjects(rawObjs)
|
||||
batch := pixel.NewBatch(&pixel.TrianglesData{}, spritesheet)
|
||||
menuObjs, menuBatch := loadObjects(menu.ObjectFiles...)
|
||||
|
||||
env := &env{objects: objects, menu: menu, batch: batch}
|
||||
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
|
||||
|
85
internal/data/i18n.go
Normal file
85
internal/data/i18n.go
Normal 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
133
internal/fonts/fonts.go
Normal 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
|
||||
}
|
@@ -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
29
internal/util/file.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user