Skip to content

Repository files navigation

FtDSharp

Replace Lua scripting in From The Depths with C#.

FtDSharp is a mod that replaces the game's Lua-based scripting system with a C# scripting environment, compiled at runtime via Roslyn. It provides a fully custom, strongly-typed API that is designed to be intuitive and easy to use, preferring to expose information directly as properties rather than requiring lookups, and offering built-in helpers for common tasks like PID control and weapon coordination.

Alpha Status

This mod is currently in early alpha. Although it already has a superset of the Lua API and many extra features, the API is still evolving rapidly based on testing and (now) user feedback. The wiki and in-box documentation are also still in progress. Expect possible breaking changes to the API as I iterate on the design and add more features. Once the API stabilizes and the documentation is more complete, I will move to a beta phase with a Steam Workshop release.

Features

  • Full C# 10: Namespaces, pattern matching, records, lambdas, LINQ, and more
  • Complete API coverage: Features a complete* superset of the Lua API, plus many new features and helpers
  • Custom typed API: Weapons, Missiles, AI, Fleet, Drawing, Blocks, Propulsion, Warnings
  • 200+ source-generated block accessors: Typed interfaces for most block types in the game, like a GBG/GBS on steroids
  • Built-in helpers: PID helper class, WeaponController for custom weapon groups
  • Extra Features: Did I hear script controlled missile trails/smoke? Inbuilt vector drawing for debugging? Yes and yes (with more to come).
  • GitHub Wiki: A full (well, not yet) API reference and example gallery on the GitHub wiki, the primary documentation source for the mod (alongside IntelliSense descriptions)
  • Significantly faster than Lua: Roslyn-compiled C# runs faster than interpreted Lua, especially for complex scripts, and the API is designed to minimize allocations and expensive operations to keep performance impact low. Your missile guidance scripts are now less of a concern than the missiles themselves!
  • Security sandbox: Reflection, file I/O, and network access are intentionally blocked to mitigate malicious scripts. This will be iteratively improved based on user feedback/as needed.
  • In-game compile and runtime error reporting: Roslyn diagnostics with line numbers shown in the Lua box log (no more cryptic Lua errors!)
  • No perfect information: Unlike vanilla Lua, FtDSharp does NOT expose perfect information about enemy vehicles. This means that the target information you get from the API is the same as with any other system.

Note: platform support is untested but should work anywhere the game runs aside from multiplayer

* Some features might only be accessible through the source-generated classes which may be less intuitive. This will be improved over time based on user feedback. Additionally, multiplayer support is not yet implemented - I am hesitant to add it as it opens up a lot of potential for abuse. If there is significant demand for multiplayer support, I will consider adding it with appropriate safeguards.

Future Improvements To Come

  • In-box documentation: Replacement for the obsolete Lua API docs in the Help tab, auto-generated from the wiki
  • Steam Workshop release: Once the mod is (relatively) stable and well-documented, it will be released on the Steam Workshop for easier installation and updates
  • More Bespoke APIs: Slow replacement of commonly used source-generated APIs with hand-crafted ones that are more intuitive, additional helpers for common patterns, behaviour overrides, and more
  • Performance Improvements: Ongoing optimizations to reduce script impact on game performance.
  • Game Balance: Based on user feedback, some API features may receive configuration options for game balance/tuning. These will be opt-in and clearly documented to avoid confusion.
  • IDE Integration: Enabling better IDE support by providing a NuGet package to allow users to write scripts in their own IDE with full IntelliSense and compile-time checking, then copy-paste into the Programmable Block. Long-term extension of this could include a Visual Studio Code extension or similar for direct editing.
  • Improved Example Scripts: The current example scripts are ad-hoc and mostly focused on demonstrating API features. Future additions will include more polished, real-world use cases and patterns.
  • Community Contributions: As the mod matures, I hope to see contributions from the community in the form of new features, API improvements (including naming things), example scripts, and documentation enhancements.

Installation

  1. Download the latest release from the Releases page
  2. Extract the zip into your From The Depths/Mods/ folder (so that plugin.json is at Mods/FtDSharp/plugin.json)
  3. Launch the game — the mod loads and does its thing!

