unit CevoMapFile;

{***********************************************************

Project:    C-evo External Map Generator
Copyright:  1999-2026 P Blackman
License:    GPLv3+

Class for the Cevo map file format
Routines to read and write the file

***********************************************************}

interface uses MapTiles, CevoMap, IniFiles;

const
    MapHeader   = 'cEvoMap' + chr(0);

type
    TmapFile = class(TObject)

    // THeader, tTile, TmapBody used by MapCheck
    type
        // Header section of the cevo map file, 24 bytes
        THeader =
        packed record
            FormatID: packed array [1..8] of Char;
            Version,
            MaxTurn,
            LX,
            LY: DWord;
        end;

        TmapBody = array [1..lxmax*lymax] of tTile;

    var
        fHeader:   tHeader;
        fBody:     TMapBody;

    private
        fFile:     file of Byte;
        fArea:     DWord;
        fINIFile:  TINIFile;
        fLoadPath: AnsiString;
        fSavePath: AnsiString;
        fSaveFileName: String;

    protected
        procedure SetWidth(W: DWord);
        procedure SetHeight(H: DWord);
        procedure InitBody;
        procedure SaveTile(W, H: Integer; T: tTerrain; River: Boolean; Res: tResource; Special: tSpecial; Start: Boolean);
        procedure LoadTerrainFromSavedGame (const TerrainData :packed array of Byte);
        function  LoadSavedFromFile: Boolean;

    public
        constructor Create;
        destructor Destroy; override;
        function MapIndex(W, H: Integer): Integer;

        function OpenMFile(FileName: String): Boolean;
        function LoadFromFile: Boolean;
        procedure LoadMapData(MyMap: TMap);

        procedure SaveMFile(FileName: String);
        procedure SaveTileData(MyMap: TMap);

        procedure ReadINIFile;
        procedure WriteINIFile;

        property Width: DWord read fHeader.LX write SetWidth;
        property Height: DWord read fHeader.LY write SetHeight;
        property Area: DWord read fArea write fArea;
        property LoadPath: AnsiString read fLoadPath;
        property SavePath: AnsiString read fSavePath;
        property SaveFileName: String read FSaveFileName;
    end;


implementation uses Classes, SysUtils, StrUtils, Message;

const
    HeaderSize  = 24;
    RiverMask   = $80;
    BookHeader  = 'cEvoBook';


constructor Tmapfile.Create;
begin
    inherited Create;

    ReadINIFile;

    with fHeader do
    begin
        FormatID := MapHeader;
        Version  := 0;
        MaxTurn  := 0;
        LX       := 0;
        LY       := 0;
    end;
end;

destructor TmapFile.Destroy;
begin
    WriteINIFile;
    fINIFile.Free;
    inherited Destroy;
end;

function TMapFile.MapIndex(W, H: Integer): Integer;
begin
    Result := W + (H - 1) * Width; // Index starts from 1 (Map editor starts from 0)
end;


// =========================  Map File Load routines ============================

procedure TmapFile.InitBody;
var S: Integer;
begin
    for S := 1 to Area do
        with fBody[S] do
        begin
            Terrain  := 0;
            Improve  := 0;
            StartPos := 0;
            Special  := NoSpecial;
        end;
end;

procedure TmapFile.SetWidth(W: DWord);
begin
    with fHeader do
    begin
        LX    := W;
        fArea := LX * LY;
    end;
    InitBody;
end;

procedure TmapFile.SetHeight(H: DWord);
begin
    with fHeader do
    begin
        LY    := H;
        fArea := LX * LY;
    end;
    InitBody;
end;


function TmapFile.OpenMFile(FileName: String): Boolean;
begin
    try
        FileMode := 0;
        AssignFile(fFile, FileName);
        Reset(fFile);
        Result    := True;
        fLoadPath := ExtractFilePath(FileName);
    except
        ShowMessage ('Failed to open ' + FileName);
        Result := False;
        fLoadPath := '';
    end;
end;

