Files
kam-mobile/vendor/kam_remake/src/mission/KM_Maps.pas
T

2024 lines
54 KiB
ObjectPascal
Raw Normal View History

2026-09-12 16:50:30 +05:00
unit KM_Maps;
{$I KaM_Remake.inc}
interface
uses
Classes, SyncObjs,
KM_MapTypes,
KM_CommonTypes, KM_CommonClasses, KM_Defaults, KM_Pics, KM_ResTexts, KM_Points;
type
TKMapsSortMethod = (
smByFavouriteAsc, smByFavouriteDesc,
smByNameAsc, smByNameDesc,
smBySizeAsc, smBySizeDesc,
smByPlayersAsc, smByPlayersDesc,
smByHumanPlayersAsc, smByHumanPlayersDesc,
smByHumanPlayersMPAsc, smByHumanPlayersMPDesc,
smByMissionModeAsc, smByMissionModeDesc);
TKMMapInfo = class;
TKMapEvent = procedure (aMap: TKMMapInfo) of object;
TKMMapInfoAmount = (iaBase, iaExtra);
TKMMapGoalInfo = packed record
Cond: TKMGoalCondition;
Play: TKMHandID;
Stat: TKMGoalStatus;
end;
// Additional map data from TXT file
TKMMapTxtInfo = class
private
// Saved
// Desc fixed string in the .txt file. Should be saved
fSmallDesc: UnicodeString;
fBigDesc: UnicodeString;
// Desc LibxID. Should be saved
fSmallDescLibx: Integer;
fBigDescLibx: Integer;
// Not saved
// Translated text, which we load by LibxID. Should not be saved
fSmallDescTranslated: UnicodeString;
fBigDescTranslated: UnicodeString;
fBlockColorSelection: Boolean;
function IsEmpty: Boolean;
function GetBlockColorSelection: Boolean;
procedure NormalizeDesc;
procedure SetSmallDesc(const aSmallDesc: UnicodeString);
procedure SetBigDesc(const aBigDesc: UnicodeString);
function GetSmallDescSanitized: UnicodeString;
function GetBigDescToDisplay: UnicodeString;
public
Author: UnicodeString;
Version: UnicodeString;
IsCoop: Boolean; // Some multiplayer missions are defined as coop
IsSpecial: Boolean; // Some missions are defined as special (e.g. tower defence, quest, etc.)
IsRMG: Boolean; // Missions that were generated via Random Map Generator
IsPlayableAsSP: Boolean; // Is MP map playable as SP map ?
DifficultyLevels: TKMMissionDifficultySet;
BlockTeamSelection: Boolean;
BlockPeacetime: Boolean;
BlockFullMapPreview: Boolean;
constructor Create;
// SmallDesc can be set as text or Libx index by the mapmaker
// Since the data is in the text, we can't forbid the mapmaker to use EOLs and other unsupported symbols
// Hence, read and let edit what is there, but sanitize for the display in the SP table
property SmallDesc: UnicodeString read fSmallDesc write SetSmallDesc;
property SmallDescLibx: Integer read fSmallDescLibx;
procedure SetSmallDescLibxAndTranslation(aSmallDescLibx: Integer; aTranslation: UnicodeString);
property SmallDescSanitized: UnicodeString read GetSmallDescSanitized;
property BigDesc: UnicodeString write SetBigDesc;
property BigDescLibx: Integer read fBigDescLibx;
procedure SetBigDescLibxAndTranslation(aBigDescLibx: Integer; aTranslation: UnicodeString);
property BigDescToDisplay: UnicodeString read GetBigDescToDisplay;
procedure Load(LoadStream: TKMemoryStream);
procedure Save(SaveStream: TKMemoryStream);
function IsSmallDescLibxSet: Boolean;
function IsBigDescLibxSet: Boolean;
function CanAddDefaultGoals: Boolean;
procedure ResetInfo;
procedure SaveTXTInfo(const aFilePath: String);
procedure LoadTXTInfo(const aFilePath: String);
function HasDifficultyLevels: Boolean;
property BlockColorSelection: Boolean read GetBlockColorSelection write fBlockColorSelection;
end;
TKMMapTxtInfoArray = array of TKMMapTxtInfo;
TKMMapInfo = class
private
fDir: String;
fName: UnicodeString; //without extension
fCRC: Cardinal;
fDatCRC: Cardinal; //Used to speed up scanning
fMapAndDatCRC: Cardinal; //Used to determine map by its .map + .dat files, ignoring other map data (.txt and .script)
fVersion: AnsiString; //Savegame version, yet unused in maps, they always have actual version
fInfoAmount: TKMMapInfoAmount;
fKind: TKMMapKind; // SP / MP / DL
fTxtInfo: TKMMapTxtInfo;
fSize: TKMMapSize;
fSizeText: String;
fCustomScriptParams: TKMCustomScriptParamDataArray;
procedure ResetInfo;
procedure LoadFromStreamObj(aStreamObj: TObject; const aPath: UnicodeString);
procedure LoadFromFile(const aPath: UnicodeString);
procedure SaveToStreamObj(aStreamObj: TObject; const aPath: UnicodeString);
procedure SaveToFile(const aPath: UnicodeString);
function GetSize: TKMMapSize;
function GetSizeText: String;
function GetFavouriteMapPic: TKMPic;
function GetCanBeHumanCount: Byte;
function GetCanBeOnlyHumanCount: Byte;
function GetCanBeAICount: Byte;
function GetCanBeOnlyAICount: Byte;
function GetCanBeHumanAndAICount: Byte;
function GetBigDesc: UnicodeString;
function GetTxtInfo: TKMMapTxtInfo;
function GetDimentions: TKMPoint;
constructor Create; overload;
function GetAICanBeOnlyClassic(aIndex: Integer): Boolean;
function GetAICanBeOnlyAdvanced(aIndex: Integer): Boolean;
public
MapSizeX, MapSizeY: Integer;
MissionMode: TKMissionMode;
LocCount: Byte;
CanBeHuman: array [0..MAX_HANDS-1] of Boolean;
CanBeClassicAI: array [0..MAX_HANDS-1] of Boolean;
CanBeAdvancedAI: array [0..MAX_HANDS-1] of Boolean;
DefaultHuman: TKMHandID;
GoalsVictoryCount, GoalsSurviveCount: array [0..MAX_HANDS-1] of Byte;
GoalsVictory: array [0..MAX_HANDS-1] of array of TKMMapGoalInfo;
GoalsSurvive: array [0..MAX_HANDS-1] of array of TKMMapGoalInfo;
Alliances: array [0..MAX_HANDS-1, 0..MAX_HANDS-1] of TKMAllianceType;
FlagColors: array [0..MAX_HANDS-1] of Cardinal;
IsFavourite: Boolean;
class function CreateDummy: TKMMapInfo;
constructor Create(const aMapName: string; aStrictParsing: Boolean; aMapKind: TKMMapKind; aSilent: Boolean = False); overload;
constructor Create(const aDir, aMapName: string; aStrictParsing: Boolean; aMapKind: TKMMapKind = mkUnknown; aSilent: Boolean = False); overload;
destructor Destroy; override;
procedure AddGoal(aGoalType: TKMGoalType; aPlayer: TKMHandID; aCondition: TKMGoalCondition; aStatus: TKMGoalStatus; aPlayerIndex: TKMHandID);
procedure LoadExtra(aAddDefailtGoals: Boolean = False);
property TxtInfo: TKMMapTxtInfo read GetTxtInfo;
property BigDesc: UnicodeString read GetBigDesc;
property InfoAmount: TKMMapInfoAmount read fInfoAmount;
property Dir: string read fDir;
property Kind: TKMMapKind read fKind;
property Name: UnicodeString read fName;
function FullPath(const aExt: string): string;
function HumanUsableLocs: TKMHandIDArray;
function AIUsableLocs: TKMHandIDArray;
function AdvancedAIUsableLocs: TKMHandIDArray;
function FixedLocsColors: TKMCardinalArray;
function AIOnlyLocsColors: TKMCardinalArray;
function IsOnlyAILoc(aLoc: Integer): Boolean;
property CRC: Cardinal read fCRC;
property MapAndDatCRC : Cardinal read fMapAndDatCRC;
function LocationName(aIndex: TKMHandID): string;
property Size: TKMMapSize read GetSize;
property SizeText: string read GetSizeText;
property Dimensions: TKMPoint read GetDimentions;
function IsValid: Boolean;
function HumanPlayerCount: Byte;
function HumanPlayerCountMP: Byte;
function AIOnlyLocCount: Byte;
function FileNameWithoutHash: UnicodeString;
function HasReadme: Boolean;
function DetermineReadmeFilePath: String;
function GetLobbyColor: Cardinal;
function IsFilenameEndMatchHash: Boolean;
function IsSinglePlayer: Boolean;
function IsMultiPlayer: Boolean;
function IsPlayableForSP: Boolean;
function IsSinglePlayerKind: Boolean;
function IsMultiPlayerKind: Boolean;
function IsDownloadedKind: Boolean;
function IsBuildingMission: Boolean;
function IsFightingMission: Boolean;
property FavouriteMapPic: TKMPic read GetFavouriteMapPic;
property AICanBeOnlyClassic[aIndex: Integer]: Boolean read GetAICanBeOnlyClassic;
property AICanBeOnlyAdvanced[aIndex: Integer]: Boolean read GetAICanBeOnlyAdvanced;
property CanBeHumanCount: Byte read GetCanBeHumanCount;
property CanBeOnlyHumanCount: Byte read GetCanBeOnlyHumanCount;
property CanBeAICount: Byte read GetCanBeAICount;
property CanBeOnlyAICount: Byte read GetCanBeOnlyAICount;
property CanBeHumanAndAICount: Byte read GetCanBeHumanAndAICount;
function HasDifferentAITypes(aExceptLoc: TKMHandID = -1): Boolean;
end;
TTCustomMapsScanner = class(TThread)
private
fMapKinds: TKMMapKindSet;
fOnComplete: TNotifyEvent;
procedure ProcessMap(const aPath: UnicodeString; aKind: TKMMapKind); virtual; abstract;
public
constructor Create(aMapKinds: TKMMapKindSet; aOnComplete: TNotifyEvent = nil);
procedure Execute; override;
end;
TTMapsScanner = class(TTCustomMapsScanner)
private
fOnMapAdd: TKMapEvent;
fOnMapAddDone: TNotifyEvent;
procedure ProcessMap(const aPath: UnicodeString; aKind: TKMMapKind); override;
public
constructor Create(aMapFolders: TKMMapKindSet; aOnMapAdd: TKMapEvent; aOnMapAddDone, aOnTerminate: TNotifyEvent; aOnComplete: TNotifyEvent = nil);
end;
TTMapsCacheUpdater = class(TTCustomMapsScanner)
private
fIsStopped: Boolean;
procedure ProcessMap(const aPath: UnicodeString; aKind: TKMMapKind); override;
public
procedure Stop;
constructor Create(aMapFolders: TKMMapKindSet);
end;
TKMapsCollection = class
private
fCount: Integer;
fMaps: array of TKMMapInfo;
fMapFolders: TKMMapKindSet;
fSortMethod: TKMapsSortMethod;
fDoSortWithFavourites: Boolean;
fCriticalSection: TCriticalSection;
fScanner: TTMapsScanner;
fScanning: Boolean; //Flag if scan is in progress
fUpdateNeeded: Boolean;
fOnRefresh: TNotifyEvent;
fOnTerminate: TNotifyEvent;
fOnComplete: TNotifyEvent;
procedure Clear;
procedure MapAdd(aMap: TKMMapInfo);
procedure MapAddDone(Sender: TObject);
procedure ScanTerminate(Sender: TObject);
procedure ScanComplete(Sender: TObject);
procedure DoSort;
function GetMap(aIndex: Integer): TKMMapInfo;
public
constructor Create(aKindSet: TKMMapKindSet; aSortMethod: TKMapsSortMethod = smByNameDesc; aDoSortWithFavourites: Boolean = False); overload;
constructor Create(aKind: TKMMapKind; aSortMethod: TKMapsSortMethod = smByNameDesc; aDoSortWithFavourites: Boolean = False); overload;
destructor Destroy; override;
property Count: Integer read fCount;
property Maps[aIndex: Integer]: TKMMapInfo read GetMap; default;
procedure Lock;
procedure Unlock;
class function FullPath(const aDirName, aFileName, aExt: string; aMapKind: TKMMapKind): string; overload;
class function FullPath(const aName, aExt: string; aMultiplayer: Boolean): string; overload;
class function FullPath(const aName, aExt: string; aMapKind: TKMMapKind): string; overload;
class function FullPath(const aName, aExt: string; aMapKind: TKMMapKind; aCRC: Cardinal): string; overload;
// class function GuessMPPath(const aName, aExt: string; aCRC: Cardinal): string;
class procedure GetAllMapPaths(const aExeDir: string; aList: TStringList);
class function GetMapCRC(const aMapPath: string): Cardinal;
procedure Refresh(aOnRefresh: TNotifyEvent; aOnTerminate: TNotifyEvent = nil;aOnComplete: TNotifyEvent = nil);
procedure TerminateScan;
procedure Sort(aSortMethod: TKMapsSortMethod; aOnSortComplete: TNotifyEvent);
property SortMethod: TKMapsSortMethod read fSortMethod; //Read-only because we should not change it while Refreshing
function Contains(const aNewName: UnicodeString): Boolean;
procedure RenameMap(aIndex: Integer; const aName: UnicodeString);
procedure DeleteMap(aIndex: Integer);
procedure MoveMap(aIndex: Integer; const aName: UnicodeString; aMapKind: TKMMapKind);
procedure UpdateState;
end;
implementation
uses
SysUtils, StrUtils, TypInfo, Math,
KromUtils,
KM_GameSettings, KM_FileIO,
KM_MissionScript_Info,
KM_ScriptPreProcessor, KM_ScriptFilesCollection,
KM_ResLocales, KM_ResTypes,
KM_CommonUtils, KM_Log, KM_MapUtils, KM_Utils;
const
MAP_TXT_INFO_MARKER = 'MapTxtInfo';
{ TKMMapInfo }
class function TKMMapInfo.CreateDummy: TKMMapInfo;
begin
Result := Create;
end;
// Dummy instance, used to fill fields
constructor TKMMapInfo.Create;
begin
inherited;
end;
constructor TKMMapInfo.Create(const aMapName: string; aStrictParsing: Boolean; aMapKind: TKMMapKind; aSilent: Boolean = False);
begin
Assert(aMapKind <> mkUnknown); // Do not allow to create 'unknown' maps with this constructor
Create(ExeDir + MAP_FOLDER_NAME[aMapKind] + PathDelim + aMapName + PathDelim, aMapName, aStrictParsing, aMapKind, aSilent);
end;
constructor TKMMapInfo.Create(const aDir, aMapName: string; aStrictParsing: Boolean; aMapKind: TKMMapKind = mkUnknown; aSilent: Boolean = False);
function GetLIBXCRC(const aSearchFile: UnicodeString): Cardinal;
var
searchRec: TSearchRec;
begin
Result := 0;
FindFirst(aSearchFile, faAnyFile - faDirectory, searchRec);
try
repeat
if (searchRec.Name <> '.') and (searchRec.Name <> '..') then
Result := Result xor Adler32CRC(ExtractFilePath(aSearchFile) + searchRec.Name);
until (FindNext(searchRec) <> 0);
finally
FindClose(searchRec);
end;
end;
var
I: Integer;
datFile, mapFile, scriptFile, txtFile, libxFiles: string;
datCRC, mapCRC, othersCRC: Cardinal;
missionParser: TKMMissionParserInfo;
scriptPreProcessor: TKMScriptPreProcessor;
scriptFiles: TKMScriptFilesCollection;
CSP: TKMCustomScriptParam;
begin
inherited Create;
fDir := aDir;
fName := aMapName;
fKind := aMapKind;
fTxtInfo := TKMMapTxtInfo.Create;
for CSP := Low(TKMCustomScriptParam) to High(TKMCustomScriptParam) do
begin
fCustomScriptParams[CSP].Added := False;
fCustomScriptParams[CSP].Data := '';
end;
datFile := fDir + fName + '.dat';
mapFile := fDir + fName + '.map';
scriptFile := fDir + fName + EXT_FILE_SCRIPT_DOT; //Needed for CRC
txtFile := fDir + fName + '.txt'; //Needed for CRC
libxFiles := fDir + fName + '.*.libx'; //Needed for CRC
fSizeText := ''; //Lazy initialization
if not FileExists(datFile) then Exit;
//Try loading info from cache, since map scanning is rather slow
LoadFromFile(fDir + fName + '.mi'); //Data will be empty if failed
//We will scan map once again if anything has changed
//In SP mode (non-strict) we check DAT CRC and version, that is enough
//In MP mode (strict) we also need exact CRCs to match maps between players
datCRC := Adler32CRC(datFile);
//.map file CRC is the slowest, so only calculate it if necessary
othersCRC := 0; //Supresses incorrect warning by Delphi
mapCRC := 0;
if aStrictParsing then
begin
mapCRC := Adler32CRC(mapFile);
othersCRC := mapCRC xor Adler32CRC(txtFile) xor GetLIBXCRC(libxFiles);
fMapAndDatCRC := datCRC xor mapCRC;
//Add main script CRC and all included scripts CRC
if FileExists(scriptFile) then
begin
othersCRC := othersCRC xor Adler32CRC(scriptFile);
scriptPreProcessor := TKMScriptPreProcessor.Create(aSilent);
try
if scriptPreProcessor.PreProcessFile(scriptFile) then
begin
//Copy custom script params
for CSP := Low(TKMCustomScriptParam) to High(TKMCustomScriptParam) do
fCustomScriptParams[CSP] := scriptPreProcessor.CustomScriptParams[CSP];
scriptFiles := scriptPreProcessor.ScriptFilesInfo;
for I := 0 to scriptFiles.IncludedCount - 1 do
othersCRC := othersCRC xor Adler32CRC(scriptFiles[I].FullFilePath);
end;
finally
scriptPreProcessor.Free;
end;
end;
end;
//Does the map need to be fully rescanned? (.mi cache is outdated?)
if (fVersion <> GAME_REVISION) or
(fDatCRC <> datCRC) or //In non-strict mode only DAT CRC matters (SP)
(aStrictParsing and (fCRC <> datCRC xor othersCRC)) //In strict mode we check all CRCs (MP)
then
begin
//Calculate OthersCRC if it wasn't calculated before
if not aStrictParsing then
begin
mapCRC := Adler32CRC(mapFile);
othersCRC := mapCRC xor Adler32CRC(scriptFile) xor Adler32CRC(txtFile);
end;
fCRC := datCRC xor othersCRC;
fDatCRC := datCRC;
fMapAndDatCRC := datCRC xor mapCRC;
fVersion := GAME_REVISION;
//First reset everything because e.g. CanBeHuman is assumed False by default and set True when we encounter SET_USER_PLAYER
ResetInfo;
missionParser := TKMMissionParserInfo.Create;
try
//Fill Self properties with MissionParser
missionParser.LoadMission(datFile, Self, pmBase);
finally
missionParser.Free;
end;
//Load additional text info
fTxtInfo.LoadTXTInfo(fDir + fName + '.txt');
if gGameSettings = nil // In case we are closing app and settings object is already destroyed
then Exit;
IsFavourite := gGameSettings.FavouriteMaps.Contains(fMapAndDatCRC);
SaveToFile(fDir + fName + '.mi'); //Save new cache file
end;
fInfoAmount := iaBase;
end;
destructor TKMMapInfo.Destroy;
begin
FreeAndNil(fTxtInfo);
inherited;
end;
procedure TKMMapInfo.AddGoal(aGoalType: TKMGoalType; aPlayer: TKMHandID; aCondition: TKMGoalCondition; aStatus: TKMGoalStatus; aPlayerIndex: TKMHandID);
var
G: TKMMapGoalInfo;
begin
G.Cond := aCondition;
G.Play := aPlayerIndex;
G.Stat := aStatus;
case aGoalType of
gltVictory: begin
SetLength(GoalsVictory[aPlayer], GoalsVictoryCount[aPlayer] + 1);
GoalsVictory[aPlayer, GoalsVictoryCount[aPlayer]] := G;
Inc(GoalsVictoryCount[aPlayer]);
end;
gltSurvive: begin
SetLength(GoalsSurvive[aPlayer], GoalsSurviveCount[aPlayer] + 1);
GoalsSurvive[aPlayer, GoalsSurviveCount[aPlayer]] := G;
Inc(GoalsSurviveCount[aPlayer]);
end;
end;
end;
function TKMMapInfo.FullPath(const aExt: string): string;
begin
Result := fDir + fName + aExt;
end;
function TKMMapInfo.HumanUsableLocs: TKMHandIDArray;
var
I: Integer;
begin
SetLength(Result, 0);
for I := 0 to MAX_HANDS - 1 do
if CanBeHuman[I] then
begin
SetLength(Result, Length(Result)+1);
Result[Length(Result)-1] := I;
end;
end;
function TKMMapInfo.AIUsableLocs: TKMHandIDArray;
var
I: Integer;
begin
SetLength(Result, 0);
for I := 0 to MAX_HANDS - 1 do
if CanBeClassicAI[I] then
begin
SetLength(Result, Length(Result)+1);
Result[Length(Result)-1] := I;
end;
end;
function TKMMapInfo.AdvancedAIUsableLocs: TKMHandIDArray;
var
I: Integer;
begin
SetLength(Result, 0);
for I := 0 to MAX_HANDS - 1 do
if CanBeAdvancedAI[I] then
begin
SetLength(Result, Length(Result)+1);
Result[Length(Result)-1] := I;
end;
end;
function TKMMapInfo.IsOnlyAILoc(aLoc: Integer): Boolean;
begin
Assert(aLoc < MAX_HANDS);
Result := not CanBeHuman[aLoc] and (CanBeClassicAI[aLoc] or CanBeAdvancedAI[aLoc]);
end;
// Color is fixed for loc if map has BlockColorSelection attribute
// or if its only AI loc, no available for player
function TKMMapInfo.FixedLocsColors: TKMCardinalArray;
var
I: Integer;
begin
SetLength(Result, 0);
if Self = nil then Exit;
SetLength(Result, LocCount);
for I := 0 to LocCount - 1 do
if TxtInfo.BlockColorSelection or IsOnlyAILoc(I) then
Result[I] := FlagColors[I]
else
Result[I] := 0;
end;
// Colors that are used by only AI locs
function TKMMapInfo.AIOnlyLocsColors: TKMCardinalArray;
var
I, K: Integer;
begin
SetLength(Result, 0);
if Self = nil then Exit;
SetLength(Result, LocCount);
K := 0;
for I := 0 to LocCount - 1 do
if IsOnlyAILoc(I) then
begin
Result[K] := FlagColors[I];
Inc(K);
end;
SetLength(Result, K);
end;
function TKMMapInfo.LocationName(aIndex: TKMHandID): string;
begin
Result := Format(gResTexts[TX_LOBBY_LOCATION_X], [aIndex + 1]);
end;
function TKMMapInfo.GetSize: TKMMapSize;
begin
if fSize = msNone then
fSize := MapSizeIndex(MapSizeX, MapSizeY);
Result := fSize;
end;
function TKMMapInfo.GetSizeText: string;
begin
if fSizeText = '' then
fSizeText := MapSizeText(MapSizeX, MapSizeY);
Result := fSizeText;
end;
function TKMMapInfo.GetTxtInfo: TKMMapTxtInfo;
begin
if Self = nil then Exit(nil);
Result := fTxtInfo;
end;
//Load additional information for map that is not in main SP list
procedure TKMMapInfo.LoadExtra(aAddDefailtGoals: Boolean = False);
var
I, K: Integer;
datFile: string;
missionParser: TKMMissionParserInfo;
gc: TKMGoalCondition;
begin
//Do not append Extra info twice
if fInfoAmount = iaExtra then Exit;
//First reset everything because e.g. CanBeHuman is assumed False by default and set True when we encounter SET_USER_PLAYER
ResetInfo;
datFile := fDir + fName + '.dat';
missionParser := TKMMissionParserInfo.Create;
try
//Fill Self properties with MissionParser
missionParser.LoadMission(datFile, Self, pmExtra);
finally
missionParser.Free;
end;
if IsFightingMission then
fTxtInfo.BlockPeacetime := True;
fTxtInfo.LoadTXTInfo(fDir + fName + '.txt');
fInfoAmount := iaExtra;
if not aAddDefailtGoals then Exit;
// Add Default goals for a certain maps
if fTxtInfo.IsPlayableAsSP and fTxtInfo.CanAddDefaultGoals then
begin
if IsBuildingMission then
gc := gcBuildings
else
gc := gcTroops;
for I := 0 to LocCount - 1 do
begin
AddGoal(gltSurvive, I, gc, gsTrue, I);
for K := 0 to LocCount - 1 do
if I <> K then
AddGoal(gltVictory, I, gc, gsFalse, K);
end;
end;
end;
procedure TKMMapInfo.ResetInfo;
var
I, K: Integer;
begin
MissionMode := mmBuilding;
DefaultHuman := 0;
fTxtInfo.ResetInfo;
for I:=0 to MAX_HANDS-1 do
begin
FlagColors[I] := DEFAULT_PLAYERS_COLORS[I];
CanBeHuman[I] := False;
CanBeClassicAI[I] := False;
CanBeAdvancedAI[I] := False;
GoalsVictoryCount[I] := 0;
SetLength(GoalsVictory[I], 0);
GoalsSurviveCount[I] := 0;
SetLength(GoalsSurvive[I], 0);
for K:=0 to MAX_HANDS-1 do
if I = K then
Alliances[I,K] := atAlly
else
Alliances[I,K] := atEnemy;
end;
end;
//todo -cPractical: Rename to LoadStreamFromFile
procedure TKMMapInfo.LoadFromStreamObj(aStreamObj: TObject; const aPath: UnicodeString);
var
S: TKMemoryStream;
begin
Assert(aStreamObj is TKMemoryStreamBinary, 'Wrong stream object class');
S := TKMemoryStreamBinary(aStreamObj);
S.LoadFromFile(aPath);
//Internal properties
S.Read(fCRC);
S.Read(fDatCRC);
S.Read(fMapAndDatCRC);
S.ReadA(fVersion);
//Exposed properties
S.Read(MapSizeX);
S.Read(MapSizeY);
S.Read(MissionMode, SizeOf(TKMissionMode));
S.Read(LocCount);
S.Read(CanBeHuman, SizeOf(CanBeHuman));
fTxtInfo.Load(S);
IsFavourite := gGameSettings.FavouriteMaps.Contains(fMapAndDatCRC);
end;
procedure TKMMapInfo.LoadFromFile(const aPath: UnicodeString);
var
S: TKMemoryStream;
errorStr: UnicodeString;
begin
if not FileExists(aPath) then Exit;
S := TKMemoryStreamBinary.Create;
try
// Try to load map cache up to 3 times (in case its updating by other thread
// its much easier and working well, then synchronize threads
if not TryExecuteMethod(LoadFromStreamObj, TObject(S), aPath, 'LoadFromStreamObj', errorStr) then
begin
gLog.AddTime(errorStr);
gLog.AddTime('Error loading map cache: "%s". The file will be deleted.', [aPath]);
KMDeleteFile(aPath);
end;
finally
//Other properties are not saved, they are fast to reload
S.Free;
end;
end;
//todo -cPractical: Rename to SaveStreamToFile
procedure TKMMapInfo.SaveToStreamObj(aStreamObj: TObject; const aPath: UnicodeString);
var
S: TKMemoryStream;
begin
Assert(aStreamObj is TKMemoryStreamBinary, 'Wrong stream object class');
S := TKMemoryStreamBinary(aStreamObj);
S.SaveToFile(aPath);
end;
procedure TKMMapInfo.SaveToFile(const aPath: UnicodeString);
var
S: TKMemoryStream;
errorStr: UnicodeString;
begin
S := TKMemoryStreamBinary.Create;
try
//Internal properties
S.Write(fCRC);
S.Write(fDatCRC);
S.Write(fMapAndDatCRC);
S.WriteA(fVersion);
//Exposed properties
S.Write(MapSizeX);
S.Write(MapSizeY);
S.Write(MissionMode, SizeOf(TKMissionMode));
S.Write(LocCount);
S.Write(CanBeHuman, SizeOf(CanBeHuman));
fTxtInfo.Save(S);
//Try to save map cache up to 3 times (in case its updating by other thread
//its much easier and working well, then synchronize threads
if not TryExecuteMethod(SaveToStreamObj, TObject(S), aPath, 'SaveToStreamObj', errorStr) then
gLog.AddTime(errorStr);
finally
//Other properties from text file are not saved, they are fast to reload
S.Free;
end;
end;
function TKMMapInfo.IsValid: Boolean;
begin
Result := (LocCount > 0) and
FileExists(fDir + fName + '.dat') and
FileExists(fDir + fName + '.map');
end;
function TKMMapInfo.HumanPlayerCount: Byte;
var
I: Integer;
begin
Result := 0;
for I := 0 to MAX_HANDS - 1 do
if CanBeHuman[I] then
Inc(Result);
end;
function TKMMapInfo.HumanPlayerCountMP: Byte;
begin
Result := HumanPlayerCount;
//Enforce MP limit
if Result > MAX_LOBBY_PLAYERS then
Result := MAX_LOBBY_PLAYERS;
end;
function TKMMapInfo.AIOnlyLocCount: Byte;
var
I: Integer;
begin
Result := 0;
for I := 0 to MAX_HANDS - 1 do
if (CanBeClassicAI[I] or CanBeAdvancedAI[I]) and not CanBeHuman[I] then
Inc(Result);
end;
//Returns True if map filename ends with this map actual CRC hash.
//Used to check if downloaded map was changed
function TKMMapInfo.IsFilenameEndMatchHash: Boolean;
begin
Result := (Length(fName) > 9)
and (fName[Length(Name)-8] = '_')
and (IntToHex(fCRC, 8) = RightStr(fName, 8));
end;
function TKMMapInfo.IsSinglePlayer: Boolean;
begin
Result := HumanPlayerCount = 1;
end;
function TKMMapInfo.IsMultiPlayer: Boolean;
begin
Result := HumanPlayerCount > 1;
end;
function TKMMapInfo.IsPlayableForSP: Boolean;
begin
Result := IsSinglePlayerKind or TxtInfo.IsPlayableAsSP;
end;
function TKMMapInfo.IsSinglePlayerKind: Boolean;
begin
Result := fKind = mkSP;
end;
function TKMMapInfo.IsMultiPlayerKind: Boolean;
begin
Result := fKind = mkMP;
end;
function TKMMapInfo.IsDownloadedKind: Boolean;
begin
Result := fKind = mkDL;
end;
function TKMMapInfo.IsBuildingMission: Boolean;
begin
Result := MissionMode = mmBuilding;
end;
function TKMMapInfo.IsFightingMission: Boolean;
begin
Result := MissionMode = mmFighting;
end;
function TKMMapInfo.FileNameWithoutHash: UnicodeString;
begin
if (fKind = mkDL) and IsFilenameEndMatchHash then
Result := LeftStr(Name, Length(Name)-9)
else
Result := Name;
end;
function TKMMapInfo.DetermineReadmeFilePath: String;
begin
if Self = nil then Exit('');
Assert(gGameSettings <> nil, 'gGameSettings = nil!');
Result := GetLocalizedFilePath(fDir + fName, gResLocales.UserLocale, gResLocales.FallbackLocale, '.pdf');
end;
function TKMMapInfo.GetFavouriteMapPic: TKMPic;
begin
Result := MakePic(rxGuiMain, IfThen(IsFavourite, 77, 85), True);
end;
function TKMMapInfo.GetCanBeHumanCount: Byte;
var
I: Integer;
begin
Result := 0;
for I := Low(CanBeHuman) to High(CanBeHuman) do
if CanBeHuman[I] then
Inc(Result);
end;
function TKMMapInfo.GetCanBeOnlyHumanCount: Byte;
var
I: Integer;
begin
Result := 0;
for I := Low(CanBeHuman) to High(CanBeHuman) do
if CanBeHuman[I] and not CanBeClassicAI[I] and not CanBeAdvancedAI[I] then
Inc(Result);
end;
function TKMMapInfo.GetDimentions: TKMPoint;
begin
Result := TKMPoint.New(MapSizeX, MapSizeY);
end;
function TKMMapInfo.GetCanBeAICount: Byte;
var
I: Integer;
begin
Result := 0;
for I := Low(CanBeClassicAI) to High(CanBeClassicAI) do
if CanBeClassicAI[I] or CanBeAdvancedAI[I] then
Inc(Result);
end;
function TKMMapInfo.GetCanBeOnlyAICount: Byte;
var
I: Integer;
begin
Result := 0;
for I := Low(CanBeHuman) to High(CanBeHuman) do
if (CanBeClassicAI[I] or CanBeAdvancedAI[I]) and not CanBeHuman[I] then
Inc(Result);
end;
function TKMMapInfo.GetAICanBeOnlyAdvanced(aIndex: Integer): Boolean;
begin
Result := CanBeAdvancedAI[aIndex] and not CanBeClassicAI[aIndex];
end;
function TKMMapInfo.GetAICanBeOnlyClassic(aIndex: Integer): Boolean;
begin
Result := CanBeClassicAI[aIndex] and not CanBeAdvancedAI[aIndex];
end;
function TKMMapInfo.GetCanBeHumanAndAICount: Byte;
var
I: Integer;
begin
Result := 0;
for I := Low(CanBeHuman) to High(CanBeHuman) do
if (CanBeClassicAI[I] or CanBeAdvancedAI[I]) and CanBeHuman[I] then
Inc(Result);
end;
function TKMMapInfo.HasDifferentAITypes(aExceptLoc: TKMHandID = -1): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to MAX_HANDS - 1 do
begin
if I = aExceptLoc then
Continue;
if CanBeClassicAI[I] and CanBeAdvancedAI[I] then
Result := True;
end;
end;
function TKMMapInfo.GetBigDesc: UnicodeString;
var
CSP: TKMCustomScriptParam;
begin
Result := '';
for CSP := Low(TKMCustomScriptParam) to High(TKMCustomScriptParam) do
if fCustomScriptParams[CSP].Added then
Result := Result + WrapColor(gResTexts[CUSTOM_MAP_PARAM_DESCR_TX[CSP]] + ':', icRed) + '|'
+ WrapColor('[' + fCustomScriptParams[CSP].Data + ']', icOrange) + '||';
Result := Result + TxtInfo.BigDescToDisplay;
// Add 1 new line for author & version section
if (TxtInfo.Author <> '') or (TxtInfo.Version <> '') and (Result <> '') then
Result := Result + '|';
if (TxtInfo.Author <> '') then
Result := Result + Format('|[$00B0FF]%s:[] %s', [gResTexts[TX_MAPED_MISSION_AUTHOR], TxtInfo.Author]);
if TxtInfo.Version <> '' then
Result := Result + Format('|[$7070FF]%s:[] %s', [gResTexts[TX_MAPED_MISSION_VERSION], TxtInfo.Version]);
end;
function TKMMapInfo.HasReadme: Boolean;
begin
Result := DetermineReadmeFilePath <> '';
end;
function TKMMapInfo.GetLobbyColor: Cardinal;
begin
if fKind = mkDL then
Result := $FFC9BBBB
else
Result := $FF9CF6FF;
end;
{ TKMMapTxtInfo }
constructor TKMMapTxtInfo.Create;
begin
inherited;
// Only these two fields should be "non-zero" by default
fSmallDescLibx := LIBX_NO_ID;
fBigDescLibx := LIBX_NO_ID;
end;
procedure TKMMapTxtInfo.SaveTXTInfo(const aFilePath: String);
var
St: String;
MD: TKMMissionDifficulty;
SL: TStringList;
procedure WriteLine(const aLineHeader: String; const aLineValue: String = '');
begin
SL.Add(aLineHeader);
if aLineValue <> '' then
SL.Add(aLineValue);
SL.Add('');
end;
begin
if IsEmpty then
begin
if FileExists(aFilePath) then
KMDeleteFile(aFilePath);
Exit;
end;
ForceDirectories(ExtractFilePath(aFilePath));
SL := TStringList.Create;
try
if Author <> '' then
WriteLine('Author', Author);
if Version <> '' then
WriteLine('Version', Version);
if fSmallDescLibx <> LIBX_NO_ID then
WriteLine('SmallDescLIBX', IntToStr(fSmallDescLibx))
else
if fSmallDesc <> '' then
WriteLine('SmallDesc', fSmallDesc);
if fBigDescLibx <> LIBX_NO_ID then
WriteLine('BigDescLIBX', IntToStr(fBigDescLibx))
else
if fBigDesc <> '' then
WriteLine('BigDesc', fBigDesc);
if IsCoop then
WriteLine('SetCoop');
if IsSpecial then
WriteLine('SetSpecial');
if IsRMG then
WriteLine('RMG');
if IsPlayableAsSP then
WriteLine('PlayableAsSP');
if BlockPeacetime then
WriteLine('BlockPeacetime');
if BlockTeamSelection then
WriteLine('BlockTeamSelection');
if BlockColorSelection then
WriteLine('BlockColorSelection');
if BlockFullMapPreview then
WriteLine('BlockFullMapPreview');
if HasDifficultyLevels then
begin
St := '';
for MD := MISSION_DIFFICULTY_MIN to MISSION_DIFFICULTY_MAX do
if MD in DifficultyLevels then
begin
if St <> '' then
St := St + ',';
St := St + GetEnumName(TypeInfo(TKMMissionDifficulty), Integer(MD));
end;
WriteLine('DifficultyLevels', St);
end;
// Use UTF8 to save Chinese properly f.e.
SL.SaveToFile(aFilePath, TEncoding.UTF8);
finally
SL.Free;
end;
end;
procedure TKMMapTxtInfo.LoadTXTInfo(const aFilePath: String);
function LoadDescriptionFromLIBX(aIndex: Integer): UnicodeString;
var
missionTexts: TKMTextLibrarySingle;
begin
Result := '';
if aIndex = -1 then Exit;
missionTexts := TKMTextLibrarySingle.Create;
missionTexts.LoadLocale(ChangeFileExt(aFilePath, '.%s.libx'));
Result := missionTexts.Texts[aIndex];
missionTexts.Free;
end;
var
I, K, tmpInt: Integer;
line: String;
stList, fileSList: TStringList;
MD: TKMMissionDifficulty;
begin
//Load additional text info
if not FileExists(aFilePath) then Exit;
try
fileSList := TStringList.Create;
try
try
// Try to load as a UTF8 file to get Chinese properly f.e.
fileSList.LoadFromFile(aFilePath, TEncoding.UTF8);
except
on E: Exception do
begin
// Even if the file is not in UTF8 we have to load it.
// We would not load proper strings (BigDesc / SmallDesc f.e.)
// but at least we will get map settings like Coop / Special etc
fileSList.LoadFromFile(aFilePath);
end;
end;
for K := 0 to fileSList.Count - 1 do
begin
line := fileSList.Strings[K];
if SameText(line, 'Author') then
Author := fileSList.Strings[K + 1];
if SameText(line, 'Version') then
Version := fileSList.Strings[K + 1];
if SameText(line, 'BigDesc') then
BigDesc := fileSList.Strings[K + 1]; // Will reset BigDescLIBX if needed
if SameText(line, 'BigDescLIBX') then
begin
tmpInt := StrToIntDef(fileSList.Strings[K + 1], LIBX_NO_ID);
if tmpInt <> LIBX_NO_ID then
SetBigDescLibxAndTranslation(tmpInt, LoadDescriptionFromLIBX(tmpInt));
end;
if SameText(line, 'SmallDesc') then
begin
SmallDesc := fileSList.Strings[K + 1]; // Will reset SmallDescLIBX if needed
end;
if SameText(line, 'SmallDescLIBX') then
begin
tmpInt := StrToIntDef(fileSList.Strings[K + 1], LIBX_NO_ID);
if tmpInt <> LIBX_NO_ID then
SetSmallDescLibxAndTranslation(tmpInt, LoadDescriptionFromLIBX(tmpInt));
end;
if SameText(line, 'SetCoop') then
begin
IsCoop := True;
BlockTeamSelection := True;
BlockPeacetime := True;
BlockFullMapPreview := True;
end;
if SameText(line, 'SetSpecial') then
IsSpecial := True;
if SameText(line, 'RMG') then
IsRMG := True;
if SameText(line, 'PlayableAsSP') then
IsPlayableAsSP := True;
if SameText(line, 'BlockTeamSelection') then
BlockTeamSelection := True;
if SameText(line, 'BlockColorSelection') then
BlockColorSelection := True;
if SameText(line, 'BlockPeacetime') then
BlockPeacetime := True;
if SameText(line, 'BlockFullMapPreview') then
BlockFullMapPreview := True;
if SameText(line, 'DifficultyLevels') then
begin
stList := TStringList.Create;
try
StringSplit(fileSList.Strings[K + 1], ',', stList);
for I := 0 to stList.Count - 1 do
for MD := MISSION_DIFFICULTY_MIN to MISSION_DIFFICULTY_MAX do
if SameText(stList[I], GetEnumName(TypeInfo(TKMMissionDifficulty), Integer(MD))) then
Include(DifficultyLevels, MD);
finally
stList.Free;
end;
end;
end;
finally
fileSList.Free;
end;
// Normalize descriptions
NormalizeDesc;
except
on E: Exception do
gLog.AddTime('Error loading map TXT file: "%s": %s', [aFilePath, E.Message]);
end;
end;
function TKMMapTxtInfo.GetSmallDescSanitized: UnicodeString;
var
I: Integer;
begin
if fSmallDescLibx = LIBX_NO_ID then
Result := fSmallDesc
else
Result := fSmallDescTranslated;
// Trim to EOL (for display in a table)
I := Pos('|', Result);
if I <> 0 then
Result := LeftStr(Result, I);
end;
function TKMMapTxtInfo.GetBigDescToDisplay: UnicodeString;
begin
if fBigDescLibx = LIBX_NO_ID then
Result := fBigDesc
else
Result := fBigDescTranslated;
end;
procedure TKMMapTxtInfo.SetSmallDesc(const aSmallDesc: UnicodeString);
begin
fSmallDesc := aSmallDesc;
if fSmallDesc <> '' then
SetSmallDescLibxAndTranslation(LIBX_NO_ID, '');
end;
procedure TKMMapTxtInfo.SetBigDesc(const aBigDesc: UnicodeString);
begin
fBigDesc := aBigDesc;
if fBigDesc <> '' then
SetBigDescLibxAndTranslation(LIBX_NO_ID, '');
end;
// Sets LibxID and its translation from Libx
procedure TKMMapTxtInfo.SetSmallDescLibxAndTranslation(aSmallDescLibx: Integer; aTranslation: UnicodeString);
begin
fSmallDescLibx := aSmallDescLibx;
fSmallDescTranslated := aTranslation;
if fSmallDescLibx <> LIBX_NO_ID then
fSmallDesc := '';
end;
// Sets LibxID and its translation from Libx
procedure TKMMapTxtInfo.SetBigDescLibxAndTranslation(aBigDescLibx: Integer; aTranslation: UnicodeString);
begin
fBigDescLibx := aBigDescLibx;
fBigDescTranslated := aTranslation;
if fBigDescLibx <> LIBX_NO_ID then
fBigDesc := '';
end;
function TKMMapTxtInfo.GetBlockColorSelection: Boolean;
begin
if Self = nil then Exit(False);
Result := fBlockColorSelection;
end;
// Normalize descriptions, thus they should have either LibxID or text
procedure TKMMapTxtInfo.NormalizeDesc;
begin
if fSmallDescLibx = LIBX_NO_ID then
fSmallDescTranslated := ''
else
fSmallDesc := '';
if fBigDescLibx = LIBX_NO_ID then
fBigDescTranslated := ''
else
fBigDesc := '';
end;
function TKMMapTxtInfo.IsSmallDescLibxSet: Boolean;
begin
Result := SmallDescLibx <> LIBX_NO_ID;
end;
function TKMMapTxtInfo.IsBigDescLibxSet: Boolean;
begin
Result := BigDescLibx <> LIBX_NO_ID;
end;
function TKMMapTxtInfo.CanAddDefaultGoals: Boolean;
begin
Result := not IsSpecial and not IsCoop;
end;
function TKMMapTxtInfo.IsEmpty: Boolean;
begin
Result := not (IsCoop or IsSpecial or IsPlayableAsSP or IsRMG
or BlockTeamSelection or BlockColorSelection or BlockPeacetime or BlockFullMapPreview
or (Author <> '') or (Version <> '')
or (fSmallDesc <> '') or IsSmallDescLibxSet
or (fBigDesc <> '') or IsBigDescLibxSet
or HasDifficultyLevels);
end;
function TKMMapTxtInfo.HasDifficultyLevels: Boolean;
var
MD: TKMMissionDifficulty;
begin
if Self = nil then Exit(False);
Result := (DifficultyLevels <> []);
//We consider there is no difficulty levels, if only one is presented
for MD := MISSION_DIFFICULTY_MIN to MISSION_DIFFICULTY_MAX do
Result := Result and (DifficultyLevels <> [MD]);
end;
procedure TKMMapTxtInfo.ResetInfo;
begin
IsCoop := False;
IsSpecial := False;
IsRMG := False;
IsPlayableAsSP := False;
BlockTeamSelection := False;
BlockColorSelection := False;
BlockPeacetime := False;
BlockFullMapPreview := False;
DifficultyLevels := [];
Author := '';
Version := '';
fSmallDesc := '';
fSmallDescLibx := LIBX_NO_ID;
fSmallDescTranslated := '';
fBigDesc := '';
fBigDescLibx := LIBX_NO_ID;
fBigDescTranslated := '';
end;
procedure TKMMapTxtInfo.Load(LoadStream: TKMemoryStream);
begin
LoadStream.CheckMarker(MAP_TXT_INFO_MARKER);
LoadStream.ReadW(Author);
LoadStream.ReadW(Version);
LoadStream.Read(IsCoop);
LoadStream.Read(IsSpecial);
LoadStream.Read(IsRMG);
LoadStream.Read(IsPlayableAsSP);
LoadStream.Read(BlockTeamSelection);
LoadStream.Read(fBlockColorSelection);
LoadStream.Read(BlockPeacetime);
LoadStream.Read(BlockFullMapPreview);
LoadStream.ReadW(fSmallDesc);
LoadStream.Read(fSmallDescLibx);
LoadStream.ReadW(fBigDesc);
LoadStream.Read(fBigDescLibx);
end;
procedure TKMMapTxtInfo.Save(SaveStream: TKMemoryStream);
begin
SaveStream.PlaceMarker(MAP_TXT_INFO_MARKER);
SaveStream.WriteW(Author);
SaveStream.WriteW(Version);
SaveStream.Write(IsCoop);
SaveStream.Write(IsSpecial);
SaveStream.Write(IsRMG);
SaveStream.Write(IsPlayableAsSP);
SaveStream.Write(BlockTeamSelection);
SaveStream.Write(fBlockColorSelection);
SaveStream.Write(BlockPeacetime);
SaveStream.Write(BlockFullMapPreview);
SaveStream.WriteW(fSmallDesc);
SaveStream.Write(fSmallDescLibx);
SaveStream.WriteW(fBigDesc);
SaveStream.Write(fBigDescLibx);
end;
{ TKMapsCollection }
constructor TKMapsCollection.Create(aKindSet: TKMMapKindSet; aSortMethod: TKMapsSortMethod = smByNameDesc; aDoSortWithFavourites: Boolean = False);
begin
inherited Create;
fMapFolders := aKindSet;
fSortMethod := aSortMethod;
fDoSortWithFavourites := aDoSortWithFavourites;
//CS is used to guard sections of code to allow only one thread at once to access them
//We mostly don't need it, as UI should access Maps only when map events are signaled
//it mostly acts as a safenet
fCriticalSection := TCriticalSection.Create;
end;
function TKMapsCollection.Contains(const aNewName: UnicodeString): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to fCount - 1 do
if LowerCase(fMaps[I].Name) = LowerCase(aNewName) then
begin
Result := True;
Exit;
end;
end;
constructor TKMapsCollection.Create(aKind: TKMMapKind; aSortMethod: TKMapsSortMethod = smByNameDesc; aDoSortWithFavourites: Boolean = False);
begin
Create([aKind], aSortMethod, aDoSortWithFavourites);
end;
destructor TKMapsCollection.Destroy;
begin
//Terminate and release the Scanner if we have one working or finished
TerminateScan;
//Release TKMMapInfo objects
Clear;
fCriticalSection.Free;
inherited;
end;
function TKMapsCollection.GetMap(aIndex: Integer): TKMMapInfo;
begin
//No point locking/unlocking here since we return a TObject that could be modified/freed
//by another thread before the caller uses it.
Assert(InRange(aIndex, 0, fCount - 1), 'aIndex = ' + IntToStr(aIndex) + ' fCount = ' + IntToStr(fCount));
Result := fMaps[aIndex];
end;
//class function TKMapsCollection.GuessMPPath(const aName, aExt: string; aCRC: Cardinal): string;
//var
// S: UnicodeString;
//begin
// S := aName + '_' + IntToHex(aCRC, 8);
// Result := MAP_FOLDER[mkDL] + PathDelim + S + PathDelim + S + aExt;
// if not FileExists(ExeDir + Result) then
// Result := MAP_FOLDER[mkMP] + PathDelim + aName + PathDelim + aName + aExt;
//end;
procedure TKMapsCollection.Lock;
begin
fCriticalSection.Enter;
end;
procedure TKMapsCollection.Unlock;
begin
fCriticalSection.Leave;
end;
procedure TKMapsCollection.Clear;
var
I: Integer;
begin
Assert(not fScanning, 'Guarding from access to inconsistent data');
for I := 0 to fCount - 1 do
FreeAndNil(fMaps[I]);
fCount := 0;
SetLength(fMaps, 0); //We could use Low and High. Need to reset array to 0 length
end;
procedure TKMapsCollection.UpdateState;
begin
if Self = nil then Exit;
if not fUpdateNeeded then Exit;
if Assigned(fOnRefresh) then
fOnRefresh(Self);
fUpdateNeeded := False;
end;
procedure TKMapsCollection.DeleteMap(aIndex: Integer);
var
I: Integer;
begin
Lock;
try
Assert(InRange(aIndex, 0, fCount - 1));
KMDeleteFolderToBin(fMaps[aIndex].Dir);
fMaps[aIndex].Free;
for I := aIndex to fCount - 2 do
fMaps[I] := fMaps[I + 1];
Dec(fCount);
SetLength(fMaps, fCount);
finally
Unlock;
end;
end;
procedure TKMapsCollection.RenameMap(aIndex: Integer; const aName: UnicodeString);
begin
MoveMap(aIndex, aName, fMaps[aIndex].fKind);
end;
procedure TKMapsCollection.MoveMap(aIndex: Integer; const aName: UnicodeString; aMapKind: TKMMapKind);
var
I: Integer;
dest: UnicodeString;
begin
Assert(InRange(aIndex, 0, fCount - 1));
if Trim(aName) = '' then Exit;
Lock;
try
dest := ExeDir + MAP_FOLDER_NAME[aMapKind] + PathDelim + aName + PathDelim;
Assert(fMaps[aIndex].Dir <> dest);
KMMoveFolder(fMaps[aIndex].Dir, dest);
//Remove the map from our list
fMaps[aIndex].Free;
for I := aIndex to fCount - 2 do
fMaps[I] := fMaps[I + 1];
Dec(fCount);
SetLength(fMaps, fCount);
finally
Unlock;
end;
end;
//For private access, where CS is managed by the caller
procedure TKMapsCollection.DoSort;
var
tempMaps: array of TKMMapInfo;
//Return True if items should be exchanged
function Compare(A, B: TKMMapInfo): Boolean;
begin
Result := False; //By default everything remains in place
case fSortMethod of
smByFavouriteAsc: Result := A.IsFavourite and not B.IsFavourite;
smByFavouriteDesc: Result := not A.IsFavourite and B.IsFavourite;
smByNameAsc: Result := CompareTextLogical(A.Name, B.Name) < 0;
smByNameDesc: Result := CompareTextLogical(A.Name, B.Name) > 0;
smBySizeAsc: // Compare by actual map area, size indexes will be sorted automatically
Result := A.MapSizeX*A.MapSizeY < B.MapSizeX*B.MapSizeY;
smBySizeDesc: // Compare by actual map area, size indexes will be sorted automatically
Result := A.MapSizeX*A.MapSizeY > B.MapSizeX*B.MapSizeY;
smByPlayersAsc: Result := A.LocCount < B.LocCount;
smByPlayersDesc: Result := A.LocCount > B.LocCount;
smByHumanPlayersAsc: Result := A.HumanPlayerCount < B.HumanPlayerCount;
smByHumanPlayersDesc: Result := A.HumanPlayerCount > B.HumanPlayerCount;
smByHumanPlayersMPAsc: Result := A.HumanPlayerCountMP < B.HumanPlayerCountMP;
smByHumanPlayersMPDesc: Result := A.HumanPlayerCountMP > B.HumanPlayerCountMP;
smByMissionModeAsc: Result := A.MissionMode < B.MissionMode;
smByMissionModeDesc: Result := A.MissionMode > B.MissionMode;
end;
if fDoSortWithFavourites and not (fSortMethod in [smByFavouriteAsc, smByFavouriteDesc]) then
begin
if A.IsFavourite and not B.IsFavourite then
Result := False
else if not A.IsFavourite and B.IsFavourite then
Result := True
end;
end;
procedure MergeSort(aLeft, aRight: Integer);
var
middle, I, J, ind1, ind2: integer;
begin
if aRight <= aLeft then
exit;
middle := (aLeft+aRight) div 2;
MergeSort(aLeft, middle);
Inc(middle);
MergeSort(middle, aRight);
ind1 := aLeft;
ind2 := middle;
for I := aLeft to aRight do
begin
if (ind1 < middle) and ((ind2 > aRight) or not Compare(fMaps[ind1], fMaps[ind2])) then
begin
tempMaps[I] := fMaps[ind1];
Inc(ind1);
end
else
begin
tempMaps[I] := fMaps[ind2];
Inc(ind2);
end;
end;
for J := aLeft to aRight do
fMaps[J] := tempMaps[J];
end;
begin
SetLength(tempMaps, fCount);
MergeSort(0, fCount - 1);
end;
//For public access
//Apply new Sort within Critical Section, as we could be in the Refresh phase
//note that we need to preserve fScanning flag
procedure TKMapsCollection.Sort(aSortMethod: TKMapsSortMethod; aOnSortComplete: TNotifyEvent);
begin
Lock;
try
if fScanning then
begin
fScanning := False;
fSortMethod := aSortMethod;
DoSort;
if Assigned(aOnSortComplete) then
aOnSortComplete(Self);
fScanning := True;
end
else
begin
fSortMethod := aSortMethod;
DoSort;
if Assigned(aOnSortComplete) then
aOnSortComplete(Self);
end;
finally
Unlock;
end;
end;
procedure TKMapsCollection.TerminateScan;
begin
if (fScanner <> nil) then
begin
fScanner.Terminate;
fScanner.WaitFor;
fScanner.Free;
fScanner := nil;
fScanning := False;
end;
fUpdateNeeded := False; //If the scan was terminated we should not run fOnRefresh next UpdateState
end;
//Start the refresh of maplist
procedure TKMapsCollection.Refresh(aOnRefresh: TNotifyEvent; aOnTerminate: TNotifyEvent = nil; aOnComplete: TNotifyEvent = nil);
begin
//Terminate previous Scanner if two scans were launched consequentialy
TerminateScan;
Clear;
fOnRefresh := aOnRefresh;
fOnComplete := aOnComplete;
fOnTerminate := aOnTerminate;
//Scan will launch upon create automatically
fScanning := True;
fScanner := TTMapsScanner.Create(fMapFolders, MapAdd, MapAddDone, ScanTerminate, ScanComplete);
end;
procedure TKMapsCollection.MapAdd(aMap: TKMMapInfo);
begin
Lock;
try
SetLength(fMaps, fCount + 1);
fMaps[fCount] := aMap;
Inc(fCount);
//Set the scanning to False so we could Sort
fScanning := False;
//Keep the maps sorted
//We signal from Locked section, so everything caused by event can safely access our Maps
DoSort;
fScanning := True;
finally
Unlock;
end;
end;
procedure TKMapsCollection.MapAddDone(Sender: TObject);
begin
fUpdateNeeded := True; //Next time the GUI thread calls UpdateState we will run fOnRefresh
end;
//All maps have been scanned
//No need to resort since that was done in last MapAdd event
procedure TKMapsCollection.ScanComplete(Sender: TObject);
begin
Lock;
try
fScanning := False;
if Assigned(fOnComplete) then
fOnComplete(Self);
finally
Unlock;
end;
end;
//Scan was terminated
//No need to resort since that was done in last MapAdd event
procedure TKMapsCollection.ScanTerminate(Sender: TObject);
begin
Lock;
try
fScanning := False;
if Assigned(fOnTerminate) then
fOnTerminate(Self);
finally
Unlock;
end;
end;
class function TKMapsCollection.FullPath(const aName, aExt: string; aMultiplayer: Boolean): string;
begin
Result := FullPath(aName, aExt, GetMapKind(aMultiplayer));
end;
class function TKMapsCollection.FullPath(const aName, aExt: string; aMapKind: TKMMapKind): string;
begin
Result := ExeDir + MAP_FOLDER_NAME[aMapKind] + PathDelim + aName + PathDelim + aName + aExt;
end;
class function TKMapsCollection.FullPath(const aDirName, aFileName, aExt: string; aMapKind: TKMMapKind): string;
begin
Result := ExeDir + MAP_FOLDER_NAME[aMapKind] + PathDelim + aDirName + PathDelim + aFileName + aExt;
end;
class function TKMapsCollection.FullPath(const aName, aExt: string; aMapKind: TKMMapKind; aCRC: Cardinal): string;
var
S: UnicodeString;
begin
S := aName;
if aMapKind = mkDL then
S := S + '_' + IntToHex(Integer(aCRC), 8);
Result := FullPath(S, aExt, aMapKind);
end;
class function TKMapsCollection.GetMapCRC(const aMapPath: string): Cardinal;
begin
Result := 0;
if FileExists(aMapPath) then
Result := Adler32CRC(aMapPath);
end;
class procedure TKMapsCollection.GetAllMapPaths(const aExeDir: string; aList: TStringList);
var
I: Integer;
searchRec: TSearchRec;
pathToMaps: TStringList;
begin
aList.Clear;
pathToMaps := TStringList.Create;
try
pathToMaps.Add(aExeDir + MAPS_FOLDER_NAME + PathDelim);
pathToMaps.Add(aExeDir + MAPS_MP_FOLDER_NAME + PathDelim);
pathToMaps.Add(aExeDir + TUTORIALS_FOLDER_NAME + PathDelim);
//Include all campaigns maps
FindFirst(aExeDir + CAMPAIGNS_FOLDER_NAME + PathDelim + '*', faDirectory, searchRec);
try
repeat
if (searchRec.Name <> '.') and (searchRec.Name <> '..') then
pathToMaps.Add(aExeDir + CAMPAIGNS_FOLDER_NAME + PathDelim + searchRec.Name + PathDelim);
until (FindNext(searchRec) <> 0);
finally
FindClose(searchRec);
end;
for I := 0 to pathToMaps.Count - 1 do
if DirectoryExists(pathToMaps[I]) then
begin
FindFirst(pathToMaps[I] + '*', faDirectory, searchRec);
try
repeat
if (searchRec.Name <> '.') and (searchRec.Name <> '..')
and FileExists(pathToMaps[I] + searchRec.Name + PathDelim + searchRec.Name + '.dat')
and FileExists(pathToMaps[I] + searchRec.Name + PathDelim + searchRec.Name + '.map') then
aList.Add(pathToMaps[I] + searchRec.Name + PathDelim + searchRec.Name + '.dat');
until (FindNext(searchRec) <> 0);
finally
FindClose(searchRec);
end;
end;
finally
pathToMaps.Free;
end;
end;
{ TTCustomMapsScanner }
constructor TTCustomMapsScanner.Create(aMapKinds: TKMMapKindSet; aOnComplete: TNotifyEvent = nil);
begin
//Thread isn't started until all constructors have run to completion
//so Create(False) may be put in front as well
inherited Create(False);
fMapKinds := aMapKinds;
fOnComplete := aOnComplete;
FreeOnTerminate := False;
end;
procedure TTCustomMapsScanner.Execute;
var
searchRec: TSearchRec;
pathToMaps: string;
MK: TKMMapKind;
begin
try
for MK in fMapKinds do
begin
pathToMaps := ExeDir + MAP_FOLDER_NAME[MK] + PathDelim;
if not DirectoryExists(pathToMaps) then Continue;
FindFirst(pathToMaps + '*', faDirectory, searchRec);
try
repeat
if (searchRec.Name <> '.') and (searchRec.Name <> '..')
and FileExists(TKMapsCollection.FullPath(searchRec.Name, '.dat', MK))
and FileExists(TKMapsCollection.FullPath(searchRec.Name, '.map', MK)) then
begin
try
ProcessMap(searchRec.Name, MK);
except
on E: Exception do
gLog.AddTime('Error loading map "%s"', [searchRec.Name]); //Just silently log an exception
end;
end;
until (FindNext(searchRec) <> 0) or Terminated;
finally
FindClose(searchRec);
end;
end;
finally
if not Terminated and Assigned(fOnComplete) then
fOnComplete(Self);
end;
end;
{ TTMapsScanner }
//aOnMapAdd - signal that there's new map that should be added
//aOnMapAddDone - signal that map has been added
//aOnTerminate - scan was terminated (but could be not complete yet)
//aOnComplete - scan is complete
constructor TTMapsScanner.Create(aMapFolders: TKMMapKindSet; aOnMapAdd: TKMapEvent; aOnMapAddDone, aOnTerminate: TNotifyEvent; aOnComplete: TNotifyEvent = nil);
begin
inherited Create(aMapFolders, aOnComplete);
Assert(Assigned(aOnMapAdd));
{$IFDEF DEBUG}
TThread.NameThreadForDebugging('MapsScanner', ThreadID);
{$ENDIF}
fOnMapAdd := aOnMapAdd;
fOnMapAddDone := aOnMapAddDone;
OnTerminate := aOnTerminate;
FreeOnTerminate := False;
end;
procedure TTMapsScanner.ProcessMap(const aPath: UnicodeString; aKind: TKMMapKind);
var
map: TKMMapInfo;
begin
map := TKMMapInfo.Create(aPath, False, aKind);
if SLOW_MAP_SCAN then
Sleep(50);
fOnMapAdd(map);
fOnMapAddDone(Self);
end;
{ TTMapsCacheUpdater }
constructor TTMapsCacheUpdater.Create(aMapFolders: TKMMapKindSet);
begin
inherited Create(aMapFolders);
{$IFDEF DEBUG}
TThread.NameThreadForDebugging('MapsCacheUpdater', ThreadID);
{$ENDIF}
FreeOnTerminate := True;
end;
procedure TTMapsCacheUpdater.ProcessMap(const aPath: UnicodeString; aKind: TKMMapKind);
var
map: TKMMapInfo;
begin
//Simply creating the TKMMapInfo updates the .mi cache file
if not fIsStopped then
begin
map := TKMMapInfo.Create(aPath, False, aKind);
map.Free;
end;
end;
procedure TTMapsCacheUpdater.Stop;
begin
if Self <> nil then
fIsStopped := True;
end;
end.