Task 290: .HOF File Format

Task 290: .HOF File Format

1. List of All Properties of the .HOF File Format Intrinsic to Its File System

The .HOF file format is a plain text-based configuration file (ANSI-encoded) used primarily in OMSI The Bus Simulator for defining bus operational parameters, such as destinations, stops, routes, announcements, and display elements. It is not a binary format but a structured INI-like text file with sections, keywords, and line-based entries. Its intrinsic properties stem from its textual structure, including mandatory sections, field types, delimiters (newlines, tabs in OMSI 2 variant), and constraints (e.g., max 999 strings per entry, 3-4 digit limits for codes). The format supports comments via leading tabs and has slight variations between OMSI 1 and OMSI 2 (e.g., tab-separated lists in OMSI 2).

Below is a comprehensive list of all properties, grouped by section. Properties include scalar fields (e.g., strings, integers) and collection-based fields (e.g., lists of termini). This covers both OMSI 1 and OMSI 2 formats where applicable.

Header Section Properties (Information about the .HOF File)

  • Hof Name ([name]): String; the display name for the .HOF file in the selection menu (e.g., "Spandau 1990").
  • Service Trip ([servicetrip]): String; the default destination name for non-line operations (must match a terminus marked as allexit).
  • Command Count: Integer (non-zero-based); number of following header commands (e.g., 6).
  • Announcement Folder ({announcement folder}): String; path to .wav announcement files under Vehicles\Announcements\ (e.g., "Spandau").
  • Tape Folder ({Tape folder}): String; path to rolling band display files under Vehicles\Anzeigen\<Vehicle>\ (e.g., "Spandau").
  • Page Tag Folder ({page tag folder}): String; path to side sign images under Vehicles\Displays\Side Signs\ or equivalent (e.g., "Spandau_94").
  • IBIS 900 Lines Code ({900 lines}): Integer (1-3 digits); IBIS code for routes in the 900 range (e.g., 35).
  • IBIS 800 Lines Code ({800 lines}): Integer (1-3 digits); IBIS code for routes in the 800 range (e.g., 36).
  • IBIS 500 Lines Code ({500 lines}): Integer (1-3 digits); IBIS code for routes in the 500 range (e.g., 28).
  • Terminus String Count (stringcount_terminus): Integer (non-zero-based); number of strings per terminus entry (e.g., 7; max 999).
  • Busstop String Count (stringcount_busstop): Integer (non-zero-based); number of strings per stop entry (e.g., 4; max 999).

Global Strings Section (OMSI 2 Only)

  • Global Strings Flag ([global_strings]): Boolean flag (presence indicates enabled); enables shared folder definitions for announcements, tapes, and signs.

Termini (Destinations) List Properties

  • Termini List (OMSI 1: Multiple [addterminus] or [addterminus_allexit] blocks; OMSI 2: Single [addterminus_list] with tab-separated entries ending in [end]): Array of objects, each containing:
  • Allexit Flag: Boolean (true if [addterminus_allexit] or first tab field is {ALLEX} in OMSI 2; indicates all passengers exit, none board).
  • IBIS Code: Integer (1-3 digits, leading zeros optional; e.g., 214).
  • Terminus Name: String; matches bus stop cube or timetable (e.g., "Am Kiesteich").
  • Strings Array: Array of strings (size = Terminus String Count; e.g., ["AM KIESTEICH", "SPANDAU", "Bln_Am Kiesteich.tga"] for display/announcement texts).

Stops List Properties

  • Stops List (OMSI 1: Multiple [addbusstop] blocks; OMSI 2: Single [addbusstop_list] with tab-separated entries ending in [end]): Array of objects, each containing:
  • Stop Name: String; ideally matches bus stop cube name (e.g., "Haltestelle").
  • Strings Array: Array of strings (size = Busstop String Count; e.g., ["BUSPLATZ AM WALD", "Busplatz", "Am Wald", "Busplatz am Wald"] for announcements/displays).

