Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
*.suo
*.user
The Long Dark Save Editor 2/bin/
The Long Dark Save Editor 2/obj/
The Long Dark Save Editor 2/obj/
# Build output for all projects (Core/Server live under src/)
[Bb]in/
[Oo]bj/
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@
Edit The Long Dark save files

Downloads from ModDB: http://www.moddb.com/mods/the-long-dark-save-editor-2/downloads

## Cross-platform web editor (Linux / macOS / Windows)

The original editor is a Windows-only WPF app. This repository also includes a
cross-platform port under `src/`: a portable core library (`TldSaveEditor.Core`)
and a small local web app (`TldSaveEditor.Server`) that runs in your browser.

**Requirements:** the [.NET 10 SDK](https://dotnet.microsoft.com/download).

**Run it:**

- Linux / macOS: `./run.sh`
- Windows: `run.cmd` (double-click) — or any platform: `dotnet run --project src/TldSaveEditor.Server -c Release`

It builds on first run, serves `http://127.0.0.1:5173` (loopback only) and opens
your browser. Your save folder is detected automatically (Windows `%LOCALAPPDATA%`,
Linux Steam/Proton prefix, macOS Application Support); you can also point it elsewhere
in the UI. Every save writes a timestamped backup to a `backups/` folder next to the
save, and the UI can restore any backup.

The editor covers the Player, Skills, Inventory, Afflictions, Map and Profile tabs.
35 changes: 32 additions & 3 deletions The Long Dark Save Editor 2/Game data/GlobalData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,25 @@ public class FoodItemSaveDataProxy
public bool m_Packaged { get; set; }
}

// The game stores liquid volume / fuel as a fixed-point Int64 where 1 litre == 1_000_000_000.
// Reading those values into a float overflows the UI (e.g. 2L is shown as 2E+09) and corrupts
// the save on write. See issues #85, #87 and the Storm Lantern part of #74.
public static class FixedPoint
{
public const long LitersScale = 1_000_000_000;
}

public class LiquidItemSaveDataProxy
{
public float m_LiquidLitersProxy { get; set; }
public long m_LiquidLitersProxy { get; set; }
public EnumWrapper<LiquidQuality> m_LiquidQuality { get; set; }

[JsonIgnore]
public double Liters
{
get => m_LiquidLitersProxy / (double)FixedPoint.LitersScale;
set => m_LiquidLitersProxy = (long)Math.Round(value * FixedPoint.LitersScale);
}
}

public class FlareItemSaveDataProxy
Expand All @@ -372,8 +387,15 @@ public class FlashlightItemSaveDataProxy
public class KeroseneLampItemSaveDataProxy
{
public float m_HoursPlayed { get; set; }
public float m_CurrentFuelLitersProxy { get; set; }
public long m_CurrentFuelLitersProxy { get; set; }
public bool m_OnProxy { get; set; }

[JsonIgnore]
public double Liters
{
get => m_CurrentFuelLitersProxy / (double)FixedPoint.LitersScale;
set => m_CurrentFuelLitersProxy = (long)Math.Round(value * FixedPoint.LitersScale);
}
}

public class ClothingItemSaveDataProxy
Expand All @@ -396,7 +418,14 @@ public class GunItemSaveDataProxy

public class WaterSupplySaveDataProxy
{
public float m_VolumeProxy { get; set; }
public long m_VolumeProxy { get; set; }

[JsonIgnore]
public double Liters
{
get => m_VolumeProxy / (double)FixedPoint.LitersScale;
set => m_VolumeProxy = (long)Math.Round(value * FixedPoint.LitersScale);
}
}

