Skip to content

Commit c72201f

Browse files
fix(editor): load spawn/house files on session restore
When restoring a session, the editor was only loading the OTBM map but not the external spawn/house XML files. This caused spawns to be empty in memory, and saving would overwrite the spawn file with an empty <spawns/> element, losing all configured spawns. Now LoadSessionFiles also reads the external spawn and house XML files after loading the OTBM, matching the behavior of LoadMapFromPath.
1 parent fc88616 commit c72201f

5 files changed

Lines changed: 119 additions & 16 deletions

File tree

src/App/AppSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class AppSettings
3737
/// <summary>Recent session snapshots (newest first). Capped at 20.</summary>
3838
public List<SessionHistoryEntry> History { get; set; } = [];
3939
public int ItemsPerPage { get; set; } = 100;
40+
public int PreferredDirection { get; set; }
4041

4142
private static string SettingsFilePath =>
4243
Path.Combine(

src/App/Controls/MapCanvasControl.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ public override void Render(DrawingContext context)
10231023
context.DrawLine(erasePen, new Point(gx + tilePixelSize - 4, gy + 4), new Point(gx + 4, gy + tilePixelSize - 4));
10241024
}
10251025
}
1026-
else if (ActiveZoneBrush > 0)
1026+
else if (ActiveZoneBrush != 0)
10271027
{
10281028
// Show zone color preview
10291029
Color zoneColor = ActiveZoneBrush switch
@@ -1032,6 +1032,7 @@ public override void Render(DrawingContext context)
10321032
4 => Color.FromArgb(80, 0, 200, 255), // NoPvP
10331033
2 => Color.FromArgb(80, 255, 255, 0), // NoLogout
10341034
8 => Color.FromArgb(80, 255, 0, 0), // PvPZone
1035+
< 0 => Color.FromArgb(60, 88, 91, 112), // Clear zone
10351036
_ => Color.FromArgb(60, 255, 255, 255),
10361037
};
10371038
var zoneBrush = new SolidColorBrush(zoneColor);
@@ -2021,6 +2022,8 @@ private void PaintZoneAt(MapPosition center, int zoneFlag, Dictionary<MapPositio
20212022

20222023
// All zone flags that we toggle
20232024
const uint allZoneFlags = 0x01 | 0x02 | 0x04 | 0x08;
2025+
// Negative values (e.g. -1) mean "clear all zones"
2026+
uint flagToApply = zoneFlag < 0 ? 0u : (uint)zoneFlag;
20242027
var tiles = GetBrushTiles(center);
20252028

20262029
foreach (var pos in tiles)
@@ -2030,13 +2033,13 @@ private void PaintZoneAt(MapPosition center, int zoneFlag, Dictionary<MapPositio
20302033

20312034
if (!_mapData.Tiles.TryGetValue(pos, out var tile))
20322035
{
2033-
if (zoneFlag == 0) continue; // clearing zone on non-existent tile is a no-op
2036+
if (flagToApply == 0) continue; // clearing zone on non-existent tile is a no-op
20342037
tile = new MapTile { Position = pos };
20352038
_mapData.Tiles[pos] = tile;
20362039
}
20372040

20382041
// Clear all zone flags, then set the requested one
2039-
tile.Flags = (uint)((tile.Flags & ~allZoneFlags) | (uint)zoneFlag);
2042+
tile.Flags = (tile.Flags & ~allZoneFlags) | flagToApply;
20402043
}
20412044
}
20422045

@@ -2335,6 +2338,8 @@ private void FillZoneArea(MapPosition min, MapPosition max, int zoneFlag)
23352338
if (_mapData == null) return;
23362339