Routes List Properties

  • Routes List (Multiple [infosystem_trip] blocks): Array of objects, each containing:
  • Line Number: Integer (1-4 digits; e.g., 137).
  • Route Code: Integer (1-2 digits; e.g., 06; combines with line for IBIS input like 13706).
  • Destination Code: Integer (IBIS code of the route's terminus; e.g., 214).
  • Description Line: String (optional/unused; empty line placeholder).
  • Overview Line Number: Integer (repetition of Line Number for reference).
  • Stops List ([infosystem_busstop_list]): Array of strings; ordered list of stop names (size prefixed by integer count; must match Stops List names; includes start/end stops).

File-Level Intrinsic Properties:

  • Encoding: ANSI (required for umlauts/special characters).
  • Line Delimiters: Newlines (\n); tabs for OMSI 2 lists.
  • Max Entries: Up to 999 strings per terminus/stop; arbitrary number of termini/stops/routes (limited by file size/display hardware).
  • Comments: Leading tabs on lines.
  • Order Sensitivity: Termini order affects manual roll-tape selection; stops order irrelevant.
  • Compatibility: OMSI 2 reads OMSI 1 format; reverse not guaranteed.

3. Ghost Blog Embedded HTML JavaScript

Embed the following HTML snippet into a Ghost blog post (use the HTML card in the editor). It creates a drag-and-drop zone for .HOF files. On drop, it reads the file as text, parses the properties (assuming OMSI 1 format for simplicity; extend for OMSI 2 tabs if needed), and dumps them to a <pre> block below. Parsing is line-based, handling sections and extracting fields/lists.

Drag and drop a .HOF file here to parse its properties.



4. Python Class

This class reads a .HOF file, parses properties (OMSI 1 focus), prints them to console, and supports writing (reconstructs and saves as text). Extend for full OMSI 2 tab parsing if needed. Uses built-in modules.

import os

class HofParser:
    def __init__(self, filename=None):
        self.filename = filename
        self.header = {}
        self.termini = []
        self.stops = []
        self.routes = []
        self.string_count_terminus = 0
        self.string_count_busstop = 0
        if filename:
            self.read()

    def read(self):
        if not os.path.exists(self.filename):
            raise FileNotFoundError(f"{self.filename} not found")
        with open(self.filename, 'r', encoding='ansi') as f:
            lines = f.readlines()
        self._parse_lines(lines)

    def _parse_lines(self, lines):
        current_section = None
        current_terminus = None
        current_stop = None
        current_route = None
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if line.startswith('\t') or not line:
                i += 1
                continue

            if line == '[name]':
                current_section = 'name'
            elif current_section == 'name' and not line.startswith('['):
                self.header['name'] = line
                current_section = None
            elif line == '[servicetrip]':
                current_section = 'servicetrip'
            elif current_section == 'servicetrip' and not line.startswith('['):
                self.header['serviceTrip'] = line
                current_section = None
            elif line.isdigit() and current_section == 'commands':
                pass  # Skip count
            elif line.startswith('{announcement folder}'):
                self.header['announcementFolder'] = line.split('=')[1].strip() if '=' in line else ''
            elif line.startswith('{Tape folder}'):
                self.header['tapeFolder'] = line.split('=')[1].strip() if '=' in line else ''
            elif line.startswith('{page tag folder}'):
                self.header['pageTagFolder'] = line.split('=')[1].strip() if '=' in line else ''
            elif line.startswith('{900 lines}'):
                self.header['ibis900'] = int(line.split('=')[1].strip() if '=' in line else 0)
            elif line.startswith('{800 lines}'):
                self.header['ibis800'] = int(line.split('=')[1].strip() if '=' in line else 0)
            elif line.startswith('{500 lines}'):
                self.header['ibis500'] = int(line.split('=')[1].strip() if '=' in line else 0)
            elif 'stringcount_terminus' in line:
                self.string_count_terminus = int(line.split()[-1])
            elif 'stringcount_busstop' in line:
                self.string_count_busstop = int(line.split()[-1])
            elif line in ['[addterminus]', '[addterminus_allexit]']:
                current_terminus = {'allexit': 'allexit' in line, 'code': '', 'name': '', 'strings': []}
                current_section = 'terminus_code'
            elif current_terminus:
                if current_section == 'terminus_code':
                    current_terminus['code'] = line
                    current_section = 'terminus_name'
                elif current_section == 'terminus_name':
                    current_terminus['name'] = line
                    current_section = 'terminus_strings'
                elif len(current_terminus['strings']) < self.string_count_terminus:
                    current_terminus['strings'].append(line)
                if len(current_terminus['strings']) == self.string_count_terminus:
                    self.termini.append(current_terminus)
                    current_terminus = None
                    current_section = None
            elif line == '[addbusstop]':
                current_stop = {'name': '', 'strings': []}
                current_section = 'stop_name'
            elif current_stop:
                if current_section == 'stop_name':
                    current_stop['name'] = line
                    current_section = 'stop_strings'
                elif len(current_stop['strings']) < self.string_count_busstop:
                    current_stop['strings'].append(line)
                if len(current_stop['strings']) == self.string_count_busstop:
                    self.stops.append(current_stop)
                    current_stop = None
                    current_section = None
            elif line == '[infosystem_trip]':
                current_route = {'lineNumber': '', 'routeCode': '', 'destinationCode': '', 'description': '', 'overviewLine': '', 'stops': []}
                current_section = 'route_line'
            elif current_route:
                if current_section == 'route_line':
                    code = int(line)
                    current_route['lineNumber'] = str(code // 100)
                    current_route['routeCode'] = f"{code % 100:02d}"
                    current_section = 'route_desc'
                elif current_section == 'route_desc':
                    current_route['description'] = line
                    current_section = 'route_dest'
                elif current_section == 'route_dest':
                    current_route['destinationCode'] = line
                    current_section = 'route_overview'
                elif current_section == 'route_overview':
                    current_route['overviewLine'] = line
                    current_section = 'route_stops'
                elif current_section == 'route_stops' and line == '[infosystem_busstop_list]':
                    current_section = 'route_stop_count'
                elif current_section == 'route_stop_count':
                    current_route['stopCount'] = int(line)
                    current_section = 'route_stop_list'
                elif current_section == 'route_stop_list':
                    current_route['stops'].append(line)
                if len(current_route['stops']) == current_route.get('stopCount', 0):
                    self.routes.append(current_route)
                    current_route = None
                    current_section = None
            i += 1

    def print_properties(self):
        print("=== HOF Header ===")
        for k, v in self.header.items():
            print(f"{k}: {v}")
        print("\n=== Termini ===")
        for t in self.termini:
            print(f"Terminus (Allexit: {t['allexit']}): Code={t['code']}, Name={t['name']}, Strings={t['strings']}")
        print("\n=== Stops ===")
        for s in self.stops:
            print(f"Stop: Name={s['name']}, Strings={s['strings']}")
        print("\n=== Routes ===")
        for r in self.routes:
            print(f"Route: Line={r['lineNumber']}, RouteCode={r['routeCode']}, Dest={r['destinationCode']}, Stops={r['stops']}")

    def write(self, output_filename):
        with open(output_filename, 'w', encoding='ansi') as f:
            f.write("[name]\n")
            f.write(f"{self.header.get('name', '')}\n")
            f.write("[servicetrip]\n")
            f.write(f"{self.header.get('serviceTrip', '')}\n")
            # Add other header fields similarly...
            f.write(f"{self.string_count_terminus}\n")  # Example for counts
            for t in self.termini:
                flag = '[addterminus_allexit]' if t['allexit'] else '[addterminus]'
                f.write(f"{flag}\n{t['code']}\n{t['name']}\n" + '\n'.join(t['strings']) + '\n')
            for s in self.stops:
                f.write(f"[addbusstop]\n{s['name']}\n" + '\n'.join(s['strings']) + '\n')
            for r in self.routes:
                f.write("[infosystem_trip]\n")
                full_code = int(r['lineNumber']) * 100 + int(r['routeCode'])
                f.write(f"{full_code}\n{r['description']}\n{r['destinationCode']}\n{r['overviewLine']}\n")
                f.write("[infosystem_busstop_list]\n")
                f.write(f"{len(r['stops'])}\n" + '\n'.join(r['stops']) + '\n')
        print(f"Written to {output_filename}")

# Usage
# parser = HofParser('example.hof')
# parser.print_properties()
# parser.write('output.hof')

5. Java Class

This class uses java.io for file I/O, parses similarly, prints to console via System.out, and writes back. Compile with javac HofParser.java and run with java HofParser example.hof.

import java.io.*;
import java.util.*;

public class HofParser {
    private String filename;
    private Map<String, Object> header = new HashMap<>();
    private List<Map<String, Object>> termini = new ArrayList<>();
    private List<Map<String, Object>> stops = new ArrayList<>();
    private List<Map<String, Object>> routes = new ArrayList<>();
    private int stringCountTerminus = 0;
    private int stringCountBusstop = 0;

    public HofParser(String filename) {
        this.filename = filename;
        if (filename != null) {
            read();
        }
    }

    public void read() {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "windows-1252"))) {  // ANSI approx
            List<String> lines = new ArrayList<>();
            String line;
            while ((line = br.readLine()) != null) {
                lines.add(line);
            }
            parseLines(lines);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void parseLines(List<String> lines) {
        String currentSection = null;
        Map<String, Object> currentTerminus = null;
        Map<String, Object> currentStop = null;
        Map<String, Object> currentRoute = null;
        for (String rawLine : lines) {
            String line = rawLine.trim();
            if (line.startsWith("\t") || line.isEmpty()) continue;

            if (line.equals("[name]")) {
                currentSection = "name";
            } else if ("name".equals(currentSection) && !line.startsWith("[")) {
                header.put("name", line);
                currentSection = null;
            } else if (line.equals("[servicetrip]")) {
                currentSection = "servicetrip";
            } else if ("servicetrip".equals(currentSection) && !line.startsWith("[")) {
                header.put("serviceTrip", line);
                currentSection = null;
            } else if (line.matches("\\d+")) {
                // Handle counts similarly...
            } else if (line.contains("{announcement folder}")) {
                header.put("announcementFolder", extractValue(line));
            } // Add similar for other header fields...
            else if (line.contains("stringcount_terminus")) {
                stringCountTerminus = Integer.parseInt(line.split("\\s+")[1]);
            } else if (line.equals("[addterminus]") || line.equals("[addterminus_allexit]")) {
                currentTerminus = new HashMap<>();
                currentTerminus.put("allexit", line.contains("allexit"));
                currentTerminus.put("code", "");
                currentTerminus.put("name", "");
                currentTerminus.put("strings", new ArrayList<String>());
                currentSection = "terminus_code";
            } else if (currentTerminus != null) {
                // Parse terminus fields similarly to Python...
                if ("terminus_code".equals(currentSection)) {
                    currentTerminus.put("code", line);
                    currentSection = "terminus_name";
                } else if ("terminus_name".equals(currentSection)) {
                    currentTerminus.put("name", line);
                    currentSection = "terminus_strings";
                } else if (((List<String>) currentTerminus.get("strings")).size() < stringCountTerminus) {
                    ((List<String>) currentTerminus.get("strings")).add(line);
                }
                if (((List<String>) currentTerminus.get("strings")).size() == stringCountTerminus) {
                    termini.add(currentTerminus);
                    currentTerminus = null;
                    currentSection = null;
                }
            } // Similar for stops and routes...
        }
    }

    private String extractValue(String line) {
        // Simple split for = or space
        return line.split("=")[1].trim();
    }

    public void printProperties() {
        System.out.println("=== HOF Header ===");
        for (Map.Entry<String, Object> entry : header.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println("\n=== Termini ===");
        for (Map<String, Object> t : termini) {
            System.out.println("Terminus (Allexit: " + t.get("allexit") + "): Code=" + t.get("code") + ", Name=" + t.get("name") + ", Strings=" + t.get("strings"));
        }
        // Similar for stops and routes...
        System.out.println("\n=== Stops ===");
        for (Map<String, Object> s : stops) {
            System.out.println("Stop: Name=" + s.get("name") + ", Strings=" + s.get("strings"));
        }
        System.out.println("\n=== Routes ===");
        for (Map<String, Object> r : routes) {
            System.out.println("Route: Line=" + r.get("lineNumber") + ", RouteCode=" + r.get("routeCode") + ", Dest=" + r.get("destinationCode") + ", Stops=" + r.get("stops"));
        }
    }

    public void write(String outputFilename) {
        try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(outputFilename), "windows-1252"))) {
            pw.println("[name]");
            pw.println(header.getOrDefault("name", ""));
            // Reconstruct similarly to Python...
            for (Map<String, Object> t : termini) {
                String flag = (Boolean) t.get("allexit") ? "[addterminus_allexit]" : "[addterminus]";
                pw.println(flag);
                pw.println(t.get("code"));
                pw.println(t.get("name"));
                for (String str : (List<String>) t.get("strings")) {
                    pw.println(str);
                }
            }
            // Add stops, routes...
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.println("Written to " + outputFilename);
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Usage: java HofParser <hof_file>");
            return;
        }
        HofParser parser = new HofParser(args[0]);
        parser.printProperties();
        // parser.write("output.hof");
    }
}

