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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ SortGenerationList/bin/
SortGenerationList/obj/
*.bak
DeltaModifier.txt

_patch_apply_pending.py
_patch*.py
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Changelog

All notable changes in **[phillenton/D-OS-Save-Editor](https://github.com/phillenton/D-OS-Save-Editor)** (this repository) are documented here. The original open-source project by Anthony Jiang is **[AnthonyZJiang/D-OS-Save-Editor](https://github.com/AnthonyZJiang/D-OS-Save-Editor)**.

## Post-fork improvements (master)

### Session and save UX

- **Dirty state:** Window title `*` and “Not saved to file” when there are in-memory changes not written to disk; close prompt; cleared on successful Save and Reset.
- **Unapplied edits:** Character **Apply** and inventory **Apply changes** enable when pending edits exist; orange labels (“Unapplied edits”) with **Apply** before the label so the button does not jump; close warns if character or inventory edits are still unapplied.
- **Tabs:** Stats, Abilities, Traits, Talents report pending edits so Apply state stays accurate.

### Inventory

- **Saving item edits:** `WriteEditsToLsxAsync` loads a fresh `globals.lsx` document; inventory changes must target **that** tree. Previously, `ItemXmlNodes` from the initial parse pointed at the old DOM, so amount and other item edits did not persist to disk while character attributes (written via fresh XPath) still worked. Writes now replay the same BFS inventory walk on the loaded document. `WritePlayer` also uses a sequential loop instead of `Parallel.For` so multiple players do not concurrently mutate one `XmlDocument`.
- **Amount (stack count):** Centralized rules via `IsAmountEditable()`. Amount is editable for stackable-style items (potions, loot, skill books, arrows, etc.) and **not** for equipment-style rows (weapons, armor, furniture), quest items, keys, **Unique** rarity, or stats ids starting with `arm_` (armor), matching game behavior better than the old allow-list (potion/gold/grenade/scroll/food only).
- **Gold:** `DataTable.GoldNames` extended (e.g. `larger_gold`) so gold is categorized and filtered correctly.
- **Layout:** Inventory pending label aligned with character bar (Apply left, message right).

### Build

- **SortGenerationList** retargeted to **.NET Framework 4.6.2** so the solution builds with the same targeting pack as the main WPF app (`App.config` / `csproj` updated).

### Other

- **wishlist.md** — backlog and done reference for future work (nested containers, skills/story tabs, D:OS 2, etc.).
6 changes: 5 additions & 1 deletion D-OS Save Editor/About.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="30,20,30,0" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="Open source at" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Margin="5,0,0,0"><Hyperlink NavigateUri="https://github.com/tmxkn1/D-OS-Save-Editor" RequestNavigate="Hyperlink_RequestNavigate">Github</Hyperlink></TextBlock>
<TextBlock Margin="5,0,0,0"><Hyperlink NavigateUri="https://github.com/phillenton/D-OS-Save-Editor" RequestNavigate="Hyperlink_RequestNavigate">Github</Hyperlink></TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="30,5,30,0" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="Original project:" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Margin="5,0,0,0"><Hyperlink NavigateUri="https://github.com/AnthonyZJiang/D-OS-Save-Editor" RequestNavigate="Hyperlink_RequestNavigate">AnthonyZJiang/D-OS-Save-Editor</Hyperlink></TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="30,0,30,0" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="Use and discussions:" HorizontalAlignment="Left" VerticalAlignment="Center"/>
Expand Down
2 changes: 1 addition & 1 deletion D-OS Save Editor/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public MainWindow()
private async Task CheckUpdate()
{
Debug.WriteLine("start");
const string urlAddress = "https://github.com/tmxkn1/D-OS-Save-Editor/blob/master/UpdateCheck";
const string urlAddress = "https://github.com/phillenton/D-OS-Save-Editor/blob/master/UpdateCheck";
_updateLink = null;
UpdatePanel.Visibility = Visibility.Collapsed;

Expand Down
1 change: 1 addition & 0 deletions D-OS Save Editor/SE/AbilitiesTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Setter Property="Width" Value="35"/>
<EventSetter Event="LostFocus" Handler="TextBoxEventSetter_OnLostFocus"/>
<EventSetter Event="PreviewTextInput" Handler="TextBoxEventSetter_OnPreviewTextInput"/>
<EventSetter Event="TextChanged" Handler="CharacterField_TextChanged"/>
</Style>
<Style TargetType="RowDefinition">
<Setter Property="Height" Value="Auto"/>
Expand Down
52 changes: 52 additions & 0 deletions D-OS Save Editor/SE/AbilitiesTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls.Primitives;

namespace D_OS_Save_Editor
{
Expand All @@ -11,6 +12,7 @@ namespace D_OS_Save_Editor
public partial class AbilitiesTab
{
private Player _player;
private bool _suppressFieldEvents;
private Brush DefaultTextBoxBorderBrush { get; }

public Player Player
Expand All @@ -30,8 +32,53 @@ public AbilitiesTab()
DefaultTextBoxBorderBrush = ManAtArmsTextBox.BorderBrush;
}

public bool HasPendingEdits()
{
if (Player == null) return false;
if (ManAtArmsTextBox.Text != Player.Abilities[(int)DataTable.Abilities.ManAtArms].ToString()) return true;
if (ExpertMarksmanTextBox.Text != Player.Abilities[(int)DataTable.Abilities.ExpertMarksman].ToString()) return true;
if (ScoundrelTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Scoundrel].ToString()) return true;
if (SingleHandedTextBox.Text != Player.Abilities[(int)DataTable.Abilities.SingleHanded].ToString()) return true;
if (TwoHandedTextBox.Text != Player.Abilities[(int)DataTable.Abilities.TwoHanded].ToString()) return true;
if (BowTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Bow].ToString()) return true;
if (CrossbowTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Crossbow].ToString()) return true;
if (ShieldSpecialistTextBox.Text != Player.Abilities[(int)DataTable.Abilities.ShieldSpecialist].ToString()) return true;
if (ArmourSpecialistTextBox.Text != Player.Abilities[(int)DataTable.Abilities.ArmourSpecialist].ToString()) return true;
if (WitchcraftTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Witchcraft].ToString()) return true;
if (TelekinesisTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Telekinesis].ToString()) return true;
if (WillpowerTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Willpower].ToString()) return true;
if (PyrokineticTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Pyrokinetic].ToString()) return true;
if (HydrosophistTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Hydrosophist].ToString()) return true;
if (AerotheurgeTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Aerotheurge].ToString()) return true;
if (GeomancerTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Geomancer].ToString()) return true;
if (BlacksmithingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Blacksmithing].ToString()) return true;
if (SneakingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Sneaking].ToString()) return true;
if (PickpocketingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Pickpocketing].ToString()) return true;
if (LockpickingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Lockpicking].ToString()) return true;
if (LoremasterTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Loremaster].ToString()) return true;
if (CraftingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Crafting].ToString()) return true;
if (BarteringTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Bartering].ToString()) return true;
if (CharismaTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Charisma].ToString()) return true;
if (LeadershipTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Leadership].ToString()) return true;
if (LuckyCharmTextBox.Text != Player.Abilities[(int)DataTable.Abilities.LuckyCharm].ToString()) return true;
if (BodyBuildingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.BodyBuilding].ToString()) return true;
if (DualWieldingTextBox.Text != Player.Abilities[(int)DataTable.Abilities.DualWielding].ToString()) return true;
if (WandTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Wand].ToString()) return true;
if (TenebriumTextBox.Text != Player.Abilities[(int)DataTable.Abilities.Tenebrium].ToString()) return true;
return false;
}