23372340
const uint allZoneFlags = 0x01 | 0x02 | 0x04 | 0x08;
2341+
// Negative values (e.g. -1) mean "clear all zones"
2342+
uint flagToApply = zoneFlag < 0 ? 0u : (uint)zoneFlag;
23382343
var positions = new List<MapPosition>();
23392344
for (int y = min.Y; y <= max.Y; y++)
23402345
for (int x = min.X; x <= max.X; x++)
@@ -2346,11 +2351,11 @@ private void FillZoneArea(MapPosition min, MapPosition max, int zoneFlag)
23462351
{
23472352
if (!_mapData.Tiles.TryGetValue(pos, out var tile))
23482353
{
2349-
if (zoneFlag == 0) continue;
2354+
if (flagToApply == 0) continue;
23502355
tile = new MapTile { Position = pos };
23512356
_mapData.Tiles[pos] = tile;
23522357
}
2353-
tile.Flags = (uint)((tile.Flags & ~allZoneFlags) | (uint)zoneFlag);
2358+
tile.Flags = (tile.Flags & ~allZoneFlags) | flagToApply;
23542359
}
23552360

23562361
int count = positions.Count;
@@ -2934,7 +2939,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
29342939
e.Handled = true;
29352940
}
29362941
// 4) Zone brush (paint tile flags)
2937-
else if (ActiveZoneBrush >= 0 && ActiveZoneBrush != 0)
2942+
else if (ActiveZoneBrush != 0)
29382943
{
29392944
_paintUndoSnapshot = new Dictionary<MapPosition, MapTile?>();
29402945
PaintZoneAt(tilePos, ActiveZoneBrush, _paintUndoSnapshot);
@@ -3203,7 +3208,7 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e)
32033208
FillRemoveBordersArea(minPos, maxPos);
32043209
e.Handled = true;
32053210
}
3206-
else if (ActiveZoneBrush > 0)
3211+
else if (ActiveZoneBrush != 0)
32073212
{
32083213
FillZoneArea(minPos, maxPos, ActiveZoneBrush);
32093214
e.Handled = true;

src/App/MainWindow.axaml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,17 @@
533533
Margin="2,0,0,0">
534534
<i:Icon Value="fa-solid fa-table-cells" FontSize="11"/>
535535
</ToggleButton>
536+
<Border DockPanel.Dock="Right" Background="#181825" CornerRadius="4" Padding="2,0" Margin="4,0,0,0"
537+
ToolTip.Tip="{Binding SpriteZoom, StringFormat='Zoom {0}x'}">
538+
<StackPanel Orientation="Horizontal" Spacing="3" VerticalAlignment="Center">
539+
<i:Icon Value="fa-solid fa-magnifying-glass" FontSize="10" Foreground="#585b70"/>
540+
<Slider Minimum="1" Maximum="6" Value="{Binding SpriteZoom}"
541+
IsSnapToTickEnabled="True" TickFrequency="1"
542+
Width="60" MinHeight="20" Padding="0"/>
543+
<TextBlock Text="{Binding SpriteZoom, StringFormat='{}{0}x'}" Foreground="#cba6f7"
544+
FontSize="10" FontWeight="Bold" VerticalAlignment="Center" MinWidth="18"/>
545+
</StackPanel>
546+
</Border>
536547
<ComboBox DockPanel.Dock="Right" ItemsSource="{Binding ClientCategoryOptions}"
537548
SelectedItem="{Binding ClientCategoryFilter}"
538549
FontSize="11" MinHeight="28" Padding="6,3" MinWidth="70" Margin="4,0,0,0"/>
@@ -643,11 +654,17 @@
643654
</ListBox.Styles>
644655
<ListBox.ItemTemplate>
645656
<DataTemplate x:DataType="vm:ClientItemViewModel">
646-
<Grid ColumnDefinitions="38,*" Height="38" Margin="2,1">
647-
<Border Classes="clientSprBox" Background="#11111b" CornerRadius="4" Width="32" Height="32"
657+
<Grid ColumnDefinitions="Auto,*"
658+
Height="{Binding #ClientItemListBox.((vm:MainWindowViewModel)DataContext).SpriteRowHeight}"
659+
Margin="2,1">
660+
<Border Classes="clientSprBox" Background="#11111b" CornerRadius="4"
661+
Width="{Binding #ClientItemListBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
662+
Height="{Binding #ClientItemListBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
648663
BorderThickness="3"
649664
VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
650-
<Image Source="{Binding Sprite}" Width="32" Height="32"
665+
<Image Source="{Binding Sprite}"
666+
Width="{Binding #ClientItemListBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
667+
Height="{Binding #ClientItemListBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
651668
RenderOptions.BitmapInterpolationMode="None" Stretch="Uniform"/>
652669
</Border>
653670
<TextBlock Grid.Column="1" Text="{Binding Id}" Foreground="#cba6f7"
@@ -674,9 +691,13 @@
674691
</ListBox.ItemsPanel>
675692
<ListBox.ItemTemplate>
676693
<DataTemplate x:DataType="vm:ClientItemViewModel">
677-
<Border Width="36" Height="36" Background="#11111b" CornerRadius="3" Margin="1"
694+
<Border Width="{Binding #ClientItemIconBox.((vm:MainWindowViewModel)DataContext).SpriteCellSize}"
695+
Height="{Binding #ClientItemIconBox.((vm:MainWindowViewModel)DataContext).SpriteCellSize}"
696+
Background="#11111b" CornerRadius="3" Margin="1"
678697
ToolTip.Tip="{Binding Id}" Classes="clientSprBox">
679-
<Image Source="{Binding Sprite}" Width="32" Height="32"
698+
<Image Source="{Binding Sprite}"
699+
Width="{Binding #ClientItemIconBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
700+
Height="{Binding #ClientItemIconBox.((vm:MainWindowViewModel)DataContext).SpriteImageSize}"
680701
RenderOptions.BitmapInterpolationMode="None" Stretch="Uniform"
681702
HorizontalAlignment="Center" VerticalAlignment="Center"/>
682703
</Border>
@@ -1960,7 +1981,7 @@
19601981
</StackPanel>
19611982
</Button>
19621983
<Button Classes="toolbar" Padding="5,2" ToolTip.Tip="Clear Zone"
1963-
Tag="0" Click="OnZoneBrushClick">
1984+
Tag="-1" Click="OnZoneBrushClick">
19641985
<i:Icon Value="fa-solid fa-xmark" FontSize="9" Foreground="#585b70"/>
19651986
</Button>
19661987
</StackPanel>

src/App/ViewModels/MainWindowViewModel.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ private void LoadFromSession(SessionViewModel session)
302302
OnPropertyChanged(nameof(ClientCategoryFilter));
303303
#pragma warning restore MVVMTK0034
304304

305+
// Clear selections before clearing collections to avoid Avalonia SelectionModel
306+
// referencing stale indices during CollectionChanged
307+
SelectedItem = null;
308+
SelectedClientItem = null;
309+
305310
// Clear visible collections — they will be repopulated by the filters below
306311
Items.Clear();
307312
ClientItems.Clear();
@@ -3944,6 +3949,25 @@ public int ClientItemsPerPage
39443949
[ObservableProperty] private bool _isPlayingAnimation;
39453950
[ObservableProperty] private bool _isClientIconView;
39463951

3952+
private int _spriteZoom = 1;
3953+
public int SpriteZoom
3954+
{
3955+
get => _spriteZoom;
3956+
set
3957+
{
3958+
value = Math.Clamp(value, 1, 6);
3959+
if (SetProperty(ref _spriteZoom, value))
3960+
{
3961+
OnPropertyChanged(nameof(SpriteImageSize));
3962+
OnPropertyChanged(nameof(SpriteCellSize));
3963+
OnPropertyChanged(nameof(SpriteRowHeight));
3964+
}
3965+
}
3966+
}
3967+
public int SpriteImageSize => 32 * _spriteZoom;
3968+
public int SpriteCellSize => 32 * _spriteZoom + 4;
3969+
public int SpriteRowHeight => 32 * _spriteZoom + 6;
3970+
39473971
public ObservableCollection<SpriteViewModel> FilmstripFrames { get; } = [];
39483972
public bool HasAnimation => (CurrentFrameGroup?.Frames ?? 1) > 1;
39493973
public bool HasMultipleGroups => (_currentCompositionThing?.FrameGroups.Length ?? 1) > 1;
@@ -4430,7 +4454,7 @@ public void OpenOtbItemEditor()
44304454

44314455
partial void OnCompositionFrameChanged(int value) { OnPropertyChanged(nameof(CompositionFrameLabel)); ReloadCompositionGridOnly(); }
44324456
partial void OnCompositionLayerChanged(int value) { OnPropertyChanged(nameof(CompositionLayerLabel)); ReloadComposition(); }
4433-
partial void OnCompositionPatternXChanged(int value) { OnPropertyChanged(nameof(CompositionPatternXLabel)); OnPropertyChanged(nameof(DirectionLabel)); ReloadComposition(); }
4457+
partial void OnCompositionPatternXChanged(int value) { OnPropertyChanged(nameof(CompositionPatternXLabel)); OnPropertyChanged(nameof(DirectionLabel)); _appSettings.PreferredDirection = value; _appSettings.Save(); ReloadComposition(); }
44344458
partial void OnCompositionPatternYChanged(int value) { OnPropertyChanged(nameof(CompositionPatternYLabel)); ReloadComposition(); }
44354459
partial void OnCompositionPatternZChanged(int value) => ReloadComposition();
44364460
partial void OnCompositionFrameGroupIndexChanged(int value) { NotifyAllCompositionLabels(); ReloadComposition(); }
@@ -4868,6 +4892,25 @@ private async Task LoadSessionFiles(SessionViewModel session)
48684892
if (!string.IsNullOrEmpty(session.MapFilePath) && File.Exists(session.MapFilePath))
48694893
{
48704894
session.MapData = OtbmFile.Load(session.MapFilePath);
4895+
4896+
// Load external spawn/house files so they are not lost on save
4897+
var mapDir = Path.GetDirectoryName(session.MapFilePath) ?? ".";
4898+
4899+
if (!string.IsNullOrEmpty(session.MapData.SpawnFile))
4900+
{
4901+
var spawnPath = Path.Combine(mapDir, session.MapData.SpawnFile);
4902+
var spawns = SpawnHouseXml.LoadSpawns(spawnPath);
4903+
session.MapData.Spawns.Clear();
4904+
session.MapData.Spawns.AddRange(spawns);
4905+
}
4906+
4907+
if (!string.IsNullOrEmpty(session.MapData.HouseFile))
4908+
{
4909+
var housePath = Path.Combine(mapDir, session.MapData.HouseFile);
4910+
var houses = SpawnHouseXml.LoadHouses(housePath);
4911+
session.MapData.Houses.Clear();
4912+
session.MapData.Houses.AddRange(houses);
4913+
}
48714914
}
48724915

48734916
session.UpdateName();
@@ -5901,10 +5944,11 @@ private void LoadComposition(DatThingType thing)
59015944
CompositionFrameGroupIndex = 0;
59025945
CompositionFrame = 0;
59035946
CompositionLayer = 0;
5904-
CompositionPatternX = 0;
5947+
CompositionPatternX = _appSettings.PreferredDirection;
59055948
CompositionPatternY = 0;
59065949
CompositionPatternZ = 0;
59075950
NotifyAllCompositionLabels();
5951+
ClampNavigationIndices();
59085952
BuildCompositionGrid();
59095953
BuildFilmstrip();
59105954

@@ -6053,9 +6097,10 @@ private void ResetThing()
60536097
CompositionFrameGroupIndex = 0;
60546098
CompositionFrame = 0;
60556099
CompositionLayer = 0;
6056-
CompositionPatternX = 0;
6100+
CompositionPatternX = _appSettings.PreferredDirection;
60576101
CompositionPatternY = 0;
60586102
CompositionPatternZ = 0;
6103+
ClampNavigationIndices();
60596104
NotifyAllCompositionLabels();
60606105
OnPropertyChanged(nameof(IsItemSelected));
60616106
OnPropertyChanged(nameof(IsOutfitSelected));

src/App/crash.log

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,34 @@
117117
at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[] args)
118118
at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, Action`1 lifetimeBuilder)
119119
at AssetsAndMapEditor.App.Program.Main(String[] args) in /Users/brewertonsantos/dev/pokeorigins-tibia/assets-and-map-editor/src/App/Program.cs:line 25
120+
[2026-04-08 10:39:14] UNHANDLED: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
121+
at System.Collections.ObjectModel.Collection`1.System.Collections.IList.get_Item(Int32 index)
122+
at Avalonia.Controls.Selection.SelectedItems`1.GetEnumerator()+MoveNext()
123+
at System.Linq.Enumerable.<ToArray>g__EnumerableToArray|324_0[TSource](IEnumerable`1 source)
124+
at Avalonia.Controls.Primitives.SelectingItemsControl.OnSelectionModelSelectionChanged(Object sender, SelectionModelSelectionChangedEventArgs e)
125+
at Avalonia.Controls.Selection.SelectionModel`1.CommitOperation(Operation operation, Boolean raisePropertyChanged)
126+
at Avalonia.Controls.Utils.CollectionChangedEventManager.Entry.<Avalonia.Utilities.IWeakEventSubscriber<System.Collections.Specialized.NotifyCollectionChangedEventArgs>.OnEvent>g__Notify|6_0(INotifyCollectionChanged incc, NotifyCollectionChangedEventArgs args, WeakReference`1[] listeners)
127+
at Avalonia.Utilities.WeakEvent`2.Subscription.OnEvent(Object sender, TEventArgs eventArgs)
128+
at System.Collections.ObjectModel.Collection`1.Clear()
129+
at AssetsAndMapEditor.App.ViewModels.MainWindowViewModel.LoadFromSession(SessionViewModel session) in /Users/brewertonsantos/dev/pokeorigins-tibia/assets-and-map-editor/src/App/ViewModels/MainWindowViewModel.cs:line 307
130+
at AssetsAndMapEditor.App.ViewModels.MainWindowViewModel.SwitchToSession(SessionViewModel session) in /Users/brewertonsantos/dev/pokeorigins-tibia/assets-and-map-editor/src/App/ViewModels/MainWindowViewModel.cs:line 201
131+
at AssetsAndMapEditor.App.MainWindow.OnSessionTabPressed(Object sender, PointerPressedEventArgs e) in /Users/brewertonsantos/dev/pokeorigins-tibia/assets-and-map-editor/src/App/MainWindow.axaml.cs:line 242
132+
at Avalonia.Interactivity.EventRoute.RaiseEventImpl(RoutedEventArgs e)
133+
at Avalonia.Interactivity.EventRoute.RaiseEvent(Interactive source, RoutedEventArgs e)
134+
at Avalonia.Interactivity.Interactive.RaiseEvent(RoutedEventArgs e)
135+
at Avalonia.Input.MouseDevice.MouseDown(IMouseDevice device, UInt64 timestamp, IInputElement root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers, IInputElement hitTest)
136+
at Avalonia.Input.MouseDevice.ProcessRawEvent(RawPointerEventArgs e)
137+
at Avalonia.Controls.TopLevel.<>c.<HandleInput>b__150_0(Object state)
138+
at Avalonia.Threading.Dispatcher.Send(SendOrPostCallback action, Object arg, Nullable`1 priority)
139+
at Avalonia.Native.TopLevelImpl.RawMouseEvent(AvnRawMouseEventType type, AvnPointerDeviceType deviceType, UInt64 timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta, Single pressure, Single xTilt, Single yTilt)
140+
at Avalonia.Native.Interop.Impl.__MicroComIAvnTopLevelEventsVTable.RawMouseEvent(Void* this, AvnRawMouseEventType type, AvnPointerDeviceType deviceType, UInt64 timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta, Single pressure, Single xTilt, Single yTilt)
141+
--- End of stack trace from previous location ---
142+
at Avalonia.Native.DispatcherImpl.RunLoop(CancellationToken token)
143+
at Avalonia.Native.DispatcherImpl.RunLoop(CancellationToken token)
144+
at Avalonia.Threading.DispatcherFrame.Run(IControlledDispatcherImpl impl)
145+
at Avalonia.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
146+
at Avalonia.Threading.Dispatcher.MainLoop(CancellationToken cancellationToken)
147+
at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.StartCore(String[] args)
148+
at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[] args)
149+
at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, Action`1 lifetimeBuilder)
150+
at AssetsAndMapEditor.App.Program.Main(String[] args) in /Users/brewertonsantos/dev/pokeorigins-tibia/assets-and-map-editor/src/App/Program.cs:line 25

0 commit comments

Comments
 (0)