aboutsummaryrefslogtreecommitdiffstats
path: root/research/forts-data-reference.org
diff options
context:
space:
mode:
Diffstat (limited to 'research/forts-data-reference.org')
-rw-r--r--research/forts-data-reference.org193
1 files changed, 193 insertions, 0 deletions
diff --git a/research/forts-data-reference.org b/research/forts-data-reference.org
new file mode 100644
index 0000000..5d4ecec
--- /dev/null
+++ b/research/forts-data-reference.org
@@ -0,0 +1,193 @@
+#+TITLE: Forts Data & Modding Reference (extracted from the shipped game)
+#+AUTHOR: Forts Clone Project
+#+DATE: 2026-06-30
+#+OPTIONS: toc:3 num:t
+
+* Source
+
+Extracted directly from a local Forts install (unpacked, NOT decompiled):
+- Base data: =SteamLibrary/steamapps/common/Forts/data/= (~2650 .lua files)
+- Workshop mods: =SteamLibrary/steamapps/workshop/content/410900/= (~80 mods)
+
+The game ships its game-logic data as plain Lua. This file records the bits
+that matter for our clone: the canonical physics/material/fire numbers, and the
+modding architecture. Numbers here are the GROUND TRUTH that validate (and in a
+few places correct) our research notes.
+
+* Canonical physics constants (=data/db/constants.lua=, table =Physics=/=Structure=)
+
+| Constant | Value | Meaning / our equivalent |
+|-----------------------------------+-----------+--------------------------------------------------|
+| Gravity | 981 | cm/s^2 (game works in cm; 1 grid = 37.5..150 cm) |
+| Oversamples | 14 | constraint solver iterations (we use 8) |
+| SpringDamping | 800 | global spring damping |
+| MinStiffness / MaxStiffness | 1e4 / 1e6 | per-material Stiffness clamps |
+| MinimumMass | 15 | |
+| Limits.AngleStressPrimaryThreshold| 30 | strut snaps if rotated >30 deg from built angle |
+| Limits.AngleStressSecondaryThreshold | 15 | secondary (softer) angle threshold |
+| Limits.MinimumStrutDivergenceAngle| 15 | min angle between two struts at a node |
+| StressWarning.CompressionThreshold| 0.2 | when the strut turns red (warning, not break) |
+| StressWarning.ExpansionThreshold | 0.2 | when the strut turns blue (warning, not break) |
+| TempBracing.Duration | 16 | temp rigid hold on fresh struts (our SETTLE_TIME)|
+| TempBracing.Scale | 1.1 | |
+| Break.Effect | structure_break.lua | strut snap FX |
+
+** IMPORTANT correction to our research
+Our research notes claimed "stress is axial only, there is no angular stress
+term." The shipped data DISPROVES that: =AngleStressPrimaryThreshold = 30= and
+=AngleStressSecondaryThreshold = 15= are real break conditions. So Forts has
+BOTH an axial deformation break (MaxCompression/MaxExpansion, see below) AND an
+angle-from-built-angle break (30/15 deg). We currently use a force-based axial
+break only — adding an angle-stress break is a faithful future option.
+
+** Stress colours (=Structure.Colours=) — matches the in-game strut colouring
+Compressed = red, Expanded = blue, AtRest = white, Braced = yellow.
+(Our build-grace radial is yellow = "Braced"; our snap is force-based.)
+
+* Material schema (=data/materials/building_materials.lua=)
+
+Materials inherit from base templates =Bracing= and =Armor= via
+=InheritMaterial(base, {overrides})=. Key per-material fields:
+
+** Structural / physics
+- =Stiffness= spring stiffness (bracing 200000, armor 250000)
+- =MaxCompression= / =MaxExpansion= axial break thresholds (bracing 0.90/1.10 = +/-10%!)
+- =MinLength= / =MaxLength= / =MaxLinkLength= / =MaxSegmentLength=
+- =Mass= (bracing 0.25, armor 0.50 -> 2x heavier, rope 0.001)
+- =AirDrag=, =SpringDamping=, =Pretension=
+- =AngleStressPrimaryThreshold= / =Secondary= (rope/fuse = 360 -> never break on angle)
+- =Node= = StandardNode | CableNode
+
+** Health / combat
+- =HitPoints= (bracing 150, backbracing 100, armor 400, rope 50, shield 60, solar 200)
+- =AbsorptionMomentumThreshold=, =ReflectionMomentumThreshold=, =PenetrationMomentumThreshold=
+- =CollidesWithFriendlyProjectiles= / =CollidesWithEnemyProjectiles=
+- =CollidesWithFriendlyBeams= / =CollidesWithEnemyBeams=
+- =ReflectsBeams=, =BeamPenetrationBlockDist=
+- =CatchesFire=, =DegreesPerSecondMin/Max= (fire spread speed ALONG the strut)
+
+** Economy
+- =MetalBuildCost=, =EnergyBuildCost=, =MetalRepairCost=, =EnergyRepairCost=
+- =MetalReclaim=, =EnergyReclaim=, =EnergyRunCost=, =BuildTime=, =ScrapTime=
+
+** Concrete values that confirm our model
+| Material | HP | Stiffness | Mass | MaxComp/Exp | Metal/Energy build | Fire deg/s |
+|---------------+-----+-----------+------+-------------+--------------------+------------|
+| bracing (wood)| 150 | 200000 | 0.25 | 0.90 / 1.10 | 0.1 / 0.5 | 20..32 |
+| backbracing | 100 | 200000 | 0.25 | 0.92 / 1.08 | 0.1 / 0.5 | 64..92 |
+| armor | 400 | 250000 | 0.50 | 0.90 / 1.10 | 1.8 / 1.5 | n/a |
+| rope | 50 | 50000 | 0.001| 0.60 / 1.50 | 0.1 / 0.5 | 20..100 |
+
+Notes:
+- Wood +/-10% deformation break == our axial snap rule. Armor 25% stiffer + 2x
+ mass == our research. 3 wood (450 HP) slightly > 1 armor (400 HP) == research.
+- *Background bracing* has =CollidesWith{Friendly,Enemy}Projectiles = false= and
+ =CollidesWithFriendlyBeams = false= but =CollidesWithEnemyBeams = true=:
+ projectiles pass straight through it; enemy LASERS still hit it. And it burns
+ 3x faster (64..92 vs 20..32 deg/s) — exactly why fire counters the
+ "hide supports as background bracing" trick.
+- Rope: =AngleStressPrimaryThreshold = 360= (no angle break), tension element.
+
+* Fire system (=data/db/constants.lua=, table =Fire=)
+
+| Field | Value | Meaning |
+|-------------------------------+-------+---------------------------------------|
+| DamageHitpointsPerSecond | 0.5 | burn DoT on struts |
+| DamageHitpointsPerSecondDevice| 2.5 | burn DoT on devices (5x faster) |
+| DegreesPerSecondMin/Max | 20/32 | base spread speed (per-material overrides) |
+| IgnitionTemp / MaxTemp | 100/150 | |
+| SegmentLength | 15 | fire is segmented along the strut |
+| Alarm.Delay | 7 | "fort on fire" alarm after 7s |
+
+Fire is a per-strut DoT that propagates strut-to-strut at a per-material rate,
+independent of the HP/stress systems. Background bracing burns fastest.
+
+* Extrusion (drag-build) constants (=data/db/constants.lua=, table =Extrusion=)
+
+| Field | Value | Our equivalent / note |
+|-----------------+-----------+------------------------------------------|
+| DefaultMaterial | "bracing" | |
+| MinAngle | 20 | min divergence angle of the new box |
+| MinOffset | 40 | min drag distance to start a box (cm) |
+| SnapAngle | 6 | drag direction snaps to 6-deg increments |
+| MaxLengthGrace | 500 | |
+
+Confirms drag-build is angle-flexible (not forced perpendicular) with a snap and
+a minimum size — matches what we built (free drag offset + MIN_BRACE_LENGTH).
+
+* Modding architecture
+
+** Layered loading
+1. Base =data/...= loads first, populating global tables (=Materials=,
+ =Weapons=, =Devices=, =Projectiles=, =Physics=, =Fire=, ...).
+2. Each active mod runs ON TOP, mutating those globals.
+3. Priority (=mod.lua=, 1..10, default 5; higher = loads later = final say),
+ then alphabetical. (Some workshop mods use larger numbers, e.g. 99.)
+4. =RegisterApplyMod(fn)= defers =fn= until AFTER all mods load, so a mod can
+ modify content added by later-loading mods.
+
+** Mod file layout (mirrors the base data tree)
+#+begin_src text
+<mod>/
+├── mod.lua # Selectable=true, Priority=N, Category="..." [, AIFortSpecialisation]
+├── displayname.lua # DisplayName = { ['English']=L"...", ... } (often UTF-16)
+├── publishedfileid.lua, itemversion.lua, preview.jpg # Steam Workshop metadata
+├── db/constants.lua # override physics/fire constants
+├── materials/building_materials.lua # add/modify materials
+├── weapons/weapon_list.lua, projectile_list.lua, <weapon>.lua
+├── devices/device_list.lua, <device>.lua, <device>/*.dds|png # + art
+└── ...
+#+end_src
+The loader injects a =path= variable = the mod's root, used for asset/file refs
+(e.g. =FileName = path.."/devices/ballast.lua"=).
+
+** mod.lua manifest (minimal)
+#+begin_src lua
+Selectable = true
+Priority = 6
+Category = "Devices" -- e.g. "Combat", "Physics", "Disable/Weapons"
+#+end_src
+
+** The mod API (global Lua functions exposed by the engine)
+| Function | Purpose |
+|--------------------------------------------+----------------------------------------|
+| =IndexOfMaterial/Weapon/Device(saveName)= | find an item's position in its list |
+| =FindMaterial/FindWeapon/FindProjectile(n)=| get an item table to mutate |
+| =InsertMaterialBefore/Behind(saveName, t)= | insert relative to an existing item |
+| =table.insert(Devices, IndexOfDevice(x)+1, t)= | the raw add pattern |
+| =MergeLists(t1, t2)= | concat list tables |
+| =RegisterApplyMod(fn)= / =DeregisterApplyMod=| run fn after all mods load |
+| =InheritMaterial(base, overrides)= | clone a base material with overrides |
+| =L"..."= | localized string literal |
+
+** Three concrete mod patterns observed
+1. *Disable* (=disable_weapon_cannon=): override file does
+ =local w = FindWeapon("cannon"); w.Enabled = false=.
+2. *Tweak* (commander mods): set globals + =RegisterApplyMod(fn)= where =fn=
+ scales the already-loaded weapon's fields (=FireStdDev=, =KickbackMean=...).
+3. *Add* (=fsballast= device mod): =table.insert(Devices, IndexOfDevice(
+ "repairstation")+1, { SaveName=..., FileName=path.."/devices/ballast.lua",
+ MetalCost=50, EnergyCost=250, ... })= plus the device's own definition file
+ (sets =Mass=, =HitPoints=, =Sprites=, =Root= scene-graph) and art.
+
+* What to adopt for our clone
+
+- *Data-driven materials/weapons/devices*: move the hard-coded material table in
+ =app.cpp= into Lua data (sol2), keyed by =SaveName=, with the field schema
+ above. Engine reads the tables at startup. (Tech-stack already plans sol2.)
+- *Numbers*: adopt the relative relationships (HP 150/100/400/50, armor 25%
+ stiffer + 2x mass, bg-brace burns ~3x faster, wood +/-10% break) even if our
+ absolute scale differs (we use world units, not cm).
+- *Layered loader*: base tables -> mods by (priority, name) -> =RegisterApplyMod=
+ pass. Inject a =path= per mod. Mirror the base directory tree.
+- *Reconsider an angle-stress break* (30/15 deg) alongside our force-based axial
+ break — it is in the real game and cheap to add.
+- *Fire (M4)*: per-strut DoT (0.5 hp/s, 2.5 for devices) propagating along
+ edges at a per-material deg/s rate; bg-brace fastest.
+- *Background bracing collision flags*: projectiles pass through, enemy beams
+ hit, fire reaches it — model these as per-material booleans, not a hard-coded
+ special case.
+#+begin_src text
+(Do NOT copy Forts' Lua/assets into the repo — reference only. Our own data
+ files should be written from scratch using this schema.)
+#+end_src