private void CharacterField_TextChanged(object sender, TextChangedEventArgs e)
{
if (_suppressFieldEvents) return;
if (Window.GetWindow(this) is SaveEditor editor)
editor.RefreshCharacterApplyPendingState();
}
public void UpdateForm()
{
_suppressFieldEvents = true;
try
{
ManAtArmsTextBox.Text = Player.Abilities[(int)DataTable.Abilities.ManAtArms].ToString();
ExpertMarksmanTextBox.Text = Player.Abilities[(int)DataTable.Abilities.ExpertMarksman].ToString();
ScoundrelTextBox.Text = Player.Abilities[(int)DataTable.Abilities.Scoundrel].ToString();
Expand Down Expand Up @@ -62,6 +109,11 @@ public void UpdateForm()
DualWieldingTextBox.Text = Player.Abilities[(int)DataTable.Abilities.DualWielding].ToString();
WandTextBox.Text = Player.Abilities[(int)DataTable.Abilities.Wand].ToString();
TenebriumTextBox.Text = Player.Abilities[(int)DataTable.Abilities.Tenebrium].ToString();
}
finally
{
_suppressFieldEvents = false;
}
}

public void SaveEdits()
Expand Down
53 changes: 33 additions & 20 deletions D-OS Save Editor/SE/InventoryTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Setter Property="HorizontalAlignment" Value="Left"></Setter>
<EventSetter Event="LostFocus" Handler="TextBoxEventSetter_OnLostFocus"/>
<EventSetter Event="PreviewTextInput" Handler="TextBoxEventSetter_OnPreviewTextInput"/>
<EventSetter Event="TextChanged" Handler="InventoryDetailField_Changed"/>
</Style>
<Style TargetType="RowDefinition">
<Setter Property="Height" Value="Auto"/>
Expand Down Expand Up @@ -45,7 +46,8 @@
<TextBox Grid.Column="1" x:Name="SearchTextBox" Uid="SearchText" Margin="10,0,20,0" VerticalAlignment="Top" HorizontalAlignment="Stretch" Width="Auto" TextChanged="SearchTextBox_OnTextChanged"/>
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="Use space to separate" FontSize="10" FontStyle="Italic"/>
</Grid>
<ListBox x:Name="ItemsListBox" Grid.Row="1" Grid.RowSpan="3" HorizontalAlignment="Left" MinHeight="430" Height="Auto" Margin="10,0,20,10" VerticalAlignment="Top" Width="239" SelectionChanged="ItemsListBox_OnSelectionChanged"/>
<TreeView x:Name="ItemsTreeView" Grid.Row="1" Grid.RowSpan="3" HorizontalAlignment="Left" MinHeight="430" Height="Auto" Margin="10,0,20,10" VerticalAlignment="Top" Width="280"
SelectedItemChanged="ItemsTreeView_OnSelectedItemChanged"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" Margin="0,0,0,10">
<Button Content="Check all" Margin="0,0,20,0" Padding="5,2" Width="80" Click="CheckAllButton_OnClick" VerticalAlignment="Top"/>
<Button Content="Uncheck all" Padding="5,2" Width="80" Click="UncheckAllButtonBase_OnClick" VerticalAlignment="Top"/>
Expand All @@ -64,6 +66,7 @@
<CheckBox x:Name="ShowScrollCheckBox" Content="Scroll" Tag="{x:Static local:ItemSortType.Scroll}"/>
<CheckBox x:Name="ShowPotionCheckBox" Content="Potion" Tag="{x:Static local:ItemSortType.Potion}"/>
<CheckBox x:Name="ShowFurnitureCheckBox" Content="Furniture" Tag="{x:Static local:ItemSortType.Furniture}"/>
<CheckBox x:Name="ShowContainerCheckBox" Content="Containers" Tag="{x:Static local:ItemSortType.Container}"/>
<CheckBox x:Name="ShowSkillbookCheckBox" Content="Skill book" Tag="{x:Static local:ItemSortType.Skillbook}"/>
<CheckBox x:Name="ShowGrenadeCheckBox" Content="Grenade" Tag="{x:Static local:ItemSortType.Granade}"/>
<CheckBox x:Name="ShowFoodCheckBox" Content="Food" Tag="{x:Static local:ItemSortType.Food}"/>
Expand All @@ -79,41 +82,44 @@
<GroupBox Header="Stats" x:Name="StatsGroupBox">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto" MinWidth="88"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Amount:"/>
<TextBlock Text="Rarity:" Grid.Row="1"/>
<TextBlock Text="Lock level:" Grid.Row="2"/>
<TextBlock Text="Required level:" Grid.Row="3"/>
<TextBlock Text="HP:" Grid.Row="0" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Durability:" Grid.Row="1" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Durability counter:" Grid.Row="2" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Repair durability penalty:" Grid.Row="3" Grid.Column="2" Margin="30,0,0,0"/>
<TextBox x:Name="AmountTextBox" Grid.Column="1" Margin="20,5,0,5" Width="90"/>
<ComboBox x:Name="RarityComboBox" Grid.Column="1" Grid.Row="1" Width="90" Height="22" Margin="20,0,0,0"/>
<TextBox x:Name="LockLevelTextBox" Grid.Row="2" Grid.Column="1"/>
<TextBox x:Name="LevelTextBox" Grid.Row="3" Grid.Column="1"/>
<StackPanel Grid.Row="0" Grid.Column="3" Orientation="Horizontal">
<TextBlock x:Name="EquipmentSlotLabel" Text="Equip slot (0–14):" VerticalAlignment="Center" Visibility="Collapsed"/>
<TextBlock x:Name="EquipmentSlotText" Grid.Column="1" Margin="12,2,0,2" VerticalAlignment="Center" Visibility="Collapsed" FontWeight="SemiBold"/>
<TextBlock Text="Amount:" Grid.Row="1"/>
<TextBlock Text="Rarity:" Grid.Row="2"/>
<TextBlock Text="Lock level:" Grid.Row="3"/>
<TextBlock Text="Required level:" Grid.Row="4"/>
<TextBlock Text="HP:" Grid.Row="1" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Durability:" Grid.Row="2" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Durability counter:" Grid.Row="3" Grid.Column="2" Margin="30,0,0,0"/>
<TextBlock Text="Repair durability penalty:" Grid.Row="4" Grid.Column="2" Margin="30,0,0,0"/>
<TextBox x:Name="AmountTextBox" Grid.Row="1" Grid.Column="1" Margin="20,5,0,5" Width="90"/>
<ComboBox x:Name="RarityComboBox" SelectionChanged="RarityComboBox_OnSelectionChanged" Grid.Column="1" Grid.Row="2" Width="90" Height="22" Margin="20,0,0,0"/>
<TextBox x:Name="LockLevelTextBox" Grid.Row="3" Grid.Column="1"/>
<TextBox x:Name="LevelTextBox" Grid.Row="4" Grid.Column="1"/>
<StackPanel Grid.Row="1" Grid.Column="3" Orientation="Horizontal">
<TextBox x:Name="VitalityTextBox" Margin="20,5,5,5" Width="35"/>
<TextBlock Text="/"/>
<TextBox x:Name="MaxVitalityPatchCheckTextBox" Margin="5,5,0,5" Width="35"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="3" Orientation="Horizontal">
<StackPanel Grid.Row="2" Grid.Column="3" Orientation="Horizontal">
<TextBox x:Name="DurabilityTextBox" Margin="20,5,5,5" Width="35"/>
<TextBlock Text="/"/>
<TextBox x:Name="MaxDurabilityPatchCheckTextBox" Margin="5,5,0,5" Width="35"/>
</StackPanel>
<TextBox x:Name="DurabilityCounterTextBox" Grid.Row="2" Grid.Column="3"/>
<TextBox x:Name="RepairDurabilityPenaltyTextBox" Grid.Row="3" Grid.Column="3"/>
<TextBox x:Name="DurabilityCounterTextBox" Grid.Row="3" Grid.Column="3"/>
<TextBox x:Name="RepairDurabilityPenaltyTextBox" Grid.Row="4" Grid.Column="3"/>
</Grid>
</GroupBox>
<GroupBox Header="Modifiers" Width="{Binding Width, ElementName=StatsGroupBox, Mode=OneWay}">
Expand Down Expand Up @@ -143,6 +149,13 @@
</Grid>
</GroupBox>
</WrapPanel>
<Button Grid.Column="1" Grid.Row="3" HorizontalAlignment="Right" Content="Apply changes" Padding="10,2" VerticalAlignment="Bottom" Margin="10" Click="ApplyChangesButton_OnClick"/>
<Grid Grid.Column="1" Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="ApplyChangesButton" Grid.Column="0" Content="Apply changes" Padding="10,2" VerticalAlignment="Center" Margin="10,0,8,0" Click="ApplyChangesButton_OnClick" IsEnabled="False"/>
<TextBlock x:Name="InventoryApplyPendingLabel" Grid.Column="1" Text="Unapplied edits" Foreground="#CC6600" FontWeight="SemiBold" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,10,0" Visibility="Collapsed"/>
</Grid>
</Grid>
</UserControl>
Loading