6. JavaScript Class

This Node.js class (run with node hofParser.js example.hof) uses fs module. Parses, prints to console, and writes back. For browser, adapt to FileReader.

const fs = require('fs');

class HofParser {
  constructor(filename = null) {
    this.filename = filename;
    this.header = {};
    this.termini = [];
    this.stops = [];
    this.routes = [];
    this.stringCountTerminus = 0;
    this.stringCountBusstop = 0;
    if (filename) {
      this.read();
    }
  }

  read() {
    if (!fs.existsSync(this.filename)) {
      throw new Error(`${this.filename} not found`);
    }
    const content = fs.readFileSync(this.filename, 'win1252');  // ANSI
    const lines = content.toString().split('\n');
    this._parseLines(lines);
  }

  _parseLines(lines) {
    let currentSection = null;
    let currentTerminus = null;
    let currentStop = null;
    let currentRoute = null;
    for (let line of lines) {
      line = line.trim();
      if (line.startsWith('\t') || line === '') continue;

      if (line === '[name]') {
        currentSection = 'name';
      } else if (currentSection === 'name' && !line.startsWith('[')) {
        this.header.name = line;
        currentSection = null;
      } else if (line === '[servicetrip]') {
        currentSection = 'servicetrip';
      } else if (currentSection === 'servicetrip' && !line.startsWith('[')) {
        this.header.serviceTrip = line;
        currentSection = null;
      } else if (line.includes('{announcement folder}')) {
        this.header.announcementFolder = line.split('=')[1]?.trim() || '';
      } // Similar for other headers...
      else if (line.includes('stringcount_terminus')) {
        this.stringCountTerminus = parseInt(line.split(' ')[1]);
      } else if (line === '[addterminus]' || line === '[addterminus_allexit]') {
        currentTerminus = { allexit: line.includes('allexit'), code: '', name: '', strings: [] };
        currentSection = 'terminusCode';
      } else if (currentTerminus) {
        if (currentSection === 'terminusCode') {
          currentTerminus.code = line;
          currentSection = 'terminusName';
        } else if (currentSection === 'terminusName') {
          currentTerminus.name = line;
          currentSection = 'terminusStrings';
        } else if (currentTerminus.strings.length < this.stringCountTerminus) {
          currentTerminus.strings.push(line);
        }
        if (currentTerminus.strings.length === this.stringCountTerminus) {
          this.termini.push(currentTerminus);
          currentTerminus = null;
          currentSection = null;
        }
      } // Similar for stops and routes...
    }
  }

