Config and manifest formats¶
CargoForge-C reads two plain-text files before any calculation happens: a ship configuration file that describes the vessel, and a cargo manifest that lists what is being loaded. Understanding these formats lets you feed real data to the program, interpret parse errors, and extend a configuration with hydrostatic tables, tank definitions, or structural limits — capabilities that unlock the full physics engine.
The mental model 🧠¶
Two files, two shapes. The ship config is a recipe card: a handful of key=value lines — length, width, lightship weight — that describe the one vessel. The cargo manifest is a packing list: one whitespace-separated row per item — id, weight, dimensions, type. CargoForge reads the recipe card into a single Ship struct, then turns each packing-list row into its own Cargo entry.
The split is deliberate. key=value is order-independent and self-describing — right for a fixed set of named vessel properties. Columns are compact and repeatable — right for a list that might hold three items or three thousand. Get the format wrong in one line and parse_ship_config / parse_cargo_list reject the file with a line number rather than guessing; the formats are strict precisely so a typo cannot quietly become bad physics.
What this actually means (plain English)¶
No jargon — here's what the ideas in this lesson actually mean, and why they matter.
- Ship config file (
key=value) = "a plain text recipe card that tells CargoForge-C what the vessel is" — five mandatory keys giveparse_ship_configthe dimensions and lightship weight it stores in theShipstruct before any cargo is touched. - Cargo manifest (whitespace-delimited columns) = "a row-per-item list of what you're loading, how heavy it is, how big, and what kind" —
parse_cargo_listturns each line into aCargoentry with weight already converted to kilograms and dimensions split into thedimensions[3]array. - Tonne-to-kilogram conversion at parse time = "the files speak tonnes because ship people always do; the code speaks kilograms; the parser is the translator" — every
weight_tonnesvalue is multiplied by 1000 on entry so thatperform_analysisand every stability formula inside it never has to think about units again. safe_atofwith range checks = "a bounce-back guard on every number field" — it callsstrtof, rejects anything outside the valid range, and returnsNANso the caller (parse_ship_configorparse_cargo_list) can bail cleanly with-1instead of silently storing a zero or garbage value.hydrostatic_tablepath in the config = "a pointer to a measured data file that replaces the built-in box-hull guesses" — when loaded,perform_analysisswitches from the fixed \(C_B = 0.75\) and \(C_W = 0.85\) approximations to linear interpolation of real KB, BM, and MTC values at the actual draft.tank_configand free-surface correction = "a list of partially-filled tanks whose sloshing liquid makes the ship wobblier than its weight alone suggests" —calculate_virtual_kg_risesums each tank's free-surface moment and subtracts the result from GM, givingGM_corrected; skipping this file leavesGMunconservatively high.- Freeing and NULLing on parse error = "when something in the cargo list is wrong, every item already allocated is freed and the pointers are set to NULL immediately" — this is the direct lesson from the use-after-free bug recorded in the journal:
parse_cargo_listsetsship->cargo = NULLafter freeing soship_cleanupcannot reach a dangling pointer.
Why it matters: if either file has a typo or a missing field, the program rejects the entire load cleanly rather than computing stability with corrupted data — but only because safe_atof, the two-phase error cleanup, and the NULL-after-free discipline are all working together. Get any one of those wrong and you get silent bad results or a crash, not an error message.
The ship configuration file¶
Ship config files use a minimal key=value format. Comment lines begin with #; blank lines are
ignored. There is no section syntax and no quoting — every value is either a number or a file
path.
The basic example, examples/sample_ship.cfg, covers the five keys that are always required:
# Ship Specifications
length_m=150
width_m=25
max_weight_tonnes=50000
# Lightship data for stability calculations
lightship_weight_tonnes=2000
lightship_kg_m=8.0
What each key means¶
| Key | Unit stored | Converted to |
|---|---|---|
length_m |
metres | stored as-is (ship->length) |
width_m |
metres | stored as-is (ship->width) |
max_weight_tonnes |
tonnes (input) | kilograms (× 1000, stored in ship->max_weight) |
lightship_weight_tonnes |
tonnes (input) | kilograms (× 1000, stored in ship->lightship_weight) |
lightship_kg_m |
metres | stored as-is (ship->lightship_kg) |
The tonne-to-kilogram conversion matters: every weight calculation inside CargoForge-C works in kilograms, but ship documents universally use tonnes. The parser does the conversion so the rest of the code never has to.
Why lightship_kg_m?
The lightship (the vessel without cargo or ballast) has its own vertical centre of gravity,
called \(KG_{light}\). When cargo is added, the overall KG is the weighted average of the
lightship moment and the cargo moments. Supplying lightship_kg_m lets the program compute
\(KG\) correctly from the very first item loaded — omitting it would make every stability result
wrong.
Numeric validation¶
All numeric keys pass through safe_atof in parser.c, which calls strtof, checks the result
against the range \([0.1,\;10^9]\), and returns NAN on failure. If any mandatory key yields
NAN, parse_ship_config returns -1 and nothing is loaded. This is why a typo such as
length_m=abc produces a clear error rather than a silent zero or a crash.
The cargo manifest¶
The cargo manifest uses whitespace-delimited columns, not key=value. Each data line has four
mandatory fields and one optional field:
From examples/sample_cargo.txt:
# Cargo Manifest
# ID Weight(t) Dimensions(LxWxH) Type
HeavyMachinery 550 20x5x3 standard
SteelBeams 400 18x2x2 bulk
ContainerA 250 12.2x2.4x2.6 reefer
ContainerB 250 12.2x2.4x2.6 reefer
SmallCrate 50 2x2x2 general
Lines starting with # are skipped; blank lines are skipped.
Column-by-column¶
ID (Cargo.id, char[32]): A user-assigned identifier. The parser copies up to 31 bytes —
longer strings are silently truncated. The ID appears in all output formats and in IMDG violation
reports, so make it meaningful and short.
Weight (Cargo.weight, stored as kg): Parsed by safe_atof with range \([0.1,\;10^6]\) in
tonnes, then multiplied by 1000. SteelBeams 400 → 400 000 kg.
Dimensions (Cargo.dimensions[3]): Three positive numbers joined by x with no spaces —
LxWxH in metres. The parser splits on x and validates each component individually. All three
must be present; 20x5 alone is a parse error.
Type (Cargo.type, char[16]): One of standard, bulk, reefer, hazardous,
fragile, or general. The type string governs constraint checks during bin-packing: reefer
cargo receives an advisory to stay on deck; fragile cargo has a tighter stacking-pressure limit;
hazardous cargo triggers the legacy 3 m separation check when no DG field is present.
DG field (optional, Cargo.dg): When a fifth token is present it is handed to
parse_dg_field. The grammar is:
For example:
3.1 means IMDG Class 3, Division 1. UN1203 is the UN number for petrol. A means the cargo
may be stowed anywhere. F-E,S-D is the EmS reference. When this field is present, the full
IMDG segregation engine (not just the legacy 3 m check) governs placement.
Parse errors clean up completely
If safe_atof rejects a weight field or a dimension component is missing, parse_cargo_list
frees every Cargo item it has already allocated, sets ship->cargo = NULL, and sets
ship->cargo_count = 0. This prevents a dangling pointer from reaching ship_cleanup. The
lesson is recorded in the bug journal: freeing a pointer without NULLing it causes
heap-use-after-free on the next access.
Two-pass parsing for files, single-pass for stdin¶
When reading from a file, parse_cargo_list makes two passes: the first counts valid data lines,
the second populates the allocated array. When reading from stdin (filename "-"), only one pass
is possible, so the parser uses a temporary buffer. This distinction matters for memory
efficiency: the two-pass approach allocates exactly the right number of Cargo slots, whereas
the stdin path may over-allocate.
The extended ship config¶
The five basic keys are sufficient for box-hull calculations. To activate table-based
hydrostatics, free-surface correction, and longitudinal strength checking, add the extended keys
shown in examples/sample_ship_full.cfg:
# Full ship configuration with hydrostatic table, tanks, and strength limits
# MV Example - 150m x 25m general cargo vessel
# Ship dimensions
length_m=150
width_m=25
max_weight_tonnes=50000
# Lightship data
lightship_weight_tonnes=2000
lightship_kg_m=8.0
# Hydrostatic table (enables table-based calculations)
hydrostatic_table=examples/sample_hydro.csv
# Tank configuration (enables free surface correction)
tank_config=examples/sample_tanks.csv
# Permissible longitudinal strength limits (from class society)
permissible_sf_tonnes=5000
permissible_bm_hog_t_m=120000
permissible_bm_sag_t_m=100000
hydrostatic_table — replacing the box-hull approximation¶
This key takes a file path. When present, parse_ship_config calls parse_hydro_table on that
path. A successful load sets ship->hydro->loaded = 1, which tells perform_analysis to use
table-based interpolation instead of the box-hull formulas.
The CSV format is comma-separated with # comments allowed. Each row represents one waterline:
draft_m, displacement_t, km_m, kb_m, bm_m, tpc_t_per_cm, mtc_tm_per_cm [, waterplane_area_m2 [, lcb_m]]
Rows must be in strictly ascending draft order; at least two rows are required. The seven-
column form is the minimum; columns 8 and 9 (waterplane_area_m2, lcb_m) default to zero if
absent. At runtime, hydro_draft_from_displacement inverse-interpolates draft from the computed
displacement, and hydro_interpolate then reads KB, BM, MTC, and KM at that draft — all by
linear interpolation between the two nearest rows.
Without this file, CargoForge-C falls back to:
The table replaces all three of these constants with ship-specific measured values — essential for any result you would stake cargo on.
tank_config — enabling free-surface correction¶
This key points to a tank CSV. When loaded, it activates calculate_virtual_kg_rise, which adds
a virtual rise in KG for each partially-filled tank:
The tank CSV format is:
All nine fields are required. fill_fraction is clamped to \([0, 1]\); tanks that are completely
empty or completely full contribute zero FSM (they have no free surface). A typical seawater
ballast tank uses density=1.025; fuel oil is typically 0.85.
Without this file, ship->tanks is NULL and perform_analysis skips the free-surface
correction — GM_corrected equals GM, which is unconservative when ballast tanks are slack.
Longitudinal strength keys¶
Three optional keys tell the program the limits issued by the classification society:
| Key | Stored in | Meaning |
|---|---|---|
permissible_sf_tonnes |
StrengthLimits.permissible_sf |
Maximum still-water shear force (t) |
permissible_bm_hog_t_m |
StrengthLimits.permissible_bm_hog |
Maximum hogging bending moment (t·m) |
permissible_bm_sag_t_m |
StrengthLimits.permissible_bm_sag |
Maximum sagging bending moment (t·m) |
All three must be present for strength checking to activate. If any one is absent, ship->strength_limits remains NULL and AnalysisResult.strength_compliant is set to -1 (meaning "not checked").
calculate_longitudinal_strength distributes weight and buoyancy across 20 hull stations, integrates to shear force and then to bending moment, and compares the peaks against these limits via check_strength_limits.
How the parser resolves file paths¶
parse_ship_config stores the string value of hydrostatic_table or tank_config into a path
buffer, then calls the corresponding parser on that path. Paths are resolved relative to the
working directory from which CargoForge-C is invoked — not relative to the config file itself.
This is why the examples use paths like examples/sample_hydro.csv when the program is expected
to be run from the repository root.
Reading from stdin
Both parse_ship_config and parse_cargo_list accept "-" as the filename, which opens
stdin instead of a file. This enables pipeline use:
Putting it together — what the parser feeds the physics engine¶
After both files parse successfully, the Ship struct holds:
ship->length,ship->width,ship->max_weight,ship->lightship_weight,ship->lightship_kgship->cargo[0..cargo_count-1]— each with weight (kg), dimensions (m), type string, position set to-1.0(unplaced), and optionally a heap-allocatedDGInfoship->hydro—NULL(box-hull) or a loadedHydroTableship->tanks—NULL(no correction) or a loadedTankConfigship->strength_limits—NULL(not checked) or loaded limits
perform_analysis reads exactly these fields. No other source of truth exists inside the library.
Recap¶
- Ship config is
key=value; five keys are mandatory, three optional groups (hydrostatics, tanks, strength limits) unlock increasingly precise physics. - All weight keys are in tonnes on disk; the parser converts them to kilograms before storing.
- Cargo manifests are whitespace-delimited with four mandatory columns and an optional DG field
using the grammar
DG:<class>[.div]:<UN>:<stowage>:<EmS>. - The
hydrostatic_tablepath replaces three box-hull constants with measured, interpolated values;tank_configenables free-surface correction; the strength keys enable SWSF/SWBM checking. - Parse errors clean up completely: freed arrays are NULLed immediately to prevent
heap-use-after-free in
ship_cleanup.
Check yourself¶
Why does CargoForge-C use two different text formats — key=value for the ship config and whitespace columns for the manifest — instead of one format for both?
The ship config is a small, fixed set of named properties, so an order-independent, self-describing key=value format fits well. The manifest is a list that can be any length, so compact whitespace-delimited rows scale better to hundreds of items.
What happens if a manifest line has a typo in its weight field?
parse_cargo_list rejects the entire file and reports a line number, rather than silently guessing a value. The formats are strict precisely so a typo can't quietly turn into wrong physics.
Next: Tokenizing and parsing in C.