// Import bare Terrain tiles from Saved Game
procedure TmapFile.LoadTerrainFromSavedGame (const TerrainData : packed array of Byte);
var W, H, T : Integer;
    Tile    : tTerrain;
    River   : Boolean;
    Res     : tResource;
begin
    for H := 1 to Height do
        for W := 1 to Width do
        begin
            T := W-1 + (H-1)*Width; // TerrainData indexed from zero

            if TerrainData [T] and $F <= $B then
                Tile := tTerrain (TerrainData [T] and $F)
            else
                Tile := Ocean; // Incoming Data invalid

            River := TerrainData [T] and RiverMask <> 0;

            if Tile = Ocean then
                Res := Nothing
            else
            if TerrainData [T] and $20 <> 0 then
                Res := EarlyBonus
            else
            if TerrainData [T] and $40 <> 0 then
                Res := LateBonus
            else
                Res := Nothing;

            // No information on Starts or Specials with bare Terrain data
            SaveTile (W, H, Tile, River, Res, NoSpecial, False);
        end;
end;

// Saved game files may have a bare Terrain only Map
function TmapFile.LoadSavedFromFile: Boolean;
var NumRead : Integer;
    MaxTurn, RND, GTurn, SaveMap : DWord;
    TerrainData : packed array of Byte;
begin
    Result  := False;
    NumRead := 0;

    BlockRead (fFile, MaxTurn,  4, NumRead);
    BlockRead (fFile, RND,      4, NumRead);
    BlockRead (fFile, GTurn,    4, NumRead);
    BlockRead (fFile, SaveMap,  4, NumRead);

    if NumRead < 4  then
        ShowMessage ('Invalid File (truncated!)')
    else
    if SaveMap <> $80000000 then
    begin
        Setlength (TerrainData, Area);
        BlockRead (fFile, TerrainData[0], Area, NumRead);
        if NumRead <> Area then
            ShowMessage ('Invalid Map (truncated!)')
        else
        begin
            LoadTerrainFromSavedGame (TerrainData);
            Result := True;
        end;
    end
    else
        ShowMessage ('No Map File Body');
end;

function TmapFile.LoadFromFile: Boolean;
var NumRead,
    LandMass: Integer;
begin
    Result  := False;
    NumRead := 0;
    LandMass := 0;
    BlockRead (fFile, fHeader, HeaderSize, NumRead);

    if NumRead <> HeaderSize then
        ShowMessage ('Invalid File (truncated!)')
    else
    if (fHeader.FormatID = MapHeader) or (fHeader.FormatID = BookHeader) then
    begin
        with fHeader do
            fArea := LX * LY;

        if (fHeader.FormatID = BookHeader) then
            BlockRead (fFile, LandMass, 4, NumRead);

        if LandMass <> 0 then
            Result := LoadSavedFromFile
        else
        begin
            BlockRead (fFile, fBody, Area * sizeof(tTile), NumRead);

            if NumRead <> Area * sizeof(tTile) then
                ShowMessage ('Truncated Map File Body')
            else
                Result := True;
        end;
    end
    else
        ShowMessage ('Not a valid C-evo map or book file');

    CloseFile(fFile);
end;


// Load the bits of the data structure needed for display/analysis
procedure TMapFile.LoadMapData(MyMap: TMap);
var W, H: Integer;
    Tb: Byte;
    T: tTerrain;
    B: tResource;
    S: tSpecial;
    R: Boolean;

begin
    for H := 1 to Height do
        for W := 1 to Width do
        begin
            Tb := fBody[W + (H - 1) * Width].Terrain;

            R := (Tb AND Rivermask) <> 0;
            B := tResource((Tb AND $60) DIV $20); // $40 & £20 translate to 1 & 2

            // mask out non terain resource info beyond $F Hex
            Tb := Tb AND $F;

            if Tb > Ord(High (tTerrain)) then
                T := Ocean // default for invalid value
            else
                T := tTerrain(Tb);

            S := fBody[MapIndex(W, H)].Special;

            MyMap.SetTerrain(W, H, T, R);
            MyMap.SetBonus  (W, H, B);
            MyMap.SetSpecial(W, H, S);

            if fBody[W + (H - 1) * Width].StartPos in [$20, $40] then
                MyMap.SetStartPosition(W, H);
        end;
    MyMap.GetCitySites;