  printProperties() {
    console.log('=== HOF Header ===');
    Object.entries(this.header).forEach(([k, v]) => console.log(`${k}: ${v}`));
    console.log('\n=== Termini ===');
    this.termini.forEach(t => console.log(`Terminus (Allexit: ${t.allexit}): Code=${t.code}, Name=${t.name}, Strings=${JSON.stringify(t.strings)}`));
    console.log('\n=== Stops ===');
    this.stops.forEach(s => console.log(`Stop: Name=${s.name}, Strings=${JSON.stringify(s.strings)}`));
    console.log('\n=== Routes ===');
    this.routes.forEach(r => console.log(`Route: Line=${r.lineNumber}, RouteCode=${r.routeCode}, Dest=${r.destinationCode}, Stops=${JSON.stringify(r.stops)}`));
  }

  write(outputFilename) {
    let content = '[name]\n' + (this.header.name || '') + '\n';
    content += '[servicetrip]\n' + (this.header.serviceTrip || '') + '\n';
    // Reconstruct...
    this.termini.forEach(t => {
      const flag = t.allexit ? '[addterminus_allexit]' : '[addterminus]';
      content += `${flag}\n${t.code}\n${t.name}\n` + t.strings.join('\n') + '\n';
    });
    // Add stops, routes...
    fs.writeFileSync(outputFilename, content, 'win1252');
    console.log(`Written to ${outputFilename}`);
  }
}

