@@ -2795,6 +2795,294 @@ public void AddMapLog(string message)
27952795 MapActionLog . RemoveAt ( MapActionLog . Count - 1 ) ;
27962796 }
27972797
2798+ // ── Map Properties & Operations ──
2799+
2800+ [ RelayCommand ]
2801+ private async Task OpenMapPropertiesAsync ( )
2802+ {
2803+ if ( MapData == null ) return ;
2804+
2805+ var dialog = new MapPropertiesDialog
2806+ {
2807+ MapDescription = MapData . Description ,
2808+ MapWidth = MapData . Width ,
2809+ MapHeight = MapData . Height ,
2810+ MapHouseFile = MapData . HouseFile ,
2811+ MapSpawnFile = MapData . SpawnFile ,
2812+ OtbmVersion = MapData . Version . ToString ( ) ,
2813+ OtbVersion = $ "{ MapData . OtbMajorVersion } .{ MapData . OtbMinorVersion } ",
2814+ TileCount = MapData . Tiles . Count ,
2815+ } ;
2816+
2817+ if ( Avalonia . Application . Current ? . ApplicationLifetime
2818+ is Avalonia . Controls . ApplicationLifetimes . IClassicDesktopStyleApplicationLifetime desktop
2819+ && desktop . MainWindow is not null )
2820+ {
2821+ await dialog . ShowDialog ( desktop . MainWindow ) ;
2822+ }
2823+ else return ;
2824+
2825+ if ( dialog . Result == null ) return ;
2826+
2827+ var r = dialog . Result ;
2828+ bool changed = false ;
2829+
2830+ if ( MapData . Description != r . Description )
2831+ { MapData . Description = r . Description ; changed = true ; }
2832+ if ( MapData . HouseFile != r . HouseFile )
2833+ { MapData . HouseFile = r . HouseFile ; changed = true ; }
2834+ if ( MapData . SpawnFile != r . SpawnFile )
2835+ { MapData . SpawnFile = r . SpawnFile ; changed = true ; }
2836+
2837+ bool resized = MapData . Width != r . Width || MapData . Height != r . Height ;
2838+ if ( resized )
2839+ {
2840+ int removed = 0 ;
2841+ var toRemove = new List < MapPosition > ( ) ;
2842+ foreach ( var pos in MapData . Tiles . Keys )
2843+ {
2844+ if ( pos . X >= r . Width || pos . Y >= r . Height )
2845+ toRemove . Add ( pos ) ;
2846+ }
2847+ foreach ( var pos in toRemove )
2848+ {
2849+ MapData . Tiles . Remove ( pos ) ;
2850+ removed ++ ;
2851+ }
2852+
2853+ MapData . Width = r . Width ;
2854+ MapData . Height = r . Height ;
2855+ changed = true ;
2856+
2857+ MapTileCount = MapData . Tiles . Count ;
2858+ AddMapLog ( $ "Map resized to { r . Width } ×{ r . Height } — { removed } tile(s) removed") ;
2859+ }
2860+
2861+ if ( changed )
2862+ {
2863+ MarkMapDirty ( ) ;
2864+ MapStatusText = $ "Map properties updated — { MapData . Tiles . Count : N0} tiles";
2865+ if ( ! resized ) AddMapLog ( "Map properties updated" ) ;
2866+ }
2867+ }
2868+
2869+ [ RelayCommand ]
2870+ private async Task ShowMapStatisticsAsync ( )
2871+ {
2872+ if ( MapData == null ) return ;
2873+
2874+ var dialog = new MapStatisticsDialog ( ) ;
2875+ dialog . Populate ( MapData ) ;
2876+
2877+ if ( Avalonia . Application . Current ? . ApplicationLifetime
2878+ is Avalonia . Controls . ApplicationLifetimes . IClassicDesktopStyleApplicationLifetime desktop
2879+ && desktop . MainWindow is not null )
2880+ {
2881+ await dialog . ShowDialog ( desktop . MainWindow ) ;
2882+ }
2883+ else
2884+ {
2885+ dialog . Show ( ) ;
2886+ }
2887+ }
2888+
2889+ [ RelayCommand ]
2890+ private void MapCleanupInvalidItems ( )
2891+ {
2892+ if ( MapData == null || _otbData == null ) return ;
2893+
2894+ var validIds = new HashSet < ushort > ( _otbData . Items . Select ( i => i . ServerId ) ) ;
2895+ int removedItems = 0 ;
2896+ var emptyTiles = new List < MapPosition > ( ) ;
2897+
2898+ foreach ( var ( pos , tile ) in MapData . Tiles )
2899+ {
2900+ int before = tile . Items . Count ;
2901+ tile . Items . RemoveAll ( item => item . Id > 0 && ! validIds . Contains ( item . Id ) ) ;
2902+ removedItems += before - tile . Items . Count ;
2903+
2904+ if ( tile . Items . Count == 0 )
2905+ emptyTiles . Add ( pos ) ;
2906+ }
2907+
2908+ foreach ( var pos in emptyTiles )
2909+ MapData . Tiles . Remove ( pos ) ;
2910+
2911+ if ( removedItems > 0 || emptyTiles . Count > 0 )
2912+ {
2913+ MarkMapDirty ( ) ;
2914+ MapTileCount = MapData . Tiles . Count ;
2915+ var msg = $ "Cleanup: removed { removedItems } invalid item(s), { emptyTiles . Count } empty tile(s)";
2916+ MapStatusText = msg ;
2917+ AddMapLog ( msg ) ;
2918+ }
2919+ else
2920+ {
2921+ AddMapLog ( "Cleanup: no invalid items found" ) ;
2922+ MapStatusText = "No invalid items found" ;
2923+ }
2924+ }
2925+
2926+ [ RelayCommand ]
2927+ private async Task MapRemoveItemsByIdAsync ( )
2928+ {
2929+ if ( MapData == null ) return ;
2930+
2931+ // Build a simple input dialog inline
2932+ var tcs = new TaskCompletionSource < ushort ? > ( ) ;
2933+ var input = new Avalonia . Controls . TextBox
2934+ {
2935+ Watermark = "Item Server ID (e.g. 2148)" ,
2936+ Background = Avalonia . Media . Brush . Parse ( "#313244" ) ,
2937+ Foreground = Avalonia . Media . Brush . Parse ( "#cdd6f4" ) ,
2938+ BorderBrush = Avalonia . Media . Brush . Parse ( "#45475a" ) ,
2939+ CornerRadius = new Avalonia . CornerRadius ( 4 ) ,
2940+ Padding = new Avalonia . Thickness ( 6 , 4 ) ,
2941+ FontSize = 12 ,
2942+ } ;
2943+ var okBtn = new Avalonia . Controls . Button
2944+ {
2945+ Content = "Remove" , Background = Avalonia . Media . Brush . Parse ( "#f38ba8" ) ,
2946+ Foreground = Avalonia . Media . Brush . Parse ( "#1e1e2e" ) , FontWeight = FontWeight . SemiBold ,
2947+ CornerRadius = new Avalonia . CornerRadius ( 4 ) , Padding = new Avalonia . Thickness ( 14 , 6 ) ,
2948+ } ;
2949+ var cancelBtn = new Avalonia . Controls . Button
2950+ {
2951+ Content = "Cancel" , Background = Avalonia . Media . Brush . Parse ( "#313244" ) ,
2952+ Foreground = Avalonia . Media . Brush . Parse ( "#cdd6f4" ) ,
2953+ CornerRadius = new Avalonia . CornerRadius ( 4 ) , Padding = new Avalonia . Thickness ( 14 , 6 ) ,
2954+ } ;
2955+
2956+ var btnPanel = new Avalonia . Controls . StackPanel
2957+ {
2958+ Orientation = Avalonia . Layout . Orientation . Horizontal , Spacing = 8 ,
2959+ HorizontalAlignment = Avalonia . Layout . HorizontalAlignment . Right ,
2960+ Margin = new Avalonia . Thickness ( 0 , 10 , 0 , 0 ) ,
2961+ } ;
2962+ btnPanel . Children . Add ( cancelBtn ) ;
2963+ btnPanel . Children . Add ( okBtn ) ;
2964+
2965+ var panel = new Avalonia . Controls . StackPanel { Spacing = 8 , Margin = new Avalonia . Thickness ( 20 ) } ;
2966+ panel . Children . Add ( new Avalonia . Controls . TextBlock
2967+ {
2968+ Text = "Enter the Server ID of the item to remove from the entire map:" ,
2969+ Foreground = Avalonia . Media . Brush . Parse ( "#cdd6f4" ) , FontSize = 12 ,
2970+ } ) ;
2971+ panel . Children . Add ( input ) ;
2972+ panel . Children . Add ( btnPanel ) ;
2973+
2974+ var dlg = new Avalonia . Controls . Window
2975+ {
2976+ Title = "Remove Items by ID" ,
2977+ Width = 380 , SizeToContent = Avalonia . Controls . SizeToContent . Height ,
2978+ CanResize = false , WindowStartupLocation = Avalonia . Controls . WindowStartupLocation . CenterOwner ,
2979+ Background = Avalonia . Media . Brush . Parse ( "#1e1e2e" ) ,
2980+ Content = panel ,
2981+ } ;
2982+
2983+ okBtn . Click += ( _ , _ ) =>
2984+ {
2985+ if ( ushort . TryParse ( input . Text ? . Trim ( ) , out var id ) && id > 0 )
2986+ { tcs . TrySetResult ( id ) ; dlg . Close ( ) ; }
2987+ } ;
2988+ cancelBtn . Click += ( _ , _ ) => { tcs . TrySetResult ( null ) ; dlg . Close ( ) ; } ;
2989+ dlg . Closing += ( _ , _ ) => tcs . TrySetResult ( null ) ;
2990+
2991+ if ( Avalonia . Application . Current ? . ApplicationLifetime
2992+ is Avalonia . Controls . ApplicationLifetimes . IClassicDesktopStyleApplicationLifetime desktop
2993+ && desktop . MainWindow is not null )
2994+ {
2995+ await dlg . ShowDialog ( desktop . MainWindow ) ;
2996+ }
2997+ else return ;
2998+
2999+ var targetId = await tcs . Task ;
3000+ if ( targetId == null ) return ;
3001+
3002+ int removed = RemoveItemIdFromMap ( MapData , targetId . Value ) ;
3003+ if ( removed > 0 )
3004+ {
3005+ MarkMapDirty ( ) ;
3006+ MapTileCount = MapData . Tiles . Count ;
3007+ var msg = $ "Removed { removed } instance(s) of item #{ targetId . Value } ";
3008+ MapStatusText = msg ;
3009+ AddMapLog ( msg ) ;
3010+ }
3011+ else
3012+ {
3013+ AddMapLog ( $ "Item #{ targetId . Value } not found on map") ;
3014+ MapStatusText = $ "Item #{ targetId . Value } not found";
3015+ }
3016+ }
3017+
3018+ private static int RemoveItemIdFromMap ( MapData map , ushort id )
3019+ {
3020+ int removed = 0 ;
3021+ var emptyTiles = new List < MapPosition > ( ) ;
3022+
3023+ foreach ( var ( pos , tile ) in map . Tiles )
3024+ {
3025+ int before = tile . Items . Count ;
3026+ tile . Items . RemoveAll ( i => i . Id == id ) ;
3027+ removed += before - tile . Items . Count ;
3028+
3029+ // Also remove from container contents
3030+ foreach ( var item in tile . Items )
3031+ removed += RemoveItemIdFromContents ( item . Contents , id ) ;
3032+
3033+ if ( tile . Items . Count == 0 )
3034+ emptyTiles . Add ( pos ) ;
3035+ }
3036+
3037+ foreach ( var pos in emptyTiles )
3038+ map . Tiles . Remove ( pos ) ;
3039+
3040+ return removed ;
3041+ }
3042+
3043+ private static int RemoveItemIdFromContents ( List < MapItem > contents , ushort id )
3044+ {
3045+ int removed = 0 ;
3046+ int before = contents . Count ;
3047+ contents . RemoveAll ( i => i . Id == id ) ;
3048+ removed += before - contents . Count ;
3049+
3050+ foreach ( var item in contents )
3051+ removed += RemoveItemIdFromContents ( item . Contents , id ) ;
3052+
3053+ return removed ;
3054+ }
3055+
3056+ [ RelayCommand ]
3057+ private void MapRemoveEmptyTiles ( )
3058+ {
3059+ if ( MapData == null ) return ;
3060+
3061+ var toRemove = new List < MapPosition > ( ) ;
3062+ foreach ( var ( pos , tile ) in MapData . Tiles )
3063+ {
3064+ if ( tile . Items . Count == 0 )
3065+ toRemove . Add ( pos ) ;
3066+ }
3067+
3068+ foreach ( var pos in toRemove )
3069+ MapData . Tiles . Remove ( pos ) ;
3070+
3071+ if ( toRemove . Count > 0 )
3072+ {
3073+ MarkMapDirty ( ) ;
3074+ MapTileCount = MapData . Tiles . Count ;
3075+ var msg = $ "Removed { toRemove . Count } empty tile(s)";
3076+ MapStatusText = msg ;
3077+ AddMapLog ( msg ) ;
3078+ }
3079+ else
3080+ {
3081+ AddMapLog ( "No empty tiles found" ) ;
3082+ MapStatusText = "No empty tiles found" ;
3083+ }
3084+ }
3085+
27983086 /// <summary>Called by MapCanvasControl.SelectedTileChanged event.</summary>
27993087 public void OnSelectedTileChanged ( MapPosition ? pos )
28003088 {
0 commit comments