unit CevoMapFile;

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

Project:    C-evo External Map Generator
Copyright:  1999-2024 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;

    private const

    private
        fFile:     file of Byte;
        fArea:     DWord;
        fINIFile:  TINIFile;
        fLoadPath: String;
        fSavePath: String;
        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);

    public
        fHeader:   tHeader;
        fBody:     TMapBody;

        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: String read fLoadPath;
        property SavePath: String read fSavePath;
        property SaveFileName: String read FSaveFileName;
    end;


implementation uses Classes, SysUtils, Message;

const
    NullSpecial = $78; // Value found in existing maps
    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  := 0;
        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;

function TmapFile.LoadFromFile: Boolean;
var NumRead: Integer;
begin
    Result  := False;
    NumRead := 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;

        BlockRead(fFile, fBody, Area * sizeof(tTile), NumRead);

        if NumRead <> Area * sizeof(tTile) then
            ShowMessage('Truncated Map File Body')
        else
            Result := True;
    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;

            // mask out non terain resouirce info beyond $F Hex
            T := tTerrain(Tb AND $F);

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

            Tb := fBody[MapIndex(W, H)].Special;
            if (Tb = 0) Or (Tb = NullSpecial) then
                S := NoSpecial
            else
                S := tSpecial((Tb+1) DIV 2);

            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 Res <> Nothing then
    begin
        // Bonus resources, bits are $20 & $40 }
        fBody[MapIndex(W, H)].Terrain := Temp OR (Ord(Res) SHL 5);
        fBody[MapIndex(W, H)].Special := NullSpecial;
    end
    else
    if Special <> NoSpecial then
    begin
        // DeadLands & Specials
        fBody[MapIndex(W, H)].Terrain := Temp;
        fBody[MapIndex(W, H)].Special := 2*Ord(Special) -1;
    end
    else
    begin
        fBody[MapIndex(W, H)].Terrain := Temp;
        fBody[MapIndex(W, H)].Special := NullSpecial;
    end;

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

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

    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);
    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.
