diff --git a/GameRealisticMap.Arma3/Edit/Imagery/ExistingImageryInfos.cs b/GameRealisticMap.Arma3/Edit/Imagery/ExistingImageryInfos.cs index a0e856c8..a1af0739 100644 --- a/GameRealisticMap.Arma3/Edit/Imagery/ExistingImageryInfos.cs +++ b/GameRealisticMap.Arma3/Edit/Imagery/ExistingImageryInfos.cs @@ -9,7 +9,7 @@ namespace GameRealisticMap.Arma3.Edit.Imagery { - public class ExistingImageryInfos : IArma3MapConfig + public class ExistingImageryInfos : IArma3MapConfig, IImageryInfos { public ExistingImageryInfos(int tileSize, double resolution, float sizeInMeters, string proPrefix, int idMapMultiplier = 1) { diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericIdMapReadStorage.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericIdMapReadStorage.cs new file mode 100644 index 00000000..c9e4f7ee --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericIdMapReadStorage.cs @@ -0,0 +1,70 @@ +using BIS.PAA; +using GameRealisticMap.Arma3.Assets; +using GameRealisticMap.Arma3.IO; +using Pmad.HugeImages.Storage; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + internal sealed class GenericIdMapReadStorage : IHugeImageStorageSlot + { + private readonly GenericImageryInfos partitioner; + private readonly IGameFileSystem fileSystem; + private readonly Dictionary materials; + private readonly int tileSize; + + public GenericIdMapReadStorage(GenericImageryInfos partitioner, IGameFileSystem fileSystem, TerrainMaterialLibrary library) + { + this.partitioner = partitioner; + this.fileSystem = fileSystem; + materials = library.Definitions.Select(d => d.Material).ToDictionary(m => m.GetColorTexturePath(partitioner), m => m, StringComparer.OrdinalIgnoreCase); + tileSize = partitioner.TileSize * partitioner.IdMapMultiplier; + } + + public void Dispose() + { + + } + + public Task?> LoadImagePart(int partId) + where TPixel : unmanaged, IPixel + { + var part = partitioner.OtherTileInfos[partId-1]; + + var textures = part.Textures.Select(tex => materials[tex]).Select(t => t.Id).ToList(); + + using var paaStream = fileSystem.OpenFileIfExists(part.Mask); + if (paaStream == null) + { + throw new FileNotFoundException($"File '{part.Mask}' was not found."); + } + var paa = new PAA(paaStream); + var map = paa.Mipmaps.OrderBy(m => m.Width).Last(); + var pixels = PAA.GetARGB32PixelData(paa, paaStream, map); + var maskImage = Image.LoadPixelData(pixels, map.Width, map.Height).CloneAs(); + if (maskImage.Width != tileSize || maskImage.Height != tileSize) + { + maskImage.Mutate(img => img.Resize(tileSize, tileSize)); + } + var finalImage = new Image(maskImage.Width, maskImage.Height); + var px = new TPixel(); + for (int x = 0; x < maskImage.Width; ++x) + { + for (int y = 0; y < maskImage.Height; ++y) + { + px.FromRgb24(IdMapReadStorage.GetColor(maskImage[x, y], textures)); + finalImage[x, y] = px; + } + } + return Task.FromResult(finalImage)!; + } + + public Task SaveImagePart(int partId, Image partImage) + where TPixel : unmanaged, IPixel + { + throw new NotSupportedException(); + } + } +} diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImageryInfos.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImageryInfos.cs new file mode 100644 index 00000000..944460fb --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImageryInfos.cs @@ -0,0 +1,229 @@ +using System.Globalization; +using BIS.PAA; +using BIS.WRP; +using GameRealisticMap.Arma3.Assets; +using GameRealisticMap.Arma3.GameEngine; +using GameRealisticMap.Arma3.IO; +using Pmad.HugeImages; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + public class GenericImageryInfos : IArma3MapConfig, IImageryInfos + { + public GenericImageryInfos(List otherTileInfos, int tileSize, double resolution, float sizeInMeters, string pboPrefix, int idMapMultiplier) + { + OtherTileInfos = otherTileInfos; + TileSize = tileSize; + Resolution = resolution; + SizeInMeters = sizeInMeters; + IdMapMultiplier = idMapMultiplier; + PboPrefix = pboPrefix; + } + + public List OtherTileInfos { get; } + + public int TileSize { get; } + + public double Resolution { get; } + + public float SizeInMeters { get; } + + public int IdMapMultiplier { get; } + + public int TotalSize => (int)Math.Floor(SizeInMeters / Resolution); + + public string PboPrefix { get; } + + public float FakeSatBlend => throw new NotImplementedException(); + + public string WorldName => throw new NotImplementedException(); + + public bool UseColorCorrection => throw new NotImplementedException(); + + public IEnumerable GetGroundDetailTextures() + { + return OtherTileInfos.SelectMany(t => Enumerable.Range(0, t.Textures.Count).Select(i => new GroundDetailTexture(t.Textures[i], t.Normals[i]))).Distinct(); + } + + public static async Task TryCreate(IGameFileSystem fileSystem, EditableWrp wrp, string pboPrefix) + { + var rvmatCache = new Dictionary(); + + var list = new List(); + for (int x = 0; x < wrp.LandRangeX; x++) + { + for (int y = 0; y < wrp.LandRangeY; y++) + { + var materialIndex = wrp.MaterialIndex[x + y * wrp.LandRangeX]; + var materialName = wrp.MatNames[materialIndex]; + var infos = await GetMaterialInfo(fileSystem, rvmatCache, materialName); + if (infos != null) + { + infos.CellIndexes.Add(new Point(x, y)); + } + else + { + return null; + } + } + } + + var tiles = new List(); + foreach (var tileGroup in rvmatCache.Values.GroupBy(v => new { v.Mask, v.Sat })) + { + tiles.Add(new IntermediateTileInfo() + { + Mask = tileGroup.Key.Mask, + Sat = tileGroup.Key.Sat, + UA = tileGroup.First().UA, + UB = tileGroup.First().UB, + VB = tileGroup.First().VB, + CellIndexes = Rectangle.FromLTRB( + tileGroup.Min(v => v.CellIndexes.Min(p => p.X)), + tileGroup.Min(v => v.CellIndexes.Min(p => p.Y)), + tileGroup.Max(v => v.CellIndexes.Max(p => p.X)), + tileGroup.Max(v => v.CellIndexes.Max(p => p.Y))), + Textures = Merge(tileGroup.Select(v => v.Textures)), + Normals = Merge(tileGroup.Select(v => v.Normals)), + }); + } + + var mask = GetPaa(tiles[0].Mask, fileSystem); + + var sat = GetPaa(tiles[0].Sat, fileSystem); + + if (mask == null || sat == null) + { + return null; + } + + var tileSize = sat.Width; + var idMapMultiplier = mask.Width / sat.Width; + var resolution = 1d / (tiles[0].UA * tileSize); + var landgridCellCount = tiles.Select(t => t.CellIndexes.Width).Max() + 1; + var tileSizeMeters = landgridCellCount * wrp.CellSize; + var step = (int)Math.Round(tileSizeMeters / resolution); + var halfOverlap = (tileSize - step) / 2; + + var fullImageSize = new Size((int)Math.Round(wrp.CellSize * wrp.LandRangeX / resolution), (int)Math.Round(wrp.CellSize * wrp.LandRangeY / resolution)); + + var top = fullImageSize.Height + tileSize - halfOverlap - step; + + var mappedTiles = new List(); + + foreach (var tile in tiles) + { + var x = Math.Round((halfOverlap - tile.UB * tileSize) / step); + var y = Math.Round((top - tile.VB * tileSize) / step); + + var grmTile = new ImageryTile((int)x, (int)y, step, halfOverlap, tileSize, top, tile.UA); + if (Math.Abs(grmTile.UB - tile.UB) > 0.0001 || Math.Abs(grmTile.VB - tile.VB) > 0.0001) + { + // Results mismatch, not compatible + return null; + } + + // It's compatible ! + mappedTiles.Add(new GenericTileInfo(grmTile, tile)); + } + + return new GenericImageryInfos( + mappedTiles.OrderBy(i => i.X).ThenBy(i => i.Y).ToList(), + tileSize, + resolution, + sizeInMeters: wrp.LandRangeX * wrp.CellSize, + pboPrefix, + idMapMultiplier); + } + + private static PAA? GetPaa(string path, IGameFileSystem fileSystem) + { + using var stream = fileSystem.OpenFileIfExists(path); + if (stream == null) + { + return null; + } + return new PAA(stream); + } + + private static async Task GetMaterialInfo(IGameFileSystem fileSystem, Dictionary rvmatCache, string materialName) + { + if (!rvmatCache.TryGetValue(materialName, out var infos)) + { + using var content = fileSystem.OpenFileIfExists(materialName); + if (content == null) + { + return null; + } + + var contextAsText = await GameConfigHelper.GetText(content); + if (TerrainRvMatInfos.ShaderMatch.Matches(contextAsText).Count == 0) + { + return null; + } + var textures = IdMapHelper.TextureMatch.Matches(contextAsText).Select(m => m.Groups[1].Value).ToList(); + var normals = IdMapHelper.NormalMatch.Matches(contextAsText).Select(m => m.Groups[1].Value).ToList(); + var sat = TerrainRvMatInfos.SatMatch.Matches(contextAsText).Select(m => m.Groups[1].Value).ToList(); + var mask = TerrainRvMatInfos.MaskMatch.Matches(contextAsText).Select(m => m.Groups[1].Value).ToList(); + var transform = TerrainRvMatInfos.UvTransformMatch.Matches(contextAsText).FirstOrDefault(); + + if (sat.Count == 1 && mask.Count == 1 && textures.Count > 0 && normals.Count > 0 && transform != null) + { + rvmatCache.Add(materialName, infos = new TerrainRvMatInfos() + { + Sat = sat[0], + Mask = mask[0], + Textures = textures, + Normals = normals, + UA = float.Parse(transform.Groups["UA"].Value, CultureInfo.InvariantCulture), + UB = float.Parse(transform.Groups["UB"].Value, CultureInfo.InvariantCulture), + VB = float.Parse(transform.Groups["VB"].Value, CultureInfo.InvariantCulture) + }); + } + } + return infos; + } + + private static List Merge(IEnumerable> enumerable) + { + var first = enumerable.First(); + + foreach (var item in enumerable.Skip(1)) + { + for (int i = 0; i < first.Count; i++) + { + if (string.IsNullOrEmpty(first[i]) && !string.IsNullOrEmpty(item[i])) + { + first[i] = item[i]; + } + } + } + return first; + } + + + public HugeImage GetIdMap(IGameFileSystem fileSystem, TerrainMaterialLibrary materials) + { + return GetIdMap(fileSystem, materials); + } + + public HugeImage GetSatMap(IGameFileSystem fileSystem) + { + return GetSatMap(fileSystem); + } + + public HugeImage GetIdMap(IGameFileSystem fileSystem, TerrainMaterialLibrary materials) where TPixel : unmanaged, IPixel + { + var parts = new ImageryTilerHugeImagePartitioner(OtherTileInfos.Select(o => o.GrmTile).ToList(), IdMapMultiplier); + return new HugeImage(new GenericIdMapReadStorage(this, fileSystem, materials), new Size(TotalSize * IdMapMultiplier), new HugeImageSettingsBase(), parts, new TPixel()); + } + + public HugeImage GetSatMap(IGameFileSystem fileSystem) where TPixel : unmanaged, IPixel + { + var parts = new ImageryTilerHugeImagePartitioner(OtherTileInfos.Select(o => o.GrmTile).ToList(), 1); + return new HugeImage(new GenericSatMapReadStorage(this, fileSystem), new Size(TotalSize), new HugeImageSettingsBase(), parts, new TPixel()); + } + } +} diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImagerySource.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImagerySource.cs new file mode 100644 index 00000000..234718e8 --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImagerySource.cs @@ -0,0 +1,74 @@ +using BIS.PAA; +using GameRealisticMap.Arma3.Assets; +using GameRealisticMap.Arma3.GameEngine; +using GameRealisticMap.Arma3.IO; +using Pmad.HugeImages; +using Pmad.HugeImages.Processing; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + public class GenericImagerySource : IImagerySource + { + private readonly GenericImageryInfos imagery; + private readonly IGameFileSystem projectDrive; + private readonly TerrainMaterialLibrary materials; + + public GenericImagerySource(GenericImageryInfos imagery, IGameFileSystem projectDrive, TerrainMaterialLibrary materials) + { + this.imagery = imagery; + this.projectDrive = projectDrive; + this.materials = materials; + } + + public Task> CreateIdMap() + { + return Task.FromResult(imagery.GetIdMap(projectDrive, materials)); + } + + public async Task CreatePictureMap() + { + var existing = LoadExisting("picturemap_ca"); + if (existing != null) + { + return existing; + } + using var satMap = imagery.GetSatMap(projectDrive); + return await satMap.ToScaledImageAsync(2048, 2048); + } + + public Task> CreateSatMap() + { + return Task.FromResult(imagery.GetSatMap(projectDrive)); + } + + public Image CreateSatOut() + { + var existing = LoadExisting("satout_ca"); + if (existing != null) + { + return existing; + } + return new Image(4, 4); + } + + private Image? LoadExisting(string name) + { + using var paaStream = projectDrive.OpenFileIfExists($"{imagery.PboPrefix}\\data\\{name}.paa"); + if (paaStream != null) + { + var paa = new PAA(paaStream); + var map = paa.Mipmaps.OrderBy(m => m.Width).Last(); + var pixels = PAA.GetARGB32PixelData(paa, paaStream, map); + return Image.LoadPixelData(pixels, map.Width, map.Height).CloneAs(); + } + using var png = projectDrive.OpenFileIfExists($"{imagery.PboPrefix}\\data\\{name}.png"); + if (png != null) + { + return Image.Load(png); + } + return null; + } + } +} \ No newline at end of file diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericSatMapReadStorage.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericSatMapReadStorage.cs new file mode 100644 index 00000000..5c99af14 --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericSatMapReadStorage.cs @@ -0,0 +1,61 @@ +using BIS.PAA; +using GameRealisticMap.Arma3.IO; +using Pmad.HugeImages.Storage; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + internal sealed class GenericSatMapReadStorage : IHugeImageStorageSlot + { + private readonly GenericImageryInfos partitioner; + private readonly IGameFileSystem fileSystem; + private readonly int tileSize; + + public GenericSatMapReadStorage(GenericImageryInfos partitioner, IGameFileSystem fileSystem) + { + this.partitioner = partitioner; + this.fileSystem = fileSystem; + tileSize = partitioner.TileSize; + } + + public void Dispose() + { + + } + + private string GetPartFileName(int partId) + { + return partitioner.OtherTileInfos[partId - 1].Sat; + } + + public Task?> LoadImagePart(int partId) + where TPixel : unmanaged, IPixel + { + var fileName = GetPartFileName(partId); + using var paaStream = fileSystem.OpenFileIfExists(fileName); + if (paaStream == null) + { + throw new FileNotFoundException($"File '{fileName}' was not found."); + } + var paa = new PAA(paaStream); + var map = paa.Mipmaps.OrderBy(m => m.Width).Last(); + var pixels = PAA.GetARGB32PixelData(paa, paaStream, map); + var img = Image.LoadPixelData(pixels, map.Width, map.Height).CloneAs(); + if (img.Width != tileSize || img.Height != tileSize) + { + img.Mutate(img => img.Resize(tileSize, tileSize)); + } + return Task.FromResult(img)!; + } + + public Task SaveImagePart(int partId, Image partImage) + where TPixel : unmanaged, IPixel + { + throw new NotSupportedException(); + } + + + } +} diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericTileInfo.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericTileInfo.cs new file mode 100644 index 00000000..115837a9 --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericTileInfo.cs @@ -0,0 +1,31 @@ +using GameRealisticMap.Arma3.GameEngine; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + public class GenericTileInfo + { + internal GenericTileInfo(ImageryTile grmTile, IntermediateTileInfo tile) + { + this.GrmTile = grmTile; + this.Mask = tile.Mask; + this.Sat = tile.Sat; + this.Textures = tile.Textures; + this.Normals = tile.Normals; + } + + public int X => GrmTile.X; + + public int Y => GrmTile.Y; + + public ImageryTile GrmTile { get; } + + public string Sat { get; } + + public string Mask { get; } + + public List Textures { get; } + + public List Normals { get; } + + } +} diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/IntermediateTileInfo.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/IntermediateTileInfo.cs new file mode 100644 index 00000000..62cabb9b --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/IntermediateTileInfo.cs @@ -0,0 +1,16 @@ +using SixLabors.ImageSharp; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + internal class IntermediateTileInfo + { + public required string Sat { get; set; } + public required string Mask { get; set; } + public required List Textures { get; set; } + public required List Normals { get; set; } + public Rectangle CellIndexes { get; internal set; } + public float UA { get; internal set; } + public float UB { get; internal set; } + public float VB { get; internal set; } + } +} \ No newline at end of file diff --git a/GameRealisticMap.Arma3/Edit/Imagery/Generic/TerrainRvMatInfos.cs b/GameRealisticMap.Arma3/Edit/Imagery/Generic/TerrainRvMatInfos.cs new file mode 100644 index 00000000..75cfc667 --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/Generic/TerrainRvMatInfos.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using System.Text.RegularExpressions; +using SixLabors.ImageSharp; + +namespace GameRealisticMap.Arma3.Edit.Imagery.Generic +{ + internal class TerrainRvMatInfos + { + internal static readonly Regex ShaderMatch = new Regex(@"PixelShaderID\s*=\s*""TerrainX"";", RegexOptions.CultureInvariant); + + internal static readonly Regex SatMatch = new Regex(@"class Stage0[\s\S]*texture\s*=\s*""([^""]*)"";\s*texGen\s*=\s*3;", RegexOptions.CultureInvariant | RegexOptions.Multiline); + + internal static readonly Regex MaskMatch = new Regex(@"class Stage1[\s\S]*texture\s*=\s*""([^""]*)"";\s*texGen\s*=\s*4;", RegexOptions.CultureInvariant | RegexOptions.Multiline); + + internal static readonly Regex UvTransformMatch = new Regex(@"aside\[\]\s*=\s*\{\s*(?[\d\.\-]+),\s*0,\s*0\s*\};\s*up\[\]\s*=\s*\{\s*0,\s*0,\s*(?[\d\.\-]+)\s*\};\s*dir\[\]\s*=\s*\{\s*0,\s*(?[\d\.\-]+),\s*0\s*\};\s*pos\[\]\s*=\s*\{\s*(?[\d\.\-]+),\s*(?[\d\.\-]+),\s*0\s*\};", RegexOptions.CultureInvariant | RegexOptions.Multiline); + + public required string Sat { get; internal set; } + public required string Mask { get; internal set; } + public required List Textures { get; internal set; } + public required List Normals { get; internal set; } + public List CellIndexes { get; internal set; } = new List(); + public float UA { get; internal set; } + public float UB { get; internal set; } + public float VB { get; internal set; } + } +} \ No newline at end of file diff --git a/GameRealisticMap.Arma3/Edit/Imagery/IImageryInfos.cs b/GameRealisticMap.Arma3/Edit/Imagery/IImageryInfos.cs new file mode 100644 index 00000000..479a7d60 --- /dev/null +++ b/GameRealisticMap.Arma3/Edit/Imagery/IImageryInfos.cs @@ -0,0 +1,20 @@ +using GameRealisticMap.Arma3.Assets; +using GameRealisticMap.Arma3.IO; +using Pmad.HugeImages; +using SixLabors.ImageSharp.PixelFormats; + +namespace GameRealisticMap.Arma3.Edit.Imagery +{ + public interface IImageryInfos + { + HugeImage GetIdMap(IGameFileSystem fileSystem, TerrainMaterialLibrary materials); + + HugeImage GetSatMap(IGameFileSystem fileSystem); + + double Resolution { get; } + + int IdMapMultiplier { get; } + + int TotalSize { get; } + } +} diff --git a/GameRealisticMap.Arma3/Edit/Imagery/IdMapHelper.cs b/GameRealisticMap.Arma3/Edit/Imagery/IdMapHelper.cs index f5c68fd0..50f9919a 100644 --- a/GameRealisticMap.Arma3/Edit/Imagery/IdMapHelper.cs +++ b/GameRealisticMap.Arma3/Edit/Imagery/IdMapHelper.cs @@ -7,9 +7,9 @@ namespace GameRealisticMap.Arma3.Edit.Imagery { public static class IdMapHelper { - internal static readonly Regex NormalMatch = new Regex(@"texture=""([^""]*)"";\r?\n\ttexGen=1;", RegexOptions.CultureInvariant); + internal static readonly Regex NormalMatch = new Regex(@"texture\s*=\s*""([^""]*)"";\s*texGen\s*=\s*1;", RegexOptions.CultureInvariant | RegexOptions.Multiline); - internal static readonly Regex TextureMatch = new Regex(@"texture=""([^""]*)"";\r?\n\ttexGen=2;", RegexOptions.CultureInvariant); + internal static readonly Regex TextureMatch = new Regex(@"texture\s*=\s*""([^""]*)"";\s*texGen\s*=\s*2;", RegexOptions.CultureInvariant | RegexOptions.Multiline); public static Task> GetUsedTextureList(EditableWrp wrp, IGameFileSystem projectDrive) { diff --git a/GameRealisticMap.Arma3/Edit/Imagery/ImageryTilerHugeImagePartitioner.cs b/GameRealisticMap.Arma3/Edit/Imagery/ImageryTilerHugeImagePartitioner.cs index 302a5451..b001b029 100644 --- a/GameRealisticMap.Arma3/Edit/Imagery/ImageryTilerHugeImagePartitioner.cs +++ b/GameRealisticMap.Arma3/Edit/Imagery/ImageryTilerHugeImagePartitioner.cs @@ -15,6 +15,12 @@ public ImageryTilerHugeImagePartitioner(ImageryTiler imageryTiler, int multiplie parts = imageryTiler.All.OrderBy(i => i.X).ThenBy(i => i.Y).ToList(); } + public ImageryTilerHugeImagePartitioner(List parts, int multiplier) + { + this.multiplier = multiplier; + this.parts = parts; + } + public List CreateParts(Size size) { return parts.Select(i => new HugeImagePartDefinition( diff --git a/GameRealisticMap.Arma3/GameEngine/GameConfigHelper.cs b/GameRealisticMap.Arma3/GameEngine/GameConfigHelper.cs new file mode 100644 index 00000000..0981d0c9 --- /dev/null +++ b/GameRealisticMap.Arma3/GameEngine/GameConfigHelper.cs @@ -0,0 +1,27 @@ +using BIS.Core.Config; + +namespace GameRealisticMap.Arma3.GameEngine +{ + internal static class GameConfigHelper + { + private static bool IsBinaryConfig(Stream stream) + { + var buffer = new byte[4]; + stream.Read(buffer, 0, 4); + stream.Position = 0; + return buffer.SequenceEqual(new byte[] { 0, (byte)'r', (byte)'a', (byte)'P' }); + } + + public static async Task GetText(Stream stream) + { + if (IsBinaryConfig(stream)) + { + return new ParamFile(stream).ToString(); + } + using (var reader = new StreamReader(stream)) + { + return await reader.ReadToEndAsync(); + } + } + } +} diff --git a/GameRealisticMap.Studio/Labels.Designer.cs b/GameRealisticMap.Studio/Labels.Designer.cs index af9e0775..e4ed3bae 100644 --- a/GameRealisticMap.Studio/Labels.Designer.cs +++ b/GameRealisticMap.Studio/Labels.Designer.cs @@ -1744,6 +1744,15 @@ public static string Default { } } + /// + /// Recherche une chaîne localisée semblable à Denied. + /// + public static string Denied { + get { + return ResourceManager.GetString("Denied", resourceCulture); + } + } + /// /// Recherche une chaîne localisée semblable à Average density. /// @@ -3556,6 +3565,15 @@ public static string ProbabilityOfThisCollection { } } + /// + /// Recherche une chaîne localisée semblable à You are not allowed to edit or to export data from this map.. + /// + public static string ProtectedMapMessage { + get { + return ResourceManager.GetString("ProtectedMapMessage", resourceCulture); + } + } + /// /// Recherche une chaîne localisée semblable à ⚡ Quick start from :. /// @@ -3880,6 +3898,15 @@ public static string RestoreDataLossMessage { } } + /// + /// Recherche une chaîne localisée semblable à Restore Imagery. + /// + public static string RestoreImagery { + get { + return ResourceManager.GetString("RestoreImagery", resourceCulture); + } + } + /// /// Recherche une chaîne localisée semblable à Restore {0}. /// diff --git a/GameRealisticMap.Studio/Labels.fr.resx b/GameRealisticMap.Studio/Labels.fr.resx index abc6d7cb..183b2ad4 100644 --- a/GameRealisticMap.Studio/Labels.fr.resx +++ b/GameRealisticMap.Studio/Labels.fr.resx @@ -1642,4 +1642,13 @@ Dernière génération le {1} Recalculer + + Restaurer l'imagerie + + + Refusé + + + Vous n'êtes pas autorisé à modifier, ou à exporter des données de cette carte. + \ No newline at end of file diff --git a/GameRealisticMap.Studio/Labels.resx b/GameRealisticMap.Studio/Labels.resx index f8ab9b34..3cef724f 100644 --- a/GameRealisticMap.Studio/Labels.resx +++ b/GameRealisticMap.Studio/Labels.resx @@ -1642,4 +1642,13 @@ Last generated on {1} Recompute + + Restore Imagery + + + Denied + + + You are not allowed to edit or to export data from this map. + \ No newline at end of file diff --git a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Arma3WorldEditorViewModel.cs b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Arma3WorldEditorViewModel.cs index 6674118b..d30127cf 100644 --- a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Arma3WorldEditorViewModel.cs +++ b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Arma3WorldEditorViewModel.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using System.Windows.Media; +using System.Windows; using BIS.Core.Config; using BIS.Core.Streams; using BIS.WRP; @@ -13,6 +13,7 @@ using GameRealisticMap.Arma3.Assets; using GameRealisticMap.Arma3.Edit; using GameRealisticMap.Arma3.Edit.Imagery; +using GameRealisticMap.Arma3.Edit.Imagery.Generic; using GameRealisticMap.Arma3.GameEngine; using GameRealisticMap.Arma3.GameEngine.Roads; using GameRealisticMap.Arma3.GameLauncher; @@ -60,6 +61,7 @@ internal class Arma3WorldEditorViewModel : PersistedDocumentOverridable, IExplor private List objectStatsItems = new List(); private List _materials = new List(); private string? initialFilePath; + private GenericImageryInfos? _genericImagery; public Arma3WorldEditorViewModel(IArma3DataModule arma3Data, IWindowManager windowManager, IArma3RecentHistory history, IArma3BackupService worldBackup) { @@ -93,6 +95,12 @@ protected override async Task DoLoad(string filePath) World = StreamHelper.Read(filePath).GetEditableWrp(); + if (LicensingGuard.IsProtected(World)) + { + CloseDueToProtection(); + return; + } + ConfigFile = ReadGameConfig(filePath, worldName); savedRevision = ConfigFile?.Revision ?? 0; @@ -105,11 +113,24 @@ protected override async Task DoLoad(string filePath) if (ConfigFile != null && WrpCompiler.SeemsGeneratedByUs(World, ConfigFile.PboPrefix)) { - Imagery = ExistingImageryInfos.TryCreate(arma3Data.ProjectDrive, ConfigFile.PboPrefix, SizeInMeters!.Value); + GrmImagery = ExistingImageryInfos.TryCreate(arma3Data.ProjectDrive, ConfigFile.PboPrefix, SizeInMeters!.Value); Materials = await MaterialItem.Create(this, World, arma3Data.ProjectDrive, ConfigFile.PboPrefix); } + if (GrmImagery == null) + { + var pboPrefix = ConfigFile?.PboPrefix ?? ""; + + var other = GenericImagery = await GenericImageryInfos.TryCreate(arma3Data.ProjectDrive, World, pboPrefix); + + if (other != null) + { + Materials = await MaterialItem.Create(this, pboPrefix, other.GetGroundDetailTextures().ToList()); + } + } + + if (Roads != null) { LoadRoads(); @@ -123,6 +144,13 @@ protected override async Task DoLoad(string filePath) await IoC.Get().AddRecentFile(filePath); } + private void CloseDueToProtection() + { + World = null; + _ = TryCloseAsync(); + MessageBox.Show(Labels.ProtectedMapMessage, Labels.Denied, MessageBoxButton.OK, MessageBoxImage.Error); + } + internal void UpdateBackupsList(string filePath) { Backups = new[] { new RevisionHistoryEntry(this) }.Concat(worldBackup.GetBackups(filePath).Select(b => new RevisionHistoryEntry(this, b))).ToList(); @@ -243,14 +271,24 @@ public List Backups set { _backups = value; NotifyOfPropertyChange(); } } - public bool IsImageryEditable => Imagery != null; + public bool IsImageryEditable => GrmImagery != null; + + public bool IsNotImageryEditable => GrmImagery == null; - public bool IsNotImageryEditable => Imagery == null; + public bool HasImagery => GrmImagery != null || GenericImagery != null; - public ExistingImageryInfos? Imagery + public ExistingImageryInfos? GrmImagery { get { return _imagery; } - set { _imagery = value; NotifyOfPropertyChange(); NotifyOfPropertyChange(nameof(IsImageryEditable)); NotifyOfPropertyChange(nameof(IsNotImageryEditable)); } + set + { + _imagery = value; + NotifyOfPropertyChange(); + NotifyOfPropertyChange(nameof(Imagery)); + NotifyOfPropertyChange(nameof(HasImagery)); + NotifyOfPropertyChange(nameof(IsImageryEditable)); + NotifyOfPropertyChange(nameof(IsNotImageryEditable)); + } } public bool IsRoadsDirty @@ -509,7 +547,8 @@ public void ClearActive() public Task ExportSatMap() { - if (_imagery != null) + var imagery = Imagery; + if (imagery != null) { var dialog = new SaveFileDialog(); dialog.Filter = "PNG|*.png"; @@ -518,7 +557,7 @@ public Task ExportSatMap() { var filename = dialog.FileName; IoC.Get() - .RunTask(GameRealisticMap.Studio.Labels.ExportSatelliteImage, ui => DoExport(ui, filename, _imagery.GetSatMap(arma3Data.ProjectDrive))); + .RunTask(GameRealisticMap.Studio.Labels.ExportSatelliteImage, ui => DoExport(ui, filename, imagery.GetSatMap(arma3Data.ProjectDrive))); } } return Task.CompletedTask; @@ -526,7 +565,8 @@ public Task ExportSatMap() public async Task ExportIdMap() { - if (_imagery != null) + var imagery = Imagery; + if (imagery != null) { var dialog = new SaveFileDialog(); dialog.Filter = "PNG|*.png"; @@ -536,7 +576,7 @@ public async Task ExportIdMap() var filename = dialog.FileName; var materials = await GetExportMaterialLibrary(); _ = IoC.Get() - .RunTask(GameRealisticMap.Studio.Labels.ExportTextureMaskImage, ui => DoExport(ui, filename, _imagery.GetIdMap(arma3Data.ProjectDrive, materials))); + .RunTask(GameRealisticMap.Studio.Labels.ExportTextureMaskImage, ui => DoExport(ui, filename, imagery.GetIdMap(arma3Data.ProjectDrive, materials))); } } @@ -744,7 +784,21 @@ public Task TakeAerialImages() Dependencies.Items)); } - public bool AreAllMaterialsFromLibrary => Materials.Count > 0 && Materials.All(m => m.IsFromLibrary); + public bool AreAllMaterialsFromLibrary => Materials.Count > 0 && Materials.All(m => m.IsFromLibrary) && GrmImagery != null; + + public GenericImageryInfos? GenericImagery + { + get { return _genericImagery; } + set + { + _genericImagery = value; + NotifyOfPropertyChange(); + NotifyOfPropertyChange(nameof(Imagery)); + NotifyOfPropertyChange(nameof(HasImagery)); + } + } + + public IImageryInfos? Imagery => (IImageryInfos?)GrmImagery ?? GenericImagery; public Task RegenerateMaterialsFromLibrary() { @@ -757,7 +811,17 @@ public Task RegenerateMaterialsFromLibrary() public ExistingImageryInfos GetConfig() { - return Imagery ?? new ExistingImageryInfos(0, 0, 0, ConfigFile?.PboPrefix ?? string.Empty); + return GrmImagery ?? new ExistingImageryInfos(0, 0, 0, ConfigFile?.PboPrefix ?? string.Empty); + } + + public void RestoreImagery() + { + var world = World; + var imagery = GenericImagery; + if (world != null && imagery != null) + { + IoC.Get().Run(new RestoreImageryTask(this, world, imagery, arma3Data.ProjectDrive)); + } } } } diff --git a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Import/RestoreImageryTask.cs b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Import/RestoreImageryTask.cs new file mode 100644 index 00000000..29008c67 --- /dev/null +++ b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/Import/RestoreImageryTask.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using BIS.WRP; +using GameRealisticMap.Arma3.Edit.Imagery; +using GameRealisticMap.Arma3.Edit.Imagery.Generic; +using GameRealisticMap.Arma3.GameEngine; +using GameRealisticMap.Arma3.IO; +using GameRealisticMap.Studio.Modules.Reporting; + +namespace GameRealisticMap.Studio.Modules.Arma3WorldEditor.ViewModels.Import +{ + internal class RestoreImageryTask : IProcessTask + { + private readonly Arma3WorldEditorViewModel editor; + private readonly EditableWrp initialWorld; + private readonly GenericImageryInfos imagery; + private readonly IProjectDrive projectDrive; + + public RestoreImageryTask(Arma3WorldEditorViewModel editor, EditableWrp initialWorld, GenericImageryInfos imagery, IProjectDrive projectDrive) + { + this.editor = editor; + this.initialWorld = initialWorld; + this.imagery = imagery; + this.projectDrive = projectDrive; + } + + public string Title => Labels.RestoreImagery; + + public bool Prompt => true; + + public async Task Run(IProgressTaskUI ui) + { + var initialMaterials = await editor.GetExportMaterialLibrary(); + var imgCompiler = new ImageryCompiler(initialMaterials, ui.Scope, projectDrive); + var wrpCompiler = new WrpCompiler(ui.Scope, projectDrive); + var tiler = await imgCompiler.Compile(imagery, new GenericImagerySource(imagery, projectDrive, initialMaterials)); + var newWorld = wrpCompiler.CreateWorldWithoutObjects(initialWorld.ToElevationGrid(), tiler, imagery.PboPrefix); + newWorld.Objects = initialWorld.Objects; + + await projectDrive.ProcessImageToPaa(ui.Scope); + + editor.World = newWorld; + editor.IsDirty = true; + editor.GenericImagery = null; + editor.GrmImagery = ExistingImageryInfos.TryCreate(projectDrive, imagery.PboPrefix, imagery.SizeInMeters); + editor.Materials = await MaterialItem.Create(editor, newWorld, projectDrive, imagery.PboPrefix); + } + } +} diff --git a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/LicensingGuard.cs b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/LicensingGuard.cs new file mode 100644 index 00000000..8c4cfab9 --- /dev/null +++ b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/LicensingGuard.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using BIS.WRP; + +namespace GameRealisticMap.Studio.Modules.Arma3WorldEditor.ViewModels +{ + internal static class LicensingGuard + { + private static readonly string[] ProtectedPrefixes = ["a3\\"]; + + public static bool IsProtected(EditableWrp? world) + { + // Maps within 'a3\' are licensed with a restrictive EULA : will deny to do anything with those maps + // Test materials path, as it's easy to move or rename wrp file, but it's hard to change materials (without a wrp editor) + return world != null && world.MatNames.Where(m => m != null).Any(IsProtectedPath); + } + + private static bool IsProtectedPath(string path) + { + return ProtectedPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/MaterialItem.cs b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/MaterialItem.cs index 584164a3..3c46a84f 100644 --- a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/MaterialItem.cs +++ b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/ViewModels/MaterialItem.cs @@ -26,29 +26,35 @@ public MaterialItem(Arma3WorldEditorViewModel parent, string texture, string nor this.libTexture = libTexture; } - public static async Task> Create(Arma3WorldEditorViewModel parent, EditableWrp wrp, ProjectDrive projectDrive, string pboPrefix) + public static async Task> Create(Arma3WorldEditorViewModel parent, EditableWrp wrp, IGameFileSystem projectDrive, string pboPrefix) { - var items = new List(); var textures = await IdMapHelper.GetUsedTextureList(wrp, projectDrive); - if (textures.Count > 0) + if (textures.Count == 0) { - var lib = IoC.Get(); - foreach (var texture in textures) + return new List(); + } + return await Create(parent, pboPrefix, textures); + } + + public static async Task> Create(Arma3WorldEditorViewModel parent, string pboPrefix, List textures) + { + var items = new List(); + var lib = IoC.Get(); + foreach (var texture in textures) + { + var libTexture = await lib.Resolve(texture.ColorTexture); + if (libTexture == null) { - var libTexture = await lib.Resolve(texture.ColorTexture); - if (libTexture == null) + if (texture.ColorTexture.StartsWith(pboPrefix, StringComparison.OrdinalIgnoreCase)) + { + libTexture = await lib.Resolve("{PboPrefix}" + texture.ColorTexture.Substring(pboPrefix.Length)); + } + else { - if (texture.ColorTexture.StartsWith(pboPrefix, StringComparison.OrdinalIgnoreCase)) - { - libTexture = await lib.Resolve("{PboPrefix}" + texture.ColorTexture.Substring(pboPrefix.Length)); - } - else - { - libTexture = await lib.ImportExternal(texture.ColorTexture, texture.NormalTexture); - } + libTexture = await lib.ImportExternal(texture.ColorTexture, texture.NormalTexture); } - items.Add(new MaterialItem(parent, texture.ColorTexture, texture.NormalTexture, libTexture)); } + items.Add(new MaterialItem(parent, texture.ColorTexture, texture.NormalTexture, libTexture)); } return items; } diff --git a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/Views/Arma3WorldEditorView.xaml b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/Views/Arma3WorldEditorView.xaml index 70627114..e25c53bf 100644 --- a/GameRealisticMap.Studio/Modules/Arma3WorldEditor/Views/Arma3WorldEditorView.xaml +++ b/GameRealisticMap.Studio/Modules/Arma3WorldEditor/Views/Arma3WorldEditorView.xaml @@ -136,7 +136,7 @@ - + - - + @@ -224,7 +227,7 @@ -