public class BedSaveDataProxy
Expand Down
14 changes: 7 additions & 7 deletions The Long Dark Save Editor 2/Helpers/ItemDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static ItemDictionary()
{
// First aid
AddItemInfo("GEAR_BottleAntibiotics", ItemCategory.FirstAid, @"{""StackableItem"": {""m_UnitsProxy"": 6}}");
AddItemInfo("GEAR_BottleHydrogenPeroxide", ItemCategory.FirstAid, @"{""LiquidItem"": {""m_LiquidLitersProxy"": 0.5,""m_LiquidQuality"": { ""Value"":""NonPotable""}}}");
AddItemInfo("GEAR_BottleHydrogenPeroxide", ItemCategory.FirstAid, @"{""LiquidItem"": {""m_LiquidLitersProxy"": 500000000,""m_LiquidQuality"": { ""Value"":""NonPotable""}}}");
AddItemInfo("GEAR_BottlePainKillers", ItemCategory.FirstAid, @"{""StackableItem"": {""m_UnitsProxy"": 6}}");
AddItemInfo("GEAR_HeavyBandage", ItemCategory.FirstAid, @"{""StackableItem"": {""m_UnitsProxy"": 1}}");
AddItemInfo("GEAR_OldMansBeardDressing", ItemCategory.FirstAid, @"{""StackableItem"": {""m_UnitsProxy"": 1}}");
Expand Down Expand Up @@ -181,8 +181,8 @@ static ItemDictionary()
AddItemInfo("GEAR_SodaGrape", ItemCategory.Food, @"{""FoodItem"": {""m_CaloriesRemainingProxy"": 250}}");
AddItemInfo("GEAR_SodaOrange", ItemCategory.Food, @"{""FoodItem"": {""m_CaloriesRemainingProxy"": 250}}");
AddItemInfo("GEAR_TomatoSoupCan", ItemCategory.Food, @"{""FoodItem"": {""m_CaloriesRemainingProxy"": 300}, ""SmashableItem"": {}}");
AddItemInfo("GEAR_WaterSupplyNotPotable", ItemCategory.Food, @"{""WaterSupply"":{""m_VolumeProxy"": 1}}", true);
AddItemInfo("GEAR_WaterSupplyPotable", ItemCategory.Food, @"{""WaterSupply"":{""m_VolumeProxy"": 1}}", true);
AddItemInfo("GEAR_WaterSupplyNotPotable", ItemCategory.Food, @"{""WaterSupply"":{""m_VolumeProxy"": 1000000000}}", true);
AddItemInfo("GEAR_WaterSupplyPotable", ItemCategory.Food, @"{""WaterSupply"":{""m_VolumeProxy"": 1000000000}}", true);
AddItemInfo("GEAR_WolfQuarter", ItemCategory.Food, @"{""BodyHarvest"": {m_QuarterBagWasteMultiplier:2, m_MeatAvailableKG: 1, m_Condition: 100, m_DamageSide:{Value:""DamageSideRight""}}}");
AddItemInfo("GEAR_BearQuarter", ItemCategory.Food, @"{""BodyHarvest"": {m_QuarterBagWasteMultiplier:2, m_MeatAvailableKG: 1, m_Condition: 100, m_DamageSide:{Value:""DamageSideRight""}}}");
AddItemInfo("GEAR_StagQuarter", ItemCategory.Food, @"{""BodyHarvest"": {m_QuarterBagWasteMultiplier:2, m_MeatAvailableKG: 1, m_Condition: 100, m_DamageSide:{Value:""DamageSideRight""}}}");
Expand Down Expand Up @@ -218,12 +218,12 @@ static ItemDictionary()
AddItemInfo("GEAR_HighQualityTools", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_Hook", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_HookAndLine", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_JerrycanRusty", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 2,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_KeroseneLampB", ItemCategory.Tools, @"{""KeroseneLampItem"": {""m_CurrentFuelLitersProxy"": 1}}");
AddItemInfo("GEAR_JerrycanRusty", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 2000000000,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_KeroseneLampB", ItemCategory.Tools, @"{""KeroseneLampItem"": {""m_CurrentFuelLitersProxy"": 1000000000}}");
AddItemInfo("GEAR_Knife", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_KnifeImprovised", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_LampFuel", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 0.5,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_LampFuelFull", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 0.5,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_LampFuel", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 500000000,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_LampFuelFull", ItemCategory.Tools, @"{""LiquidItem"": {m_LiquidLitersProxy: 500000000,m_LiquidQuality:{Value:""NonPotable""}}}");
AddItemInfo("GEAR_Line", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_MagnifyingLens", ItemCategory.Tools, @"{}");
AddItemInfo("GEAR_NewsprintRoll", ItemCategory.Tools, @"{}");
Expand Down
10 changes: 3 additions & 7 deletions The Long Dark Save Editor 2/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,9 @@ public ObservableCollection<EnumerationMember> Saves

public MainWindow()
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
//MissingMemberHandling = MissingMemberHandling.Error,
FloatFormatHandling = FloatFormatHandling.Symbol,
// Serialize byte arrays as arrays of numbers instead of base64
Converters = new List<JsonConverter> { new ByteArrayConverter() },
};
// JsonConvert.DefaultSettings (FloatFormatHandling.Symbol + ByteArrayConverter) is
// configured once in GameSave's static constructor so the WPF app and the web server
// share a single definition.

#if DEBUG
IsDebug = true;
Expand Down
6 changes: 3 additions & 3 deletions The Long Dark Save Editor 2/Tabs/InventoryTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@
<StackPanel Visibility="{Binding Gear.LiquidItem, Converter={StaticResource ObjectToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" Margin="0 0 0 5">
<Label Width="130" Content="{x:Static p:Resources.AmountLiter}" />
<TextBox Width="200" Text="{Binding Gear.LiquidItem.m_LiquidLitersProxy}"></TextBox>
<TextBox Width="200" Text="{Binding Gear.LiquidItem.Liters}"></TextBox>
</StackPanel>
</StackPanel>
<StackPanel Visibility="{Binding Gear.KeroseneLampItem, Converter={StaticResource ObjectToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" Margin="0 0 0 5">
<Label Width="130" Content="{x:Static p:Resources.AmountLiter}" />
<TextBox Width="200" Text="{Binding Gear.KeroseneLampItem.m_CurrentFuelLitersProxy}"></TextBox>
<TextBox Width="200" Text="{Binding Gear.KeroseneLampItem.Liters}"></TextBox>
</StackPanel>
</StackPanel>
<StackPanel Visibility="{Binding Gear.WeaponItem, Converter={StaticResource ObjectToVisibilityConverter}}">
Expand All @@ -78,7 +78,7 @@
<StackPanel Visibility="{Binding Gear.WaterSupply, Converter={StaticResource ObjectToVisibilityConverter}}">
<StackPanel Orientation="Horizontal" Margin="0 0 0 5">
<Label Width="130" Content="{x:Static p:Resources.AmountLiter}" />
<TextBox Width="200" Text="{Binding Gear.WaterSupply.m_VolumeProxy}"></TextBox>
<TextBox Width="200" Text="{Binding Gear.WaterSupply.Liters}"></TextBox>
</StackPanel>
</StackPanel>
<StackPanel Visibility="{Binding Gear.InProgressItem, Converter={StaticResource ObjectToVisibilityConverter}}">
Expand Down
6 changes: 6 additions & 0 deletions TldSaveEditor.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/TldSaveEditor.Core/TldSaveEditor.Core.csproj" />
<Project Path="src/TldSaveEditor.Server/TldSaveEditor.Server.csproj" />
</Folder>
</Solution>
10 changes: 10 additions & 0 deletions run.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@echo off
REM Launch the TLD Save Editor (local web app) on Windows.
REM Builds on first run, then serves http://127.0.0.1:5173 and opens your browser.
REM Requires the .NET 10 SDK (https://dotnet.microsoft.com/download).
REM
REM Usage:
REM run.cmd (double-click, or run from a terminal)
REM run.cmd --no-browser run without opening a browser
cd /d "%~dp0"
dotnet run --project "src\TldSaveEditor.Server" -c Release %*
12 changes: 12 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Launch the TLD Save Editor (local web app). Builds on first run, then starts the
# server bound to 127.0.0.1:5173 and opens your browser.
#
# Usage:
# ./run.sh # build (Release) + run + open browser
# ./run.sh --no-browser # run without opening a browser
set -euo pipefail

cd "$(dirname "$0")"

exec dotnet run --project src/TldSaveEditor.Server -c Release "$@"
176 changes: 176 additions & 0 deletions src/TldSaveEditor.Core/GameData/Afflictions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using The_Long_Dark_Save_Editor_2.Helpers.Helpers;

namespace The_Long_Dark_Save_Editor_2.Game_data
{

public class Affliction
{

public AfflictionType AfflictionType { get; set; }
public int Location { get; set; }
public ICommand RemoveCommand { get; set; }

public Affliction(ObservableCollection<Affliction> collection)
{
RemoveCommand = new CommandHandler(() =>
{
collection.Remove(this);
});
}
}

public class Hypothermia : Affliction
{
public Hypothermia(ObservableCollection<Affliction> collection) : base(collection) { }

private static int hoursSpentFreezingRequired = 10;
public float ElapsedHours { get; set; }
public float ElapsedWarmHours { get; set; }
public float RiskChance
{
get { return ElapsedHours / hoursSpentFreezingRequired; }
}
public string Cause { get; set; }
}

public class Frostbite : Affliction
{
public Frostbite(ObservableCollection<Affliction> collection) : base(collection) { }

private static int[] bodyAreaHP = new int[] { 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100 };

public float DamagePercentage
{
get { return Damage / bodyAreaHP[Location]; }
set { Damage = bodyAreaHP[Location] * value; }
}
public float Damage { get; set; }
}

public class FoodPoisoning : Affliction
{
public FoodPoisoning(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool AntibioticsTaken { get; set; }
public float ElapsedRest { get; set; }
public string Cause { get; set; }
}

public class Dysentery : Affliction
{
public Dysentery(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool AntibioticsTaken { get; set; }
public float ElapsedRest { get; set; }
public float CleanWaterConsumed { get; set; }
}

public class SprainAffliction : Affliction
{
public SprainAffliction(ObservableCollection<Affliction> collection) : base(collection) { }

public string CauseLocID { get; set; }
public float Duration { get; set; }
public float ElapsedHours { get; set; }
public float ElapsedRest { get; set; }
}

public class Burns : Affliction
{
public Burns(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool PainKillersTaken { get; set; }
public bool BandageApplied { get; set; }
public string CauseLocID { get; set; }
}

public class BurnsElectric : Affliction
{
public BurnsElectric(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool PainKillersTaken { get; set; }
public bool BandageApplied { get; set; }
}

public class BloodLoss : Affliction
{
public BloodLoss(ObservableCollection<Affliction> collection) : base(collection) { }

public string CauseLocID { get; set; }
public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
}

public class Infection : Affliction
{
public Infection(ObservableCollection<Affliction> collection) : base(collection) { }

public string CauseLocID { get; set; }
public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool AntibioticsTaken { get; set; }
public float ElapsedRest { get; set; }
}

public class InfectionRisk : Affliction
{
public InfectionRisk(ObservableCollection<Affliction> collection) : base(collection) { }

public string CauseLocID { get; set; }
public float ElapsedHours { get; set; }
public float DurationHours { get; set; }
public bool AntisepticTaken { get; set; }
public float CurrentInfectionChance { get; set; }
public bool Constant { get; set; }
}

public class CabinFever : Affliction
{
public CabinFever(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHours { get; set; }
}

public class IntestinalParasites : Affliction
{
public IntestinalParasites(ObservableCollection<Affliction> collection) : base(collection) { }

public float CurrentInfectionChance { get; set; }
public float ParasitesElapsedHours { get; set; }
public float RiskElapsedHours { get; set; }
public float RiskDurationHours { get; set; }
public int DosesTaken { get; set; }
public bool HasTakenDoseToday { get; set; }
public int DayToAllowNextDose { get; set; }
public int PiecesEatenThisRiskCycle { get; set; }

}

public class BrokenRib : Affliction
{
public BrokenRib(ObservableCollection<Affliction> collection) : base(collection) { }

public string CauseLocID { get; set; }
public int PainKillersTaken { get; set; }
public int BandagesApplied { get; set; }
public float ElapsedRest { get; set; }
public float NumHoursRestForCure { get; set; }
}

public class WellFed : Affliction
{
public WellFed(ObservableCollection<Affliction> collection) : base(collection) { }

public float ElapsedHoursNotStarving { get; set; }
}
}
Loading