end;


// =========================  Map File Save routines ============================

procedure TmapFile.SaveMFile(FileName: String);
var NumWrite: Integer;
begin
    FileMode := 2;
    AssignFile(fFile, FileName);
    ReWrite(fFile);

    NumWrite := 0;
    BlockWrite(fFile, fHeader, HeaderSize, NumWrite);
    Assert(NumWrite = HeaderSize, 'Wrong size file header');

    BlockWrite(fFile, fBody, Area * sizeof(tTile), NumWrite);
    Assert(NumWrite = Area * sizeof(tTile), 'Wrong size file body');

    CloseFile(fFile);
    fSaveFileName := ExtractFileName(FileName);
    fSavePath     := ExtractFilePath(FileName);
end;


procedure TmapFile.SaveTile(W, H: Integer; T: tTerrain; River: Boolean; Res: tResource; Special: tSpecial; Start: Boolean);
var temp: Byte;
begin
    if River AND (T <> Ocean) AND (T <> Coast) then
        Temp := Ord(T) OR RiverMask
    else
        temp := Ord(T);

    Assert(NOT ((t = Ocean) AND (Res > Nothing)), 'Resource in Ocean square ' + IntToStr(W) + IntToStr(H));

    if Ord(Special) = 0 then
        Special := NoSpecial;
    fBody[MapIndex(W, H)].Special := Special;

    if (Res <> Nothing) and (Special = NoSpecial) then
        // Bonus resources, bits are $20 & $40 }
        fBody[MapIndex(W, H)].Terrain := Temp OR (Ord(Res) SHL 5)
    else
        fBody[MapIndex(W, H)].Terrain := Temp;

    with fBody[MapIndex(W, H)] do
        // Check Terrain value valid
        Assert((Terrain AND $0F <= $0B), 'Terrain value invalid ' + IntToHex (Terrain AND $0F));

    with fBody[MapIndex(W, H)] do
        // Check Resource value valid
        Assert((Terrain AND $E0) IN [$00, $20, $40, $80, $A0, $C0], 'Resource value invalid '+ IntToHex (Terrain AND $E0));

    if Start then
        fBody[MapIndex(W, H)].StartPos := $40;
end;


procedure TMapFile.SaveTileData(MyMap: TMap);
var W, H: Integer;

begin
    //MyMap.PlaceStartPositions;

    for H := 1 to Height do
        for W := 1 to Width do
            SaveTile(W, H, MyMap.GetTerrain(W, H),
                MyMap.Tiles[W, H].River,
                MyMap.GetBonus(W, H),
                MyMap.GetSpecial(W, H),
                MyMap.StartPosition(W, H));
end;


// =========================  INI File routines ===========================
procedure TmapFile.ReadINIFile;
var MyPath,
    inifilename: String;
begin
    MyPath      := GetAppConfigDir(False);
    inifilename := MyPath + 'CivMapFile.INI';
    fIniFile    := TINIFile.Create(inifilename);
    fLoadPath   := fIniFile.ReadString('MAPPATHS', 'LOAD', MyPath);
    RemoveTrailingChars (fLoadPath, ['/','\']);

    fSavePath   := fIniFile.ReadString ('MAPPATHS', 'SAVE', MyPath);

    // No default for file name
    fSaveFileName := fIniFile.ReadString('MAPPATHS', 'NAME', '');
end;

procedure TmapFile.WriteINIFile;
begin
    fIniFile.WriteString('MAPPATHS', 'LOAD', fLoadPath);
    fIniFile.WriteString('MAPPATHS', 'SAVE', fSavePath);
    fIniFile.WriteString('MAPPATHS', 'NAME', fSaveFileName);
end;


initialization

    Assert (Sizeof (tMapFile.tHeader) = headerSize);
end.
