Skip to content
Open
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
65 changes: 50 additions & 15 deletions src/Ray1MapRepacker/Exporter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using BinarySerializer;
using BinarySerializer.Image;
using BinarySerializer.Ray1;
using BinarySerializer.Ray1.PC;

namespace Ray1MapRepacker;
Expand All @@ -12,15 +13,29 @@ public class Exporter(Context context)
/// </summary>
/// <param name="levFilePath"> Path to the lev file that should be exported. </param>
/// <param name="mapFileDir"> Path to the map dir, the lev data should be exported to. </param>
/// <param name="tilesetNamePrefix"> Name prefix for the created tileset file. </param>
/// <param name="mapFileName"> Name of the map file, the lev data is exported to. </param>
public void ExportLevel(string levFilePath, string mapFileDir, string tilesetNamePrefix, string mapFileName)
/// <param name="tilesetNamePrefix"> Name prefix for the created tileset file. </param>
public void ExportLevel(string levFilePath, string mapFileDir, string mapFileName, string tilesetNamePrefix)
{
Console.WriteLine($"Starting export process for {levFilePath}");

LevelFile levFile = ContextHelper.ReadLevelFile(context, levFilePath);
ExportTileSet(levFile, mapFileDir, tilesetNamePrefix);
ExportMap(levFile, mapFileDir, mapFileName);
ExportAndSaveMap(levFile, mapFileDir, mapFileName, []);

Console.WriteLine($"Finished export process for {levFilePath}");
}

public void ExportLevelForceUsingTileSet(string levFilePath, string mapFileDir, string mapFileName, string tileSetPathPCX)
{
Console.WriteLine($"Starting export process for {levFilePath}");

context.AddFile(new LinearFile(context, tileSetPathPCX));
PCX pcx = FileFactory.Read<PCX>(context, tileSetPathPCX);
LevelFile levFile = ContextHelper.ReadLevelFile(context, levFilePath);

ushort[][] tileSetIndexMaps = TileSetHelpers.CreateTileSetIndexMaps(levFile, pcx);
ExportAndSaveMap(levFile, mapFileDir, mapFileName, tileSetIndexMaps);

Console.WriteLine($"Finished export process for {levFilePath}");
}
Expand Down Expand Up @@ -54,26 +69,46 @@ private void ExportTileSet(LevelFile levFile, string mapFileDir, string tilesetN
Console.WriteLine("Finished exporting tileset");
}

private void ExportMap(LevelFile levFile, string mapFileDir, string mapName)
private void ExportAndSaveMap(LevelFile levFile, string mapFileDir, string mapName, ushort[][] tileSetIndexMaps)
{
Console.WriteLine("Exporting map");

// Export map in the universal format (used by the Mapper)
UniversalMap map = new()
Console.WriteLine("Exporting and saving map");

UniversalMap map = new UniversalMap()
{
Width = levFile.MapInfo.Width,
Height = levFile.MapInfo.Height,
Tiles = levFile.MapInfo.Blocks.Select(x => new UniversalMapBlock
{
TileIndex = x.TileIndex,
BlockType = x.BlockType
}).ToArray()
Tiles = MapBlocksToTiles(levFile.MapInfo.Blocks, tileSetIndexMaps)
};

string mapFilePath = Path.Combine(mapFileDir, mapName);
context.AddFile(new LinearFile(context, mapFilePath));
FileFactory.Write<UniversalMap>(context, mapFilePath, map);

Console.WriteLine("Finished exporting map");
Console.WriteLine("Finished exporting and saving map");
}

private UniversalMapBlock[] MapBlocksToTiles(Block[] blocks, ushort[][] tileSetIndexMaps)
{
if (tileSetIndexMaps.Length == 0)
{
return blocks.Select(x => new UniversalMapBlock
{
TileIndex = x.TileIndex,
BlockType = x.BlockType
}).ToArray();
}
else
{
// TODO array out of bounds..
return blocks.Select(x => new UniversalMapBlock
{
TileIndex = x.RenderMode == Block.BlockRenderMode.Opaque
? tileSetIndexMaps[0][Math.Min(tileSetIndexMaps[0].Length, x.TileIndex)]
: x.RenderMode == Block.BlockRenderMode.Transparent
? tileSetIndexMaps[1][Math.Min(tileSetIndexMaps[1].Length, x.TileIndex)]
: (ushort)0, // TODO handle fully transparent mode correctly..
BlockType = x.BlockType
}).ToArray();
}
}
}
98 changes: 98 additions & 0 deletions src/Ray1MapRepacker/ImageHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,104 @@ public static RGB666Color[] ConvertPal888To666(RGB888Color[] pal888)
return pal666;
}