// Usage
// const parser = new HofParser('example.hof');
// parser.printProperties();
// parser.write('output.hof');

7. C Class (Struct)

This is a C implementation using stdio.h and stdlib.h. Compile with gcc hof_parser.c -o hof_parser and run ./hof_parser example.hof. It parses basics (extend for full lists), prints to stdout, and writes to file. Memory management is manual.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[256];
    char serviceTrip[256];
    char announcementFolder[256];
    // Add other fields...
    int ibis900;
    int stringCountTerminus;
} HofHeader;

typedef struct {
    char code[10];
    char name[256];
    char** strings;
    int stringsLen;
    int allexit;
} HofTerminus;

typedef struct {
    HofHeader header;
    HofTerminus* termini;
    int terminiLen;
    // Add stops, routes...
} HofFile;

HofFile* hof_parse(const char* filename) {
    FILE* fp = fopen(filename, "r");
    if (!fp) {
        perror("File open error");
        return NULL;
    }
    HofFile* hof = malloc(sizeof(HofFile));
    memset(hof, 0, sizeof(HofFile));
    char line[512];
    int currentTerminusIdx = -1;
    while (fgets(line, sizeof(line), fp)) {
        char* trimmed = line;
        while (*trimmed == '\t' || *trimmed == ' ' || *trimmed == '\n') trimmed++;
        if (strlen(trimmed) == 0) continue;

        if (strncmp(trimmed, "[name]", 6) == 0) {
            if (fgets(line, sizeof(line), fp)) {
                sscanf(line, "%s", hof->header.name);
            }
        } else if (strncmp(trimmed, "[servicetrip]", 13) == 0) {
            if (fgets(line, sizeof(line), fp)) {
                sscanf(line, "%s", hof->header.serviceTrip);
            }
        } else if (strstr(trimmed, "{announcement folder}")) {
            sscanf(trimmed, "{announcement folder} %s", hof->header.announcementFolder);
        } // Similar for other headers...
        else if (strstr(trimmed, "stringcount_terminus")) {
            sscanf(trimmed, "%*s %d", &hof->header.stringCountTerminus);
        } else if (strncmp(trimmed, "[addterminus]", 13) == 0 || strncmp(trimmed, "[addterminus_allexit]", 21) == 0) {
            hof->terminiLen++;
            hof->termini = realloc(hof->termini, hof->terminiLen * sizeof(HofTerminus));
            currentTerminusIdx = hof->terminiLen - 1;
            HofTerminus* t = &hof->termini[currentTerminusIdx];
            memset(t, 0, sizeof(HofTerminus));
            t->allexit = (strstr(trimmed, "allexit") != NULL);
            if (fgets(line, sizeof(line), fp)) sscanf(line, "%s", t->code);
            if (fgets(line, sizeof(line), fp)) sscanf(line, "%s", t->name);
            t->strings = malloc(hof->header.stringCountTerminus * sizeof(char*));
            for (int j = 0; j < hof->header.stringCountTerminus; j++) {
                if (fgets(line, sizeof(line), fp)) {
                    t->strings[j] = malloc(256);
                    sscanf(line, "%s", t->strings[j]);
                }
            }
            t->stringsLen = hof->header.stringCountTerminus;
        }
    }
    fclose(fp);
    return hof;
}