Quick Start

Place a Programmable Block (cosmetic change from Lua Box with the mod enabled). The default template compiles and runs immediately:

public class MyScript
{
    [OnStart]
    public void Initialize()
    {
        Log("FtDSharp script template running.");
        Log($"I am {Game.MainConstruct.Name} at {Game.MainConstruct.Position}");
    }

    [OnPhysicsTick]
    public void Update()
    {
        // TODO: implement your logic here.
    }
}

Scripts receive implicit global usings (System, System.Linq, UnityEngine, FtDSharp, and static FtDSharp.Logging), so Log() works without imports. Use the Game. prefix for construct and timing APIs unless you add your own using static FtDSharp.Game;.

Your script must declare at least one public entry-point method with an attribute such as [OnPhysicsTick], [OnStart], or [OnStop]:

[OnPhysicsTick]
void Update();  // Called every physics tick (~40 Hz at 1x)

[OnStart]
void Initialize();  // Called once when the script activates

[OnStop]
void Stop();  // Called on deactivation or destruction

Timing values are available from Game:

  • Game.RealDeltaTime (wall-clock dt between script ticks)
  • Game.GameDeltaTime (in-game scaled dt)
  • Game.RealTime / Game.GameTime

For a more detailed guide on getting started and more advanced topics, check the GitHub wiki

Why C#?

FtD's built-in Lua API is functional, but writing anything non-trivial means wrestling with index-based loops, zero type safety, and terrible error handling. FtDSharp replaces all of that with a strongly-typed C# API that handles the boilerplate for you, extends scripting capabilities to match breadboards, and gives you the full (ish) power of C# to write and debug your scripts.

Missile Guidance: Lua vs C#

A real community Lua missile guidance script - 60+ lines with index loops, magic numbers, and all API calls must occur within Update(I):

Lua: 60+ lines
-- Credit to jalanisa for corrections
launchers = {}

function init(I)
    I:ClearLogs()
    for iw = 0, I:GetWeaponCount()-1 do
        local w = I:GetWeaponInfo(iw)
        if w.Valid and w.WeaponType == 7 then -- magic number for missile launcher
            table.insert(launchers, iw)
        end
    end
end

function apply_to_missiles(I, fn)
    for it = 0, I:GetLuaTransceiverCount()-1 do
        for im = 0, I:GetLuaControlledMissileCount(it)-1 do
            fn(it, im)
        end
    end
end

function Update(I)
    if init then init = init(I) end

    if I:GetNumberOfTargets(0) == 0 then
        apply_to_missiles(I, function(it, im)
            I:DetonateLuaControlledMissile(it, im)
        end)
        return
    end

    for _, iw in ipairs(launchers) do
        I:FireWeapon(iw, 0)
    end

    local target = I:GetTargetInfo(0, 0)

    apply_to_missiles(I, function(it, im)
        local missileInfo = I:GetLuaControlledMissileInfo(it, im)
        local timeToImpact = Vector3.Distance(missileInfo.Position, target.Position)
                           / missileInfo.Velocity.magnitude;

        local predicted = target.Position + target.Velocity * timeToImpact;
        I:SetLuaControlledMissileAimPoint(it, im, predicted.x, predicted.y, predicted.z)
    end)
end

C#: 26 lines, same logic:

public class MissileGuidance
{
    [OnPhysicsTick]
    public void Update()
    {
        var target = AI.HighestPriorityMainframe.PrimaryTarget;
        if (target == null)
        {
            foreach (var m in Guidance.Missiles) m.Detonate();
            return;
        }

        foreach (var controller in Weapons.MissileControllers)
            controller.Fire();

        foreach (var missile in Guidance.Missiles)
        {
            float timeToImpact = Vector3.Distance(missile.Position, target.Position)
                               / missile.Velocity.magnitude;

            Vector3 predicted = target.Position + target.Velocity * timeToImpact;

            missile.AimAt(predicted);
        }
    }
}

No index loops or annoying boilerplate.

