diff --git a/CentrED/Tools/LargeScale/Operations/CopyMove.cs b/CentrED/Tools/LargeScale/Operations/CopyMove.cs index 2c10d276..6189c842 100644 --- a/CentrED/Tools/LargeScale/Operations/CopyMove.cs +++ b/CentrED/Tools/LargeScale/Operations/CopyMove.cs @@ -26,6 +26,13 @@ public class CopyMove : RemoteLargeScaleTool // New: current area for conversions private RectU16 _currentArea; private bool _hasArea = false; + + private bool useAlternateSource = false; + private string alternateMapPath = ""; + private string alternateStaIdxPath = ""; + private string alternateStaticsPath = ""; + private int alternateMapWidth = 7168; // Default to Map0 dimensions + private int alternateMapHeight = 4096; public void SetArea(RectU16 area) { @@ -36,33 +43,199 @@ public void SetArea(RectU16 area) public override bool DrawUI() { var changed = false; - - // Copy / Move - changed |= ImGui.RadioButton(LangManager.Get(COPY), ref copyMove_type, (int)LSO.CopyMove.Copy); - ImGui.SameLine(); - changed |= ImGui.RadioButton(LangManager.Get(MOVE), ref copyMove_type, (int)LSO.CopyMove.Move); + + // Copy/Move radio buttons - DISABLE Move when using alternate source + if (useAlternateSource) + { + // Force Copy mode when using alternate source + if (copyMove_type != (int)LSO.CopyMove.Copy) + { + copyMove_type = (int)LSO.CopyMove.Copy; + changed = true; + } + + // Draw Copy as enabled + changed |= ImGui.RadioButton(LangManager.Get(COPY), ref copyMove_type, (int)LSO.CopyMove.Copy); + + // Draw Move as disabled + ImGui.BeginDisabled(); + ImGui.SameLine(); + int disabledMove = (int)LSO.CopyMove.Move; + ImGui.RadioButton(LangManager.Get(MOVE), ref disabledMove, (int)LSO.CopyMove.Move); + ImGui.EndDisabled(); + + // Show tooltip on disabled Move button + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + ImGui.SetTooltip("Move is not supported when using a different source map.\nOnly Copy is available."); + } + } + else + { + // Normal mode - both Copy and Move enabled + changed |= ImGui.RadioButton(LangManager.Get(COPY), ref copyMove_type, (int)LSO.CopyMove.Copy); + ImGui.SameLine(); + changed |= ImGui.RadioButton(LangManager.Get(MOVE), ref copyMove_type, (int)LSO.CopyMove.Move); + } ImGui.Separator(); - - // Relative / Absolute toggle with auto-conversion - int prevMode = copyMove_coordMode; - changed |= ImGui.RadioButton(LangManager.Get(COORD_MODE_RELATIVE), ref copyMove_coordMode, 0); - ImGui.SameLine(); - changed |= ImGui.RadioButton(LangManager.Get(COORD_MODE_ABSOLUTE), ref copyMove_coordMode, 1); - - if (prevMode != copyMove_coordMode && _hasArea) + + // NEW: Checkbox for alternate source + changed |= ImGui.Checkbox("Use Different Source Map", ref useAlternateSource); + + if (useAlternateSource) + { + ImGui.Text("Source Map Configuration:"); + ImGui.Separator(); + + // File paths + ImGui.InputText("Map File##alt", ref alternateMapPath, 256); + ImGui.SameLine(); + if (ImGui.Button("Browse...##map")) + { + TinyFileDialogs.TryOpenFile + ("Select Map File", Environment.CurrentDirectory, ["*.mul"], null, false, out var result); + if (!string.IsNullOrEmpty(result)) + { + alternateMapPath = result; + // Auto-populate related files + var basePath = alternateMapPath.Replace(".mul", ""); + var mapNumber = basePath[basePath.Length - 1]; // Get map number + var directory = Path.GetDirectoryName(alternateMapPath); + var baseFileName = Path.GetFileNameWithoutExtension(alternateMapPath) + .Replace("Map", "").Replace("map", ""); + + alternateStaIdxPath = Path.Combine(directory, $"Staidx{mapNumber}.mul"); + alternateStaticsPath = Path.Combine(directory, $"Statics{mapNumber}.mul"); + + // Set default dimensions based on common UO map sizes + switch(mapNumber) + { + case '0': // OLD Felucca/Trammel + alternateMapWidth = 6144; + alternateMapHeight = 4096; + break; + case '1': // Felucca/Trammel + alternateMapWidth = 7168; + alternateMapHeight = 4096; + break; + case '2': // Ilshenar + alternateMapWidth = 2304; + alternateMapHeight = 1600; + break; + case '3': // Malas + alternateMapWidth = 2560; + alternateMapHeight = 2048; + break; + case '4': // Tokuno + alternateMapWidth = 1448; + alternateMapHeight = 1448; + break; + case '5': // TerMur + alternateMapWidth = 1280; + alternateMapHeight = 4096; + break; + case '6': // Custom + alternateMapWidth = 1280; + alternateMapHeight = 4096; + break; + default: + // Keep current values + break; + } + } + } + + ImGui.InputText("StaIdx File##alt", ref alternateStaIdxPath, 256); + ImGui.InputText("Statics File##alt", ref alternateStaticsPath, 256); + + ImGui.Separator(); + + ImGui.Text("Map Dimensions (in tiles):"); + changed |= ImGuiEx.DragInt("Width##alt", ref alternateMapWidth, 1, 64, 8192); + ImGui.SameLine(); + ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Width of the source map in tiles.\nMap0: 6144, Map1: 7168, Map2: 2304, etc..."); + } + + changed |= ImGuiEx.DragInt("Height##alt", ref alternateMapHeight, 1, 64, 8192); + ImGui.SameLine(); + ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Height of the source map in tiles.\nMap0: 4096, Map1: 4096, Map2: 1600, etc..."); + } + + ImGui.Separator(); + + // Validation warning + if (!string.IsNullOrEmpty(alternateMapPath) && !File.Exists(alternateMapPath)) + { + ImGui.TextColored(new System.Numerics.Vector4(1, 0, 0, 1), "Warning: Map file not found!"); + } + if (!string.IsNullOrEmpty(alternateStaIdxPath) && !File.Exists(alternateStaIdxPath)) + { + ImGui.TextColored(new System.Numerics.Vector4(1, 0.5f, 0, 1), "Warning: StaIdx file not found!"); + } + if (!string.IsNullOrEmpty(alternateStaticsPath) && !File.Exists(alternateStaticsPath)) + { + ImGui.TextColored(new System.Numerics.Vector4(1, 0.5f, 0, 1), "Warning: Statics file not found!"); + } + } + + ImGui.Separator(); + + // DESTINATION COORDINATES SECTION + // When using alternate source, force Absolute mode and disable Relative + if (useAlternateSource) { - if (copyMove_coordMode == 1) + // Force Absolute mode + if (copyMove_coordMode != 1) { - // Relative -> Absolute - copyMove_inputX = _currentArea.X1 + copyMove_inputX; - copyMove_inputY = _currentArea.Y1 + copyMove_inputY; + copyMove_coordMode = 1; + changed = true; } - else + + // Draw Relative as disabled + ImGui.BeginDisabled(); + int disabledRelative = 0; + ImGui.RadioButton(LangManager.Get(COORD_MODE_RELATIVE), ref disabledRelative, 0); + ImGui.EndDisabled(); + + // Show tooltip on disabled Relative button + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { - // Absolute -> Relative - copyMove_inputX = copyMove_inputX - _currentArea.X1; - copyMove_inputY = copyMove_inputY - _currentArea.Y1; + ImGui.SetTooltip("Relative coordinates are not supported when using a different source map.\nOnly Absolute coordinates are available."); + } + + ImGui.SameLine(); + // Draw Absolute as enabled + changed |= ImGui.RadioButton(LangManager.Get(COORD_MODE_ABSOLUTE), ref copyMove_coordMode, 1); + } + else + { + // Normal mode - both Relative and Absolute enabled + int prevMode = copyMove_coordMode; + changed |= ImGui.RadioButton(LangManager.Get(COORD_MODE_RELATIVE), ref copyMove_coordMode, 0); + ImGui.SameLine(); + changed |= ImGui.RadioButton(LangManager.Get(COORD_MODE_ABSOLUTE), ref copyMove_coordMode, 1); + + if (prevMode != copyMove_coordMode && _hasArea) + { + if (copyMove_coordMode == 1) + { + // Relative -> Absolute + copyMove_inputX = _currentArea.X1 + copyMove_inputX; + copyMove_inputY = _currentArea.Y1 + copyMove_inputY; + } + else + { + // Absolute -> Relative + copyMove_inputX = copyMove_inputX - _currentArea.X1; + copyMove_inputY = copyMove_inputY - _currentArea.Y1; + } } } @@ -104,7 +277,7 @@ public override bool DrawUI() public override bool CanSubmit(RectU16 area) { - // Ensure offsets computed from current inputs/mode/area + // Calculate offsets if (copyMove_coordMode == 1) { copyMove_offsetX = copyMove_inputX - area.X1; @@ -116,7 +289,40 @@ public override bool CanSubmit(RectU16 area) copyMove_offsetY = copyMove_inputY; } - // Existing bounds checks + // Validation for alternate source map dimensions + if (useAlternateSource) + { + // Check if source area exists within alternate map bounds + if (area.X1 >= alternateMapWidth || area.X2 >= alternateMapWidth) + { + _submitStatus = $"Source area exceeds alternate map width ({alternateMapWidth})"; + return false; + } + if (area.Y1 >= alternateMapHeight || area.Y2 >= alternateMapHeight) + { + _submitStatus = $"Source area exceeds alternate map height ({alternateMapHeight})"; + return false; + } + + // Check if files exist + if (!File.Exists(alternateMapPath)) + { + _submitStatus = "Alternate map file not found"; + return false; + } + if (!File.Exists(alternateStaIdxPath)) + { + _submitStatus = "Alternate StaIdx file not found"; + return false; + } + if (!File.Exists(alternateStaticsPath)) + { + _submitStatus = "Alternate Statics file not found"; + return false; + } + } + + // Existing destination validation if (copyMove_offsetX < 0 && copyMove_offsetX + area.X1 < 0) { _submitStatus = LangManager.Get(INVALID_OFFSET_X); @@ -142,7 +348,29 @@ public override bool CanSubmit(RectU16 area) protected override ILargeScaleOperation SubmitLSO() { - // Still submit offsets; absolute entries were converted above - return new LSOCopyMove((LSO.CopyMove)copyMove_type, copyMove_erase, copyMove_offsetX, copyMove_offsetY); + if (useAlternateSource) + { + + return new LSOCopyMove( + (LSO.CopyMove)copyMove_type, + copyMove_erase, + copyMove_offsetX, + copyMove_offsetY, + alternateMapPath, + alternateStaIdxPath, + alternateStaticsPath, + alternateMapWidth, + alternateMapHeight + ); + } + else + { + return new LSOCopyMove( + (LSO.CopyMove)copyMove_type, + copyMove_erase, + copyMove_offsetX, + copyMove_offsetY + ); + } } } \ No newline at end of file diff --git a/Client/Map/LargeScaleOperation.cs b/Client/Map/LargeScaleOperation.cs index e0cfcbe5..fe869775 100644 --- a/Client/Map/LargeScaleOperation.cs +++ b/Client/Map/LargeScaleOperation.cs @@ -1,4 +1,5 @@ -using static CentrED.Network.LSO; +using CentrED.Utility; +using static CentrED.Network.LSO; namespace CentrED.Client.Map; @@ -13,15 +14,53 @@ public class LSOCopyMove : ILargeScaleOperation private readonly int offsetX; private readonly int offsetY; private readonly bool erase; - + + // NEW: Alternate map source fields + private readonly bool useAlternateSource; + private readonly string alternateMapPath; + private readonly string alternateStaIdxPath; + private readonly string alternateStaticsPath; + private readonly int alternateMapWidth; + private readonly int alternateMapHeight; + + // Original constructor (backwards compatible) public LSOCopyMove(CopyMove type, bool erase, int offsetX, int offsetY) { this.type = type; this.erase = erase; this.offsetX = offsetX; this.offsetY = offsetY; + this.useAlternateSource = false; + this.alternateMapPath = string.Empty; + this.alternateStaIdxPath = string.Empty; + this.alternateStaticsPath = string.Empty; + this.alternateMapWidth = 0; + this.alternateMapHeight = 0; } + // NEW: Extended constructor with alternate map support + public LSOCopyMove( + CopyMove type, + bool erase, + int offsetX, + int offsetY, + string alternateMapPath, + string alternateStaIdxPath, + string alternateStaticsPath, + int alternateMapWidth, + int alternateMapHeight) + { + this.type = type; + this.erase = erase; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.useAlternateSource = true; + this.alternateMapPath = alternateMapPath ?? string.Empty; + this.alternateStaIdxPath = alternateStaIdxPath ?? string.Empty; + this.alternateStaticsPath = alternateStaticsPath ?? string.Empty; + this.alternateMapWidth = alternateMapWidth; + this.alternateMapHeight = alternateMapHeight; + } public void Write(BinaryWriter writer) { @@ -29,6 +68,18 @@ public void Write(BinaryWriter writer) writer.Write(offsetX); writer.Write(offsetY); writer.Write(erase); + + // NEW: Write alternate source flag and data + writer.Write(useAlternateSource); + + if (useAlternateSource) + { + writer.WriteStringNull(alternateMapPath); + writer.WriteStringNull(alternateStaIdxPath); + writer.WriteStringNull(alternateStaticsPath); + writer.Write((ushort)alternateMapWidth); + writer.Write((ushort)alternateMapHeight); + } } } diff --git a/Server/Map/LargeScaleOperations.cs b/Server/Map/LargeScaleOperations.cs index f2f491af..bd0187be 100644 --- a/Server/Map/LargeScaleOperations.cs +++ b/Server/Map/LargeScaleOperations.cs @@ -15,12 +15,41 @@ public class LsCopyMove : LargeScaleOperation public int OffsetY; public bool Erase; + // NEW: Alternate map source fields + public bool UseAlternateSource; + public string AlternateMapPath; + public string AlternateStaIdxPath; + public string AlternateStaticsPath; + public ushort AlternateMapWidth; + public ushort AlternateMapHeight; + public LsCopyMove(ref SpanReader reader) { - Type = (CopyMove)reader.ReadByte(); - OffsetX = reader.ReadInt32(); - OffsetY = reader.ReadInt32(); - Erase = reader.ReadBoolean(); + try + { + Type = (CopyMove)reader.ReadByte(); + OffsetX = reader.ReadInt32(); + OffsetY = reader.ReadInt32(); + Erase = reader.ReadBoolean(); + + // NEW: Read alternate source flag and data + UseAlternateSource = reader.ReadBoolean(); + + if (UseAlternateSource) + { + AlternateMapPath = reader.ReadString(); + AlternateStaIdxPath = reader.ReadString(); + AlternateStaticsPath = reader.ReadString(); + AlternateMapWidth = reader.ReadUInt16(); + AlternateMapHeight = reader.ReadUInt16(); + } + } + catch (Exception ex) + { + Console.WriteLine($"[LsCopyMove] ERROR reading from SpanReader: {ex.Message}"); + Console.WriteLine($"[LsCopyMove] Stack trace: {ex.StackTrace}"); + throw; + } } } diff --git a/Server/Map/ServerLandscape.cs b/Server/Map/ServerLandscape.cs index 14698c19..cbb4afda 100644 --- a/Server/Map/ServerLandscape.cs +++ b/Server/Map/ServerLandscape.cs @@ -99,6 +99,63 @@ Logger logger BlockCache.Resize(Math.Max(config.Map.Width, config.Map.Height) + 1); } + // NEW: Constructor for loading alternate map sources (read-only) + public ServerLandscape( + string mapPath, + string staIdxPath, + string staticsPath, + ushort width, + ushort height, + TileDataProvider tileDataProvider + ) : base((ushort)(width / 8), (ushort)(height / 8)) + { + _logger = null!; // Set to null for alternate sources + + var mapFile = new FileInfo(mapPath); + if (!mapFile.Exists) + { + Console.WriteLine($"[ServerLandscape] ERROR: Map file not found!"); + throw new Exception($"Alternate map file not found: {mapPath}"); + } + + _map = mapFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + _mapReader = new BinaryReader(_map, Encoding.UTF8); + _mapWriter = null!; + + IsUop = mapFile.Extension.Equals(".uop", StringComparison.OrdinalIgnoreCase); + if (IsUop) + { + Console.WriteLine($"[ServerLandscape] UOP format detected"); + string uopPattern = mapFile.Name.Replace(mapFile.Extension, "").ToLowerInvariant(); + ReadUopFiles(uopPattern); + } + + var staidxFile = new FileInfo(staIdxPath); + if (!staidxFile.Exists) + { + Console.WriteLine($"[ServerLandscape] ERROR: StaIdx file not found!"); + throw new Exception($"Alternate StaIdx file not found: {staIdxPath}"); + } + + _staidx = staidxFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + _staidxReader = new BinaryReader(_staidx, Encoding.UTF8); + _staidxWriter = null!; + + var staticsFile = new FileInfo(staticsPath); + if (!staticsFile.Exists) + { + Console.WriteLine($"[ServerLandscape] ERROR: Statics file not found!"); + throw new Exception($"Alternate Statics file not found: {staticsPath}"); + } + + _statics = staticsFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + _staticsReader = new BinaryReader(_statics, Encoding.UTF8); + _staticsWriter = null!; + + TileDataProvider = tileDataProvider; + _radarMap = null!; // No radar map for alternate sources + } + private void InitMap(FileInfo map) { using var mapFile = map.Open(FileMode.CreateNew, FileAccess.Write); @@ -591,15 +648,15 @@ private void Dispose(bool disposing) { if (disposing) { - _map.Dispose(); - _statics.Dispose(); - _staidx.Dispose(); - _mapReader.Dispose(); - _staticsReader.Dispose(); - _staidxReader.Dispose(); - _mapWriter.Dispose(); - _staticsWriter.Dispose(); - _staidxWriter.Dispose(); + _map?.Dispose(); + _statics?.Dispose(); + _staidx?.Dispose(); + _mapReader?.Dispose(); + _staticsReader?.Dispose(); + _staidxReader?.Dispose(); + _mapWriter?.Dispose(); + _staticsWriter?.Dispose(); + _staidxWriter?.Dispose(); } } diff --git a/Server/Map/ServerLandscapePacketHandlers.cs b/Server/Map/ServerLandscapePacketHandlers.cs index 9615c734..6d6c5085 100644 --- a/Server/Map/ServerLandscapePacketHandlers.cs +++ b/Server/Map/ServerLandscapePacketHandlers.cs @@ -271,6 +271,9 @@ private void OnLargeScaleCommandPacket(SpanReader reader, NetState ns ns.LogInfo(logMsg); ns.Parent.Send(new ServerStatePacket(ServerState.Other, logMsg)); ns.Parent.Flush(); + + bool radarUpdateStarted = false; // NEW: Track if radar update was started + try { var affectedBlocks = new bool[Width * Height]; @@ -335,14 +338,17 @@ private void OnLargeScaleCommandPacket(SpanReader reader, NetState ns if (reader.ReadBoolean()) operations.Add(new LsDeleteStatics(ref reader)); if (reader.ReadBoolean()) - operations.Add(new LsInsertStatics(ref reader)); + operations.Add(new LsInsertStatics(ref reader)); //We have read everything, now we can validate foreach (var operation in operations) { operation.Validate(this); } + // NEW: Only start radar update AFTER all reading/validation is successful _radarMap.BeginUpdate(); + radarUpdateStarted = true; // NEW: Mark that we started the update + foreach (ushort blockX in xBlockRange) { foreach (ushort blockY in yBlockRange) @@ -411,7 +417,11 @@ private void OnLargeScaleCommandPacket(SpanReader reader, NetState ns } finally { - _radarMap.EndUpdate(ns); + // NEW: Only end radar update if it was actually started + if (radarUpdateStarted) + { + _radarMap.EndUpdate(ns); + } ns.Parent.Send(new ServerStatePacket(ServerState.Running)); } ns.LogInfo("Large scale operation ended."); @@ -424,6 +434,7 @@ private void LsoApply(LargeScaleOperation lso, LandTile landTile, IEnumerable