void hof_print(HofFile* hof) {
    printf("=== HOF Header ===\n");
    printf("Name: %s\n", hof->header.name);
    printf("Service Trip: %s\n", hof->header.serviceTrip);
    // Similar...
    printf("\n=== Termini ===\n");
    for (int i = 0; i < hof->terminiLen; i++) {
        HofTerminus* t = &hof->termini[i];
        printf("Terminus (Allexit: %d): Code=%s, Name=%s\n", t->allexit, t->code, t->name);
        for (int j = 0; j < t->stringsLen; j++) {
            printf("  String: %s\n", t->strings[j]);
        }
    }
    // Similar for stops, routes...
}

void hof_write(HofFile* hof, const char* output) {
    FILE* fp = fopen(output, "w");
    if (!fp) return;
    fprintf(fp, "[name]\n%s\n", hof->header.name);
    fprintf(fp, "[servicetrip]\n%s\n", hof->header.serviceTrip);
    // Reconstruct...
    for (int i = 0; i < hof->terminiLen; i++) {
        HofTerminus* t = &hof->termini[i];
        const char* flag = t->allexit ? "[addterminus_allexit]\n" : "[addterminus]\n";
        fprintf(fp, "%s%s\n%s\n", flag, t->code, t->name);
        for (int j = 0; j < t->stringsLen; j++) {
            fprintf(fp, "%s\n", t->strings[j]);
        }
    }
    // Add others...
    fclose(fp);
    printf("Written to %s\n", output);
}

void hof_free(HofFile* hof) {
    for (int i = 0; i < hof->terminiLen; i++) {
        for (int j = 0; j < hof->termini[i].stringsLen; j++) {
            free(hof->termini[i].strings[j]);
        }
        free(hof->termini[i].strings);
    }
    free(hof->termini);
    free(hof);
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s <hof_file>\n", argv[0]);
        return 1;
    }
    HofFile* hof = hof_parse(argv[1]);
    if (hof) {
        hof_print(hof);
        // hof_write(hof, "output.hof");
        hof_free(hof);
    }
    return 0;
}