Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, TerrainMaterial> 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<Image<TPixel>?> LoadImagePart<TPixel>(int partId)
where TPixel : unmanaged, IPixel<TPixel>
{
Copy link

Copilot AI Apr 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing an element using 'partId-1' assumes that the part IDs are 1-based and that the index will always be within bounds. Please add a validation to ensure 'partId' is valid to prevent potential out-of-range errors.

Suggested change
{
{
if (partId < 1 || partId > partitioner.OtherTileInfos.Count)
{
throw new ArgumentOutOfRangeException(nameof(partId), "partId must be within the valid range.");
}

Copilot uses AI. Check for mistakes.
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<Bgra32>(pixels, map.Width, map.Height).CloneAs<Rgba32>();
if (maskImage.Width != tileSize || maskImage.Height != tileSize)
{
maskImage.Mutate(img => img.Resize(tileSize, tileSize));
}
var finalImage = new Image<TPixel>(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<TPixel>(int partId, Image<TPixel> partImage)
where TPixel : unmanaged, IPixel<TPixel>
{
throw new NotSupportedException();
}
}
}
229 changes: 229 additions & 0 deletions GameRealisticMap.Arma3/Edit/Imagery/Generic/GenericImageryInfos.cs
Original file line number Diff line number Diff line change
@@ -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<GenericTileInfo> 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<GenericTileInfo> 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<GroundDetailTexture> 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<GenericImageryInfos?> TryCreate(IGameFileSystem fileSystem, EditableWrp wrp, string pboPrefix)
{
var rvmatCache = new Dictionary<string, TerrainRvMatInfos>();

var list = new List<IntermediateTileInfo>();
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<IntermediateTileInfo>();
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<GenericTileInfo>();

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<TerrainRvMatInfos?> GetMaterialInfo(IGameFileSystem fileSystem, Dictionary<string, TerrainRvMatInfos> 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<string> Merge(IEnumerable<List<string>> 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<Rgb24> GetIdMap(IGameFileSystem fileSystem, TerrainMaterialLibrary materials)
{
return GetIdMap<Rgb24>(fileSystem, materials);
}

public HugeImage<Rgb24> GetSatMap(IGameFileSystem fileSystem)
{
return GetSatMap<Rgb24>(fileSystem);
}

public HugeImage<TPixel> GetIdMap<TPixel>(IGameFileSystem fileSystem, TerrainMaterialLibrary materials) where TPixel : unmanaged, IPixel<TPixel>
{
var parts = new ImageryTilerHugeImagePartitioner(OtherTileInfos.Select(o => o.GrmTile).ToList(), IdMapMultiplier);
return new HugeImage<TPixel>(new GenericIdMapReadStorage(this, fileSystem, materials), new Size(TotalSize * IdMapMultiplier), new HugeImageSettingsBase(), parts, new TPixel());
}

public HugeImage<TPixel> GetSatMap<TPixel>(IGameFileSystem fileSystem) where TPixel : unmanaged, IPixel<TPixel>
{
var parts = new ImageryTilerHugeImagePartitioner(OtherTileInfos.Select(o => o.GrmTile).ToList(), 1);
return new HugeImage<TPixel>(new GenericSatMapReadStorage(this, fileSystem), new Size(TotalSize), new HugeImageSettingsBase(), parts, new TPixel());
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HugeImage<Rgba32>> CreateIdMap()
{
return Task.FromResult(imagery.GetIdMap<Rgba32>(projectDrive, materials));
}

public async Task<Image> 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<HugeImage<Rgba32>> CreateSatMap()
{
return Task.FromResult(imagery.GetSatMap<Rgba32>(projectDrive));
}

public Image CreateSatOut()
{
var existing = LoadExisting("satout_ca");
if (existing != null)
{
return existing;
}
return new Image<Rgb24>(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<Bgra32>(pixels, map.Width, map.Height).CloneAs<Rgb24>();
}
using var png = projectDrive.OpenFileIfExists($"{imagery.PboPrefix}\\data\\{name}.png");
if (png != null)
{
return Image.Load(png);
}
return null;
}
}
}
Loading
Loading