Kenpachi
Core Developers
Hi there. Long time no see. 
I'm currently working on a small server/client application just as a finger exercise for me. Therefor I'm using RO files and stumbled over the .gat cell types and their relation with the water level in .rsw files.
According to *Athena and some other projects there should be just a few possible types:
Additionally I found different ways to check if a cell is under water or not:
So I did a little testing a wrote a tool to collect all combinations of cell types and water level checks in every map. This is the result:

What? Now I really got confused. Even if only the *Athena way (IsUnderWaterUpperLeft) to check cell types is payed attention to, there is a significant difference to what one would expect:

See? Maybe I'm using wrong algorithms, but they seem to be correct:
So could anybody please tell me, how AEGIS handles this? Maybe Hercules can profit by this, too.
~Kenpachi
I'm currently working on a small server/client application just as a finger exercise for me. Therefor I'm using RO files and stumbled over the .gat cell types and their relation with the water level in .rsw files.
According to *Athena and some other projects there should be just a few possible types:
// from Hercules\src\map\map.c - map_gat2cell()
case 0: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // walkable ground
case 1: cell.walkable = 0; cell.shootable = 0; cell.water = 0; break; // non-walkable ground
case 2: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
case 3: cell.walkable = 1; cell.shootable = 1; cell.water = 1; break; // walkable water
case 4: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
case 5: cell.walkable = 0; cell.shootable = 1; cell.water = 0; break; // gap (snipable)
case 6: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
// from Hercules\src\map\map.c - map_cell2gat
if( cell.walkable == 1 && cell.shootable == 1 && cell.water == 0 ) return 0;
if( cell.walkable == 0 && cell.shootable == 0 && cell.water == 0 ) return 1;
if( cell.walkable == 1 && cell.shootable == 1 && cell.water == 1 ) return 3;
if( cell.walkable == 0 && cell.shootable == 1 && cell.water == 0 ) return 5;
case 0: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // walkable ground
case 1: cell.walkable = 0; cell.shootable = 0; cell.water = 0; break; // non-walkable ground
case 2: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
case 3: cell.walkable = 1; cell.shootable = 1; cell.water = 1; break; // walkable water
case 4: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
case 5: cell.walkable = 0; cell.shootable = 1; cell.water = 0; break; // gap (snipable)
case 6: cell.walkable = 1; cell.shootable = 1; cell.water = 0; break; // ???
// from Hercules\src\map\map.c - map_cell2gat
if( cell.walkable == 1 && cell.shootable == 1 && cell.water == 0 ) return 0;
if( cell.walkable == 0 && cell.shootable == 0 && cell.water == 0 ) return 1;
if( cell.walkable == 1 && cell.shootable == 1 && cell.water == 1 ) return 3;
if( cell.walkable == 0 && cell.shootable == 1 && cell.water == 0 ) return 5;
Additionally I found different ways to check if a cell is under water or not:
// *Athena and others
if(gat_cell.upperLeftHeight > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
// OpenKore and others
float averageDepth = (gat_block.upperLeftHeight + gat_block.upperRightHeight + gat_block.lowerLeftHeight + gat_block.lowerRightHeight) / 4;
if(averageDepth > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
// Me for testing purposes
if(gat_cell.upperLeftHeight > WATER_LEVEL_FROM_RSW || gat_cell.upperRightHeight > WATER_LEVEL_FROM_RSW ||
gat_cell.lowerLeftHeight > WATER_LEVEL_FROM_RSW || gat_cell.lowerRightHeight > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
if(gat_cell.upperLeftHeight > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
// OpenKore and others
float averageDepth = (gat_block.upperLeftHeight + gat_block.upperRightHeight + gat_block.lowerLeftHeight + gat_block.lowerRightHeight) / 4;
if(averageDepth > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
// Me for testing purposes
if(gat_cell.upperLeftHeight > WATER_LEVEL_FROM_RSW || gat_cell.upperRightHeight > WATER_LEVEL_FROM_RSW ||
gat_cell.lowerLeftHeight > WATER_LEVEL_FROM_RSW || gat_cell.lowerRightHeight > WATER_LEVEL_FROM_RSW)
// cell is under water
else
// cell is not under water
So I did a little testing a wrote a tool to collect all combinations of cell types and water level checks in every map. This is the result:

What? Now I really got confused. Even if only the *Athena way (IsUnderWaterUpperLeft) to check cell types is payed attention to, there is a significant difference to what one would expect:

See? Maybe I'm using wrong algorithms, but they seem to be correct:
private void ReadFile(string filePath)
{
_currentFileNumber++;
_currentFile = filePath;
_worker.ReportProgress(default);
var waterLevel = GetWaterLevel(filePath.Replace(".gat", ".rsw"));
using var reader = new BinaryReader(File.OpenRead(filePath));
reader.ReadBytes(6); // Skip magic header
var width = reader.ReadInt32();
var height = reader.ReadInt32();
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var ul = reader.ReadSingle();
var ur = reader.ReadSingle();
var ll = reader.ReadSingle();
var lr = reader.ReadSingle();
var type = reader.ReadByte();
reader.ReadBytes(3); // Skip unknown bytes
var cellType = new CellType {Type = type, MapData = $"{Path.GetFileName(filePath),-15} [x={x + 1} - y={y + 1}]", Count = 1};
if (waterLevel == 0)
cellType.IsUnderWaterAny = cellType.IsUnderWaterAverage = cellType.IsUnderWaterUpperLeft = false;
else
{
cellType.IsUnderWaterAny = ul > waterLevel || ur > waterLevel || ll > waterLevel || lr > waterLevel;
cellType.IsUnderWaterAverage = (ul + ur + ll + lr) / 4 > waterLevel;
cellType.IsUnderWaterUpperLeft = ul > waterLevel;
}
bool Match(CellType o) => o.Type == cellType.Type && o.IsUnderWaterAny == cellType.IsUnderWaterAny &&
o.IsUnderWaterAverage == cellType.IsUnderWaterAverage &&
o.IsUnderWaterUpperLeft == cellType.IsUnderWaterUpperLeft;
if (!_types.Exists(Match))
_types.Add(cellType);
else
_types[_types.FindIndex(Match)].Count++;
}
}
}
private static float GetWaterLevel(string filePath)
{
if (!File.Exists(filePath)) return 0;
using var reader = new BinaryReader(File.OpenRead(filePath));
reader.ReadBytes(4); // Skip magic header
var majorVersion = reader.ReadByte();
var minorVersion = reader.ReadByte();
reader.ReadBytes((majorVersion > 1 || (majorVersion == 1 && minorVersion >= 4)) ? 160 : 120); // Skip unused bytes
return reader.ReadSingle();
}
{
_currentFileNumber++;
_currentFile = filePath;
_worker.ReportProgress(default);
var waterLevel = GetWaterLevel(filePath.Replace(".gat", ".rsw"));
using var reader = new BinaryReader(File.OpenRead(filePath));
reader.ReadBytes(6); // Skip magic header
var width = reader.ReadInt32();
var height = reader.ReadInt32();
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var ul = reader.ReadSingle();
var ur = reader.ReadSingle();
var ll = reader.ReadSingle();
var lr = reader.ReadSingle();
var type = reader.ReadByte();
reader.ReadBytes(3); // Skip unknown bytes
var cellType = new CellType {Type = type, MapData = $"{Path.GetFileName(filePath),-15} [x={x + 1} - y={y + 1}]", Count = 1};
if (waterLevel == 0)
cellType.IsUnderWaterAny = cellType.IsUnderWaterAverage = cellType.IsUnderWaterUpperLeft = false;
else
{
cellType.IsUnderWaterAny = ul > waterLevel || ur > waterLevel || ll > waterLevel || lr > waterLevel;
cellType.IsUnderWaterAverage = (ul + ur + ll + lr) / 4 > waterLevel;
cellType.IsUnderWaterUpperLeft = ul > waterLevel;
}
bool Match(CellType o) => o.Type == cellType.Type && o.IsUnderWaterAny == cellType.IsUnderWaterAny &&
o.IsUnderWaterAverage == cellType.IsUnderWaterAverage &&
o.IsUnderWaterUpperLeft == cellType.IsUnderWaterUpperLeft;
if (!_types.Exists(Match))
_types.Add(cellType);
else
_types[_types.FindIndex(Match)].Count++;
}
}
}
private static float GetWaterLevel(string filePath)
{
if (!File.Exists(filePath)) return 0;
using var reader = new BinaryReader(File.OpenRead(filePath));
reader.ReadBytes(4); // Skip magic header
var majorVersion = reader.ReadByte();
var minorVersion = reader.ReadByte();
reader.ReadBytes((majorVersion > 1 || (majorVersion == 1 && minorVersion >= 4)) ? 160 : 120); // Skip unused bytes
return reader.ReadSingle();
}
So could anybody please tell me, how AEGIS handles this? Maybe Hercules can profit by this, too.
~Kenpachi