// Map all indices of the lev file palettes onto the PCX palette, to allow a comparison between tiles later on
public static byte[][] CreateColorPaletteIndexMap(RGB666Color[][] palettesLevel, RGB666Color[] palettePCX)
{
if(palettesLevel.Length == 0 || palettePCX.Length == 0)
return [];

RGB666Color black = new RGB666Color(0f, 0f, 0f);
RGB666Color white = new RGB666Color(1f, 1f, 1f);


// Create a mapping for every palette in the level
byte[][] indexMap = new byte[palettesLevel.Length][];
for (ushort palIndex = 0; palIndex < palettesLevel.Length; palIndex++)
{
RGB666Color[] palette = palettesLevel[palIndex];
HashSet<ushort> zeroMappings = [];
// Handle empty palette
if (palettePCX.Length == 0)
{
indexMap[palIndex] = [];
continue;
}

// Map every palette color index onto one of the PCX palette
indexMap[palIndex] = new byte[palette.Length];
for (ushort colorIndex = 100; colorIndex != palette.Length; colorIndex++)
{
RGB666Color currentColor = palette[colorIndex];

int index = 255;
if (!CompareColorsFuzzy(currentColor, black, 0f)
&& !CompareColorsFuzzy(currentColor, white, 0f))
{
index = Array.FindIndex(palettePCX, c => CompareColorsFuzzy(currentColor, c, 0f));
}

byte clampedIndex = (byte) Math.Min(palette.Length - 1, Math.Max(0, index));

if (index != -1)
Console.WriteLine($"mapping colorIndex {colorIndex}->{clampedIndex} with color: {currentColor}->{palettePCX[clampedIndex]}");

if (index == -1)
zeroMappings.Add(colorIndex);

indexMap[palIndex][colorIndex] = clampedIndex;
}


if (zeroMappings.Count != 0)
Console.WriteLine($"mapped {palette.Length - zeroMappings.Count} of {palette.Length} without threshold ({zeroMappings.Count} invalid) for palette {palIndex}");

const float startThreshold = 0.008f; // rounding errors are at around 0.016f
const float baseThreshold = 0.004f; // lower value => higher accuracy - 0.004f is around value 1 difference in RGBA value
const byte maxIterationCount = 32; // higher value => more mapping hits, but larger color differences in mapping possible and longer processing time
// => up to RGB value difference for each color value of 34 is possible with startThreshold = 0.008f, baseThreshold = 0.004f and maxIterationCount = 32
for (byte thresholdIteration = 0; thresholdIteration < maxIterationCount; thresholdIteration++)
{
if (zeroMappings.Count == 0)
break;

ushort[] currentZeroMappings = zeroMappings.ToArray();

foreach (ushort colorIndex in currentZeroMappings)
{
RGB666Color currentColor = palette[colorIndex];
float threshold = startThreshold + (thresholdIteration * baseThreshold);

int index = 255;
if (!CompareColorsFuzzy(currentColor, black, threshold)
&& !CompareColorsFuzzy(currentColor, white, threshold))
{
index = Array.FindIndex(palettePCX, c => CompareColorsFuzzy(currentColor, c, threshold));
}

if (index != -1)
{
zeroMappings.Remove(colorIndex);
byte clampedIndex = (byte) Math.Min(palette.Length - 1, Math.Max(0, index));
Console.WriteLine($"retried mapping colorIndex {colorIndex}->{index} with color: {currentColor}->{palettePCX[index]} in iteration {thresholdIteration}");
indexMap[palIndex][colorIndex] = clampedIndex;
}
}
}

Console.WriteLine($"mapped {palette.Length - zeroMappings.Count} of {palette.Length} ({zeroMappings.Count} invalid) for palette {palIndex}");
}

return indexMap;
}

private static bool CompareColorsFuzzy(RGB666Color color0, RGB666Color color1, float threshold)
{
return Math.Abs(color0.Red - color1.Red) <= threshold
&& Math.Abs(color0.Green - color1.Green) <= threshold
&& Math.Abs(color0.Blue - color1.Blue) <= threshold;
}

/// <summary>
/// Creates a new PCX instance from image data
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Ray1MapRepacker/Importer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public class Importer(Context context)
/// </summary>
/// <param name="levFilePath"> Path to the lev file that should be replaced by the import. </param>
/// <param name="mapFileDir"> Path to the map dir, the lev data should be imported from. </param>
/// <param name="tilesetNamePrefix"> Name prefix for the tileset PCX file, to import from. </param>
/// <param name="mapFileName"> Name of the map file, the lev data should be imported from. </param>
public void ImportLevel(string levFilePath, string mapFileDir, string tilesetNamePrefix, string mapFileName)
/// <param name="tilesetNamePrefix"> Name prefix for the tileset PCX file, to import from. </param>
public void ImportLevel(string levFilePath, string mapFileDir, string mapFileName, string tilesetNamePrefix)
{
Console.WriteLine($"Starting import process for {levFilePath}");

Expand Down
16 changes: 13 additions & 3 deletions src/Ray1MapRepacker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ void ShowHelpScreen()
"Usage:\n" +
" -e <level-path> <output-path> | Exports the tileset and map from the level to the output path.\n" +
" A separate tileset PCX file is exported per available palette.\n" +
" <tile-set-path> | In addition, a tileset path to an existing PCX file can be attached\n" +
" to avoid exporting the tileset, but forcing the map to use the existing one.\n" +
" -i <level-path> <input-path> | Imports the tileset and map to the level from the input path.\n" +
" The files have to be named the same as when exporting. Only the\n" +
" first tileset PCX will be used for the tiles. The rest will only\n" +
" be used to import alternative palettes.");
}

// Parse args
if (args.Length != 3)
if (args.Length is < 3 or > 4)
{
ShowHelpScreen();
return;
Expand All @@ -43,12 +45,20 @@ void ShowHelpScreen()
// Export
if (mode == "-e")
{
new Exporter(context).ExportLevel(levFilePath, mapFileDir, TileSetFileNamePrefix, MapFileName);
if (args.Length > 3)
{
string tileSetPathPCX = args[3];
new Exporter(context).ExportLevelForceUsingTileSet(levFilePath, mapFileDir, MapFileName, tileSetPathPCX);
}
else
{
new Exporter(context).ExportLevel(levFilePath, mapFileDir, MapFileName, TileSetFileNamePrefix);
}
}
// Import
else if (mode == "-i")
{
new Importer(context).ImportLevel(levFilePath, mapFileDir, TileSetFileNamePrefix, MapFileName);
new Importer(context).ImportLevel(levFilePath, mapFileDir, MapFileName, TileSetFileNamePrefix);
}
else
{
Expand Down
Loading