Documentation
¶
Overview ¶
Package tdf implements reading and writing of Total Annihilation TDF/FBI files.
TDF (Text Data File) and FBI (Feature/Building/Item) files share the same format: - Section-based structure with [SECTION_NAME] headers - Key-value pairs in format: key=value; - Values enclosed in braces { } - Case-insensitive keys - Support for string, numeric, and list values
Example usage:
// Reading
doc, err := tdf.ParseFile("units/ARMCOM.FBI")
section := doc.Section("UNITINFO")
name := section.String("UnitName")
cost := section.Int("BuildCostMetal")
// Writing
doc := tdf.NewDocument()
unit := doc.AddSection("UNITINFO")
unit.SetString("UnitName", "ARMCOM")
unit.SetInt("BuildCostMetal", 2500)
doc.WriteFile("output.fbi")
Index ¶
- func Canonicalize(data []byte) ([]byte, error)
- func Marshal(v any) ([]byte, error)
- func MisparsedKeys(v any) []string
- func SemanticEqual(a, b []byte) (bool, string)
- func Unmarshal(data []byte, v any) error
- type Decoder
- type Document
- func (d *Document) AddSection(name string) *Section
- func (d *Document) HasSection(name string) bool
- func (d *Document) Section(name string) *Section
- func (d *Document) Sections() []*Section
- func (d *Document) String() string
- func (d *Document) Write(w io.Writer) error
- func (d *Document) WriteFile(path string) error
- type Encoder
- type Field
- type ScalarMarshaler
- type ScalarUnmarshaler
- type Section
- func (s *Section) Bool(key string) bool
- func (s *Section) Fields() []*Field
- func (s *Section) Float(key string) float64
- func (s *Section) Get(key string) (string, bool)
- func (s *Section) Has(key string) bool
- func (s *Section) Int(key string) int
- func (s *Section) List(key string) []string
- func (s *Section) Name() string
- func (s *Section) Sections() []*Section
- func (s *Section) Set(key, value string)
- func (s *Section) SetBool(key string, value bool)
- func (s *Section) SetFloat(key string, value float64)
- func (s *Section) SetInt(key string, value int)
- func (s *Section) SetList(key string, values []string)
- func (s *Section) SetString(key, value string)
- func (s *Section) String(key string) string
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Canonicalize ¶
Canonicalize parses TDF bytes and re-emits them with normalised whitespace and comments stripped, preserving every section and field value verbatim. Running it twice is idempotent, which makes it a useful lossless round-trip check.
func Marshal ¶
Marshal renders v as TDF text. v must be a struct, a slice of structs, or a pointer to either. A slice produces one top-level [section] per element (named by its `tdf:",name"` field); a struct produces its tagged fields and nested sections at the top level. The inverse of Unmarshal.
func MisparsedKeys ¶
MisparsedKeys walks an already-decoded value and reports keys that fell through to a ",remaining" catch-all map even though the enclosing struct declares a typed field for that key. Unmarshal routes a value to the catch-all when it cannot be parsed into its declared field type (see decodeFieldChild), so the document still round-trips losslessly. Because the value is preserved, a byte-level round-trip check cannot reveal the mismatch — but it means the value landed in the catch-all instead of the field it was meant for, which is almost always a mis-typed struct field (or genuinely malformed game data).
Each result is formatted as "Type.key=value".
func SemanticEqual ¶
SemanticEqual reports whether two TDF documents are equivalent ignoring comments, whitespace, key/section-name case, field ordering, and numeric formatting (".6" == "0.60" == "0.6"). A field that is absent on one side is treated as equal to a zero-valued field on the other (a missing numeric or string key defaults to zero/empty in the engine), which lets `omitempty` struct fields round-trip semantically. When the documents differ, the second return value describes the first mismatch found.
func Unmarshal ¶
Unmarshal parses TDF/FBI/GUI bytes into v, which must be a non-nil pointer to a struct or to a slice of structs.
When v points to a slice, each top-level [section] becomes one element; a field tagged `tdf:",name"` on the element struct receives the section header. When v points to a struct, the document's top-level sections and fields are matched against that struct's tagged fields.
Field mapping by Go type:
- string / numeric / bool (and pointers to them): key=value
- []string / []int ...: a single space-separated value
- struct / *struct: a nested [name]{ } section
- []struct: repeated [name]{ } sections (matched by exact name, or by name prefix when several share a stem like GADGET0, GADGET1)
- map[string]scalar: a section whose keys are dynamic (e.g. [DAMAGE])
- map[string]string tagged `,remaining`: catch-all for unmatched keys
Types ¶
type Decoder ¶
type Decoder struct {
// contains filtered or unexported fields
}
Decoder reads a TDF document from an io.Reader. A slice target is filled one top-level [section] at a time as the reader is consumed, so the whole input never has to be buffered into a single string the way Unmarshal does.
func NewDecoder ¶
NewDecoder returns a Decoder that reads from r.
func (*Decoder) Decode ¶
Decode parses the document into v, which must be a non-nil pointer to a struct or to a slice of structs, exactly as for Unmarshal. A struct target gathers every top-level element before decoding (fields may appear in any order); a slice target decodes each section as it is read.
type Document ¶
type Document struct {
// contains filtered or unexported fields
}
Document represents a complete TDF/FBI file
func NewDocument ¶
func NewDocument() *Document
NewDocument creates a new empty TDF document
Example ¶
package main
import (
"fmt"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
doc := tdf.NewDocument()
unit := doc.AddSection("UNITINFO")
unit.SetString("UnitName", "CUSTOMBOT")
unit.SetString("Side", "ARM")
unit.SetInt("BuildCostMetal", 150)
unit.SetFloat("MaxVelocity", 3.0)
unit.SetBool("canmove", true)
unit.SetList("Category", []string{"ARM", "KBOT", "WEAPON"})
fmt.Print(doc.String())
}
Output: [UNITINFO] { UnitName=CUSTOMBOT; Side=ARM; BuildCostMetal=150; MaxVelocity=3; canmove=1; Category=ARM KBOT WEAPON; }
func Parse ¶
Parse parses TDF content from a reader
Example ¶
package main
import (
"fmt"
"log"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
content := `[UNITINFO]
{
UnitName=ARMCOM;
Side=ARM;
Description=Commander;
BuildCostMetal=2500;
MaxVelocity=1.15;
Builder=1;
Category=ARM COMMANDER CTRL_C;
}`
doc, err := tdf.ParseString(content)
if err != nil {
log.Fatal(err)
}
unit := doc.Section("UNITINFO")
fmt.Println("Unit:", unit.String("UnitName"))
fmt.Println("Side:", unit.String("Side"))
fmt.Println("Cost:", unit.Int("BuildCostMetal"))
fmt.Println("Speed:", unit.Float("MaxVelocity"))
fmt.Println("Builder:", unit.Bool("Builder"))
fmt.Println("Categories:", unit.List("Category"))
}
Output: Unit: ARMCOM Side: ARM Cost: 2500 Speed: 1.15 Builder: true Categories: [ARM COMMANDER CTRL_C]
func ParseString ¶
ParseString parses TDF content from a string
func (*Document) AddSection ¶
AddSection adds a new section or returns existing one
func (*Document) HasSection ¶
HasSection checks if a section exists
func (*Document) Section ¶
Section returns a section by name (case-insensitive)
Example ¶
package main
import (
"fmt"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
content := `[MISSION0]
{
missionfile=example.ufo;
missionname=Build a vehicle plant;
}`
doc, _ := tdf.ParseString(content)
mission := doc.Section("MISSION0")
if mission != nil {
fmt.Println("File:", mission.String("missionfile"))
fmt.Println("Name:", mission.String("missionname"))
}
}
Output: File: example.ufo Name: Build a vehicle plant
func (*Document) WriteFile ¶
WriteFile writes the document to a file
Example ¶
package main
import (
"fmt"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
doc := tdf.NewDocument()
feature := doc.AddSection("MetalDeposit")
feature.SetString("world", "greenworld")
feature.SetString("description", "Metal Patch")
feature.SetInt("metal", 500)
feature.SetBool("reclaimable", true)
// In real code, check error
_ = doc.WriteFile("/tmp/metal.tdf")
fmt.Println("File written")
}
Output: File written
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
Encoder writes TDF documents straight to an io.Writer. Unlike Marshal, which builds the whole document in memory before returning bytes, a slice target is emitted one top-level [section] at a time, so peak memory is bounded by the largest single section rather than the full document.
func NewEncoder ¶
NewEncoder returns an Encoder that writes to w.
type Field ¶
type Field struct {
// contains filtered or unexported fields
}
Field represents a key-value pair in a section
type ScalarMarshaler ¶
ScalarMarshaler lets a type render itself to a single TDF value (the right side of key=value). Implement it together with ScalarUnmarshaler on a type to have the codec treat it as a scalar field instead of a nested [section].
type ScalarUnmarshaler ¶
ScalarUnmarshaler lets a type parse itself from a single TDF value.
type Section ¶
type Section struct {
// contains filtered or unexported fields
}
Section represents a [SECTION] in a TDF file
func (*Section) Bool ¶
Bool returns a boolean value (false if not found) Accepts: 1/0, true/false, yes/no (case-insensitive)
Example ¶
package main
import (
"fmt"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
content := `[UNITINFO]
{
Builder=1;
canmove=true;
Upright=yes;
NoAutoFire=0;
}`
doc, _ := tdf.ParseString(content)
unit := doc.Section("UNITINFO")
fmt.Println("Builder:", unit.Bool("Builder"))
fmt.Println("Can Move:", unit.Bool("canmove"))
fmt.Println("Upright:", unit.Bool("Upright"))
fmt.Println("No Auto Fire:", unit.Bool("NoAutoFire"))
}
Output: Builder: true Can Move: true Upright: true No Auto Fire: false
func (*Section) List ¶
List returns a value split by spaces (for category lists, etc.)
Example ¶
package main
import (
"fmt"
"github.com/coreprime/kbot/formats/tdf"
)
func main() {
content := `[UNITINFO]
{
Category=ARM KBOT LEVEL2 CONSTR NOWEAPON;
}`
doc, _ := tdf.ParseString(content)
unit := doc.Section("UNITINFO")
for _, category := range unit.List("Category") {
fmt.Println("-", category)
}
}
Output: - ARM - KBOT - LEVEL2 - CONSTR - NOWEAPON