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.
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.
- 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.
- 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.
- Download the latest release from the Releases page
- Extract the zip into your
From The Depths/Mods/folder (so thatplugin.jsonis atMods/FtDSharp/plugin.json) - Launch the game — the mod loads and does its thing!
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 destructionTiming 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
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.
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)
endC#: 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.
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
endC#: 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().
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
endEven 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.
Lua: Errors are cryptic, usually unhelpful, and only show happen at runtime when the offending code is executed:

FtDSharp: Clear Roslyn diagnostics with line numbers, shown in the Lua box log:

Note: the pretty UI shown here is part of jalanisa's AtsuLuaEditor mod, but FtDSharp's error reporting works with or without it.
| 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
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 |
- .NET SDK 8.0+
- A copy of From The Depths (for the game's managed DLLs)
-
Clone the repository:
git clone https://github.com/trk20/FtDSharp.git
-
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 intoReferences/:mkdir -p References cp ftd-managed/UnityEngine.CoreModule.dll References/
(
release.shdoes this automatically forScriptProject/) -
Run the code generator to produce API bindings:
dotnet run --project FtDSharp.CodeGen
-
Build:
dotnet build
The post-build step copies the required DLLs to the project root, ready for the game's mod loader.
This project is currently licensed under CC BY-SA 4.0.