PID Control: Lua vs C#

Lua PID: you have to build everything yourself:

Lua: 50+ lines just for the PID implementation
-- Credit to errorstringexpectedgotnil on the OFD discord for this Lua PID library implementation
PIDLib = {}

PIDLib.sdt = 0.025

function PIDLib.new(Kp, Ti, Td)
  local pid = {}
  pid.Kp = Kp
  pid.Td = Td
  pid.errSum = 0
  pid.prevErr = 0

  if Ti == 0 then
    pid.inverseTi = 0
  else
    pid.inverseTi = 1/Ti
  end

  setmetatable(pid, PIDLib.mt)

  return pid
end

function PIDLib.iterate(pid, err, deltaTime)
  pid.errSum = pid.errSum + err * deltaTime
  local output = pid.Kp * (err + pid.inverseTi * pid.errSum + pid.Td * ((err - pid.prevErr) / deltaTime))
  pid.prevErr = err
  return output
end

function PIDLib.opinion(pid, err, deltaTime)
  return pid.Kp * (err + pid.inverseTi * (pid.errSum + err) + pid.Td * ((err - pid.prevErr) / deltaTime))
end

function PIDLib.reset(pid)
  pid.errSum = 0
  pid.prevErr = 0
end

function PIDLib.standardIterate(pid, err)
  return PIDLib.iterate(pid, err, PIDLib.sdt)
end

function PIDLib.standardOpinion(pid, err)
  return PIDLib.opinion(pid, err, PIDLib.sdt)
end

PIDLib.mt = {}

PIDLib.mt.__add = PIDLib.standardIterate
PIDLib.mt.__mul = PIDLib.standardOpinion
PIDLib.mt.__unm = PIDLib.reset

-- Then in your main script, manually wire it up:
local altPid = PIDLib.new(0.1, 50, 0.5)
function Update(I)
    local error = targetAltitude - I:GetConstructPosition().y
    local output = PIDLib.iterate(altPid, error, 0.025)
    I:RequestThrustControl(5, output)  -- raw axis index
end

C#: just bind and update:

public class AltitudeHold
{
    private readonly PID _altPid = PID.Bind(
        input: () => Game.MainConstruct.Position.y,
        output: v => Game.MainConstruct.Propulsion.Hover = v,
        setpoint: () => 200f // defaults same as AI PIDs - kP=0.05f, kI=250f, kD=0.3f
    );

    [OnPhysicsTick]
    public void Update() => _altPid.Update(Game.GameDeltaTime);
}

The PID helper class handles error computation, integral accumulation, derivative smoothing, and output clamping. You just bind inputs and outputs and call Update().

Weapon Control: Lua vs C#

Firing cannons at a target with velocity prediction in Lua:

Lua — manual lead calculation with no gravity or ballistic arc
-- Credit to maglor6 on the OFD discord for this Lua weapon control example
function Update(I)
    for AI = 0, (I:GetNumberOfMainframes() - 1) do
        for T = 0, (I:GetNumberOfTargets(AI) - 1) do
            TI = I:GetTargetPositionInfo(AI, T)
            TP = TI.Position
            TV = TI.Velocity
            R = TI.Range
            for W = 0, (I:GetWeaponCount() - 1) do
                WI = I:GetWeaponInfo(W)
                GP = WI.GlobalPosition
                V = WI.Speed
                IT = R / V
                APX = TP.x - GP.x + IT * TV.x
                APY = TP.y - GP.y + IT * TV.y
                APZ = TP.z - GP.z + IT * TV.z
                I:AimWeaponInDirection(W, APX, APY, APZ, 0)
                I:FireWeapon(W, 0)
            end
        end
    end
end

Even with all this manual math, this Lua version only handles simple velocity leading - not accounting for gravity, possible high vs low ballistic arcs, or terrain checks. That could easily be a few hundred more lines of Lua code to implement properly.

C# — one-line tracking with full ballistic prediction:

public class SimpleWeaponControl
{
    [OnPhysicsTick]
    public void Update()
    {
        var target = AI.HighestPriorityMainframe.PrimaryTarget;
        if (target == null) return;

        foreach (var weapon in Weapons.All)
            if (weapon.Track(target).CanFire)
                weapon.Fire();
    }
}

weapon.Track() handles velocity leading, acceleration, gravity, ballistic arc selection, checks terrain blocking, and returns a status you can access easily, no index loops or manual vector math required.

Error Handling

Lua: Errors are cryptic, usually unhelpful, and only show happen at runtime when the offending code is executed: IMG_20260217_074224

FtDSharp: Clear Roslyn diagnostics with line numbers, shown in the Lua box log: 546665243-96ea2b2b-e137-4030-9776-d47d13ced1c6

Note: the pretty UI shown here is part of jalanisa's AtsuLuaEditor mod, but FtDSharp's error reporting works with or without it.

Brief API Overview

Static Class Purpose
Game MainConstruct, Time, TicksSinceStart
AI Mainframes, HighestPriorityMainframe
Weapons All, Turrets, APS, MissileControllers, typed accessors
Guidance Active missiles in flight
Friendly All, AllExcludingSelf, Fleets, MyFleet
Warnings IncomingProjectiles, IncomingMissiles
Drawing Arrow, Line, Sphere, Cross, and more
Blocks Auto-generated typed accessors for all block types
Logging Log, LogWarning, LogError, ClearLogs
Helper Purpose
PID PID controller with bindable input/output
WeaponController Coordinate multiple weapons and turrets
Key Interface Description
IWeapon Weapon block with Track, Fire, AimAt
ITurret Turret coordinating child weapons
IMainframe AI mainframe with PrimaryTarget, GetAimpoint
ITargetable Anything trackable: Position, Velocity, Acceleration
IMissile Script-controllable missile with typed parts
IBlock Base for all blocks: Position, Parent, IsOnRoot

For detailed API documentation and examples, see the GitHub wiki

Example Scripts

The ExampleScripts/ folder contains working examples demonstrating a large portion of the API surface:

Script Demonstrates
BasicThrustControl.cs Altitude hold + target tracking via Propulsion API
DrawingDemo.cs Visual debug Drawing API (arrows, spheres, gimbals)
FleetAwarenessDemo.cs Fleet/friendly awareness with visualization
GenericBlockGetterSetterDemo.cs Auto-generated block API with read/write properties
MissilePartsDemo.cs Typed missile part access and visual control
MultiAIAimpointComparison.cs Comparing aimpoints across multiple AI mainframes
NaiveMissileGuidance.cs Simple missile guidance: fire + aim at target
PidControlDemo.cs PID controller helper with bound inputs/outputs
ProjectileWarningsDemo.cs Incoming projectile warning visualization
SubObjectHierarchy.cs SpinBlock parent hierarchy inspection
SubTurretDemo.cs Independent sub-turret targeting with WeaponController
TargetInfoPrinter.cs Print detailed target information
WeaponControlDemo.cs Full weapon control with typed accessors + pattern matching
WeaponTypePropertiesDemo.cs Typed block interfaces extending IWeapon

Building from Source

Prerequisites

  • .NET SDK 8.0+
  • A copy of From The Depths (for the game's managed DLLs)

Steps

  1. Clone the repository:

    git clone https://github.com/trk20/FtDSharp.git
  2. Copy the game's managed DLLs into a ftd-managed/ folder at the project root:

    <Steam>/steamapps/common/From The Depths/From_The_Depths_Data/Managed/*  →  ftd-managed/
    

    For IDE projects (ScriptProject/, ExampleScripts/), also copy UnityEngine into References/:

    mkdir -p References
    cp ftd-managed/UnityEngine.CoreModule.dll References/

    (release.sh does this automatically for ScriptProject/)

  3. Run the code generator to produce API bindings:

    dotnet run --project FtDSharp.CodeGen
  4. Build:

    dotnet build

    The post-build step copies the required DLLs to the project root, ready for the game's mod loader.

License

This project is currently licensed under CC BY-SA 4.0.

About

Mod for the game From The Depths that replaces Lua scripting with C#

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages