tdf

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: MIT Imports: 9 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Canonicalize

func Canonicalize(data []byte) ([]byte, error)

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

func Marshal(v any) ([]byte, error)

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

func MisparsedKeys(v any) []string

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

func SemanticEqual(a, b []byte) (bool, string)

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

func Unmarshal(data []byte, v any) error

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

func NewDecoder(r io.Reader) *Decoder

NewDecoder returns a Decoder that reads from r.

func (*Decoder) Decode

func (d *Decoder) Decode(v any) error

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

func Parse(r io.Reader) (*Document, error)

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 ParseFile

func ParseFile(path string) (*Document, error)

ParseFile parses a TDF file

func ParseString

func ParseString(content string) (*Document, error)

ParseString parses TDF content from a string

func (*Document) AddSection

func (d *Document) AddSection(name string) *Section

AddSection adds a new section or returns existing one

func (*Document) HasSection

func (d *Document) HasSection(name string) bool

HasSection checks if a section exists

func (*Document) Section

func (d *Document) Section(name string) *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) Sections

func (d *Document) Sections() []*Section

Sections returns all sections

func (*Document) String

func (d *Document) String() string

String returns the TDF content as a string

func (*Document) Write

func (d *Document) Write(w io.Writer) error

Write writes the document to a writer

func (*Document) WriteFile

func (d *Document) WriteFile(path string) error

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

func NewEncoder(w io.Writer) *Encoder

NewEncoder returns an Encoder that writes to w.

func (*Encoder) Encode

func (e *Encoder) Encode(v any) error

Encode renders v as TDF text to the underlying writer and flushes. v must be a struct, a slice of structs, or a pointer to either, exactly as for Marshal.

type Field

type Field struct {
	// contains filtered or unexported fields
}

Field represents a key-value pair in a section

func (*Field) Key

func (f *Field) Key() string

Key returns the field key

func (*Field) Value

func (f *Field) Value() string

Value returns the field value

type ScalarMarshaler

type ScalarMarshaler interface {
	MarshalTDF() (string, error)
}

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

type ScalarUnmarshaler interface {
	UnmarshalTDF(string) error
}

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

func (s *Section) Bool(key string) 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) Fields

func (s *Section) Fields() []*Field

Fields returns all fields in order

func (*Section) Float

func (s *Section) Float(key string) float64

Float returns a float value (0.0 if not found or invalid)

func (*Section) Get

func (s *Section) Get(key string) (string, bool)

Get returns a field value by key (case-insensitive)

func (*Section) Has

func (s *Section) Has(key string) bool

Has checks if a field exists

func (*Section) Int

func (s *Section) Int(key string) int

Int returns an integer value (0 if not found or invalid)

func (*Section) List

func (s *Section) List(key string) []string

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

func (*Section) Name

func (s *Section) Name() string

Name returns the section name

func (*Section) Sections

func (s *Section) Sections() []*Section

Sections returns nested subsections

func (*Section) Set

func (s *Section) Set(key, value string)

Set sets a field value

func (*Section) SetBool

func (s *Section) SetBool(key string, value bool)

SetBool sets a boolean value (as 1 or 0)

func (*Section) SetFloat

func (s *Section) SetFloat(key string, value float64)

SetFloat sets a float value

func (*Section) SetInt

func (s *Section) SetInt(key string, value int)

SetInt sets an integer value

func (*Section) SetList

func (s *Section) SetList(key string, values []string)

SetList sets a space-separated list value

func (*Section) SetString

func (s *Section) SetString(key, value string)

SetString sets a string value

func (*Section) String

func (s *Section) String(key string) string

String returns a string value (empty string if not found)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL