diff --git a/ChivalryServerLauncher.dpr b/ChivalryServerLauncher.dpr
index 1e9d139..c9d7227 100644
--- a/ChivalryServerLauncher.dpr
+++ b/ChivalryServerLauncher.dpr
@@ -14,7 +14,8 @@ uses
Game.List in 'source\model\Game.List.pas',
Persist.GameList in 'source\persist\Persist.GameList.pas',
Forms.Map in 'source\view\Forms.Map.pas' {MapForm},
- Frame.MapPreview in 'source\view\Frame.MapPreview.pas' {MapPreviewFrame: TFrame};
+ Frame.MapPreview in 'source\view\Frame.MapPreview.pas' {MapPreviewFrame: TFrame},
+ UDKIniFile in 'source\UDKIniFile.pas';
{$R *.res}
diff --git a/ChivalryServerLauncher.dproj b/ChivalryServerLauncher.dproj
index b0045fc..92a2677 100644
--- a/ChivalryServerLauncher.dproj
+++ b/ChivalryServerLauncher.dproj
@@ -44,7 +44,7 @@
CompanyName=X²Software;FileDescription=Chivalry Server Launcher;FileVersion=0.1.0.0;InternalName=;LegalCopyright=Copyright (c) 2014 X²Software;LegalTrademarks=;OriginalFilename=ChivalryServerLauncher.exe;ProductName=Chivalry Server Launcher;ProductVersion=0.1
1
true
- 1043
+ 1033
0
resources\icons\MainIcon.ico
None
@@ -69,6 +69,7 @@
true
+ true
$(BDS)\bin\default_app.manifest
@@ -104,6 +105,7 @@
dfm
TFrame
+
Base
diff --git a/ChivalryServerLauncher.res b/ChivalryServerLauncher.res
index 65fb718..2d30be7 100644
Binary files a/ChivalryServerLauncher.res and b/ChivalryServerLauncher.res differ
diff --git a/source/UDKIniFile.pas b/source/UDKIniFile.pas
new file mode 100644
index 0000000..980520a
--- /dev/null
+++ b/source/UDKIniFile.pas
@@ -0,0 +1,444 @@
+unit UDKIniFile;
+
+interface
+uses
+ System.Classes,
+ System.IniFiles,
+ System.SysUtils;
+
+
+type
+ { Basically a TMemIniFile with benefits (TMemIniFile isn't virtual enough
+ to allow simple additions to it). Adds helpers to handle multiple keys
+ with the same name. }
+ TUDKIniFile = class(TCustomIniFile)
+ private
+ FSections: TStringList;
+ FEncoding: TEncoding;
+
+ function GetCaseSensitive: Boolean;
+ procedure SetCaseSensitive(Value: Boolean);
+ protected
+ function AddSection(const ASection: string): TStrings;
+ procedure LoadValues;
+
+ property Sections: TStringList read FSections;
+ public
+ constructor Create(const AFileName: string); overload;
+ constructor Create(const AFileName: string; AEncoding: TEncoding); overload;
+ destructor Destroy; override;
+
+ procedure Clear;
+
+ function ReadString(const ASection, AIdent, ADefault: string): string; override;
+ procedure DeleteKey(const ASection, AIdent: string); override;
+ procedure EraseSection(const ASection: string); override;
+ procedure ReadSection(const ASection: string; AStrings: TStrings); override;
+ procedure ReadSections(AStrings: TStrings); override;
+ procedure ReadSectionValues(const ASection: string; AStrings: TStrings); override;
+ procedure UpdateFile; override;
+ procedure WriteString(const ASection, AIdent, AValue: string); override;
+
+ procedure GetStrings(AList: TStrings);
+ procedure SetStrings(AList: TStrings);
+
+ { Helpers for duplicate keys }
+ procedure ReadDuplicateStrings(const ASection, AIdent: string; AList: TStrings);
+ procedure WriteDuplicateString(const ASection, AIdent, AValue: string);
+ procedure DeleteDuplicateKeys(const ASection, AIdent: string);
+
+ property CaseSensitive: Boolean read GetCaseSensitive write SetCaseSensitive;
+ property Encoding: TEncoding read FEncoding write FEncoding;
+ end;
+
+
+implementation
+type
+ { Added here so we can access the protected members }
+ TUDKHashedStringList = class(THashedStringList);
+
+
+{ TUDKIniFile }
+constructor TUDKIniFile.Create(const AFileName: string);
+begin
+ Create(AFilename, nil);
+end;
+
+
+constructor TUDKIniFile.Create(const AFileName: string; AEncoding: TEncoding);
+begin
+ inherited Create(AFileName);
+
+ FEncoding := AEncoding;
+ FSections := TUDKHashedStringList.Create;
+ LoadValues;
+end;
+
+
+destructor TUDKIniFile.Destroy;
+begin
+ Clear;
+ FreeAndNil(FSections);
+
+ inherited Destroy;
+end;
+
+
+procedure TUDKIniFile.Clear;
+var
+ sectionIndex: Integer;
+
+begin
+ for sectionIndex := 0 to Pred(Sections.Count) do
+ Sections.Objects[sectionIndex].Free;
+
+ Sections.Clear;
+end;
+
+
+function TUDKIniFile.ReadString(const ASection, AIdent, ADefault: string): string;
+var
+ sectionIndex: Integer;
+ strings: TStrings;
+
+begin
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ strings := TStrings(Sections.Objects[sectionIndex]);
+ sectionIndex := strings.IndexOfName(AIdent);
+
+ if sectionIndex > -1 then
+ begin
+ Result := Copy(strings[sectionIndex], Length(AIdent) + 2, MaxInt);
+ exit;
+ end;
+ end;
+
+ Result := ADefault;
+end;
+
+
+procedure TUDKIniFile.DeleteKey(const ASection, AIdent: string);
+var
+ sectionIndex: Integer;
+ keyIndex: Integer;
+ Strings: TStrings;
+
+begin
+ sectionIndex := FSections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ strings := TStrings(FSections.Objects[sectionIndex]);
+ keyIndex := strings.IndexOfName(AIdent);
+
+ if keyIndex > -1 then
+ strings.Delete(keyIndex);
+ end;
+end;
+
+
+procedure TUDKIniFile.EraseSection(const ASection: string);
+var
+ sectionIndex: Integer;
+
+begin
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ TStrings(Sections.Objects[sectionIndex]).Free;
+ Sections.Delete(sectionIndex);
+ end;
+end;
+
+
+procedure TUDKIniFile.ReadSection(const ASection: string; AStrings: TStrings);
+var
+ sectionIndex: Integer;
+ keyIndex: Integer;
+ sectionStrings: TStrings;
+
+begin
+ AStrings.BeginUpdate;
+ try
+ AStrings.Clear;
+
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ sectionStrings := TStrings(Sections.Objects[sectionIndex]);
+ for keyIndex := 0 to Pred(sectionStrings.Count) do
+ AStrings.Add(sectionStrings.Names[keyIndex]);
+ end;
+ finally
+ AStrings.EndUpdate;
+ end;
+end;
+
+
+procedure TUDKIniFile.ReadSections(AStrings: TStrings);
+begin
+ AStrings.Assign(Sections);
+end;
+
+
+procedure TUDKIniFile.ReadSectionValues(const ASection: string; AStrings: TStrings);
+var
+ sectionIndex: Integer;
+
+begin
+ AStrings.BeginUpdate;
+ try
+ AStrings.Clear;
+
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ AStrings.Assign(TStrings(Sections.Objects[sectionIndex]));
+ finally
+ AStrings.EndUpdate;
+ end;
+end;
+
+
+procedure TUDKIniFile.UpdateFile;
+var
+ list: TStringList;
+
+begin
+ list := TStringList.Create;
+ try
+ GetStrings(list);
+ list.SaveToFile(FileName, FEncoding);
+ finally
+ FreeAndNil(list);
+ end;
+end;
+
+
+procedure TUDKIniFile.WriteString(const ASection, AIdent, AValue: string);
+var
+ sectionIndex: Integer;
+ keyIndex: Integer;
+ value: string;
+ strings: TStrings;
+
+begin
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ strings := TStrings(Sections.Objects[sectionIndex])
+ else
+ strings := AddSection(ASection);
+
+ value := AIdent + '=' + AValue;
+ keyIndex := strings.IndexOfName(AIdent);
+
+ if keyIndex > -1 then
+ strings[keyIndex] := value
+ else
+ strings.Add(value);
+end;
+
+
+procedure TUDKIniFile.GetStrings(AList: TStrings);
+var
+ sectionIndex: Integer;
+ strings: TStrings;
+ value: string;
+
+begin
+ AList.BeginUpdate;
+ try
+ for sectionIndex := 0 to Pred(Sections.Count) do
+ begin
+ AList.Add('[' + Sections[sectionIndex] + ']');
+
+ strings := TStrings(Sections.Objects[sectionIndex]);
+ for value in strings do
+ AList.Add(value);
+
+ AList.Add('');
+ end;
+ finally
+ AList.EndUpdate;
+ end;
+end;
+
+
+procedure TUDKIniFile.SetStrings(AList: TStrings);
+var
+ lineIndex: Integer;
+ line: string;
+ separatorPos: Integer;
+ strings: TStrings;
+
+begin
+ Clear;
+ strings := nil;
+
+ for lineIndex := 0 to Pred(AList.Count) do
+ begin
+ line := Trim(AList[lineIndex]);
+
+ if (Length(line) > 0) and (line[1] <> ';') then
+ begin
+ if (line[1] = '[') and (line[Length(line)] = ']') then
+ begin
+ Delete(line, 1, 1);
+ SetLength(line, Length(line) - 1);
+
+ strings := AddSection(Trim(line));
+ end
+ else
+ if Assigned(strings) then
+ begin
+ separatorPos := Pos('=', line);
+ if separatorPos > 0 then
+ strings.Add(Trim(Copy(line, 1, Pred(separatorPos))) + '=' + Trim(Copy(line, Succ(separatorPos), MaxInt)))
+ else
+ strings.Add(line);
+ end;
+ end;
+ end;
+end;
+
+
+procedure TUDKIniFile.ReadDuplicateStrings(const ASection, AIdent: string; AList: TStrings);
+var
+ sectionIndex: Integer;
+ keyIndex: Integer;
+ Strings: TStrings;
+
+begin
+ sectionIndex := FSections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ strings := TStrings(FSections.Objects[sectionIndex]);
+
+ for keyIndex := 0 to Pred(strings.Count) do
+ begin
+ if (CaseSensitive and (strings.Names[keyIndex] = AIdent)) or
+ ((not CaseSensitive) and SameText(strings.Names[keyIndex], AIdent)) then
+ begin
+ AList.Add(strings.ValueFromIndex[keyIndex]);
+ end;
+ end;
+ end;
+end;
+
+
+procedure TUDKIniFile.WriteDuplicateString(const ASection, AIdent, AValue: string);
+var
+ sectionIndex: Integer;
+ strings: TStrings;
+
+begin
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ strings := TStrings(Sections.Objects[sectionIndex])
+ else
+ strings := AddSection(ASection);
+
+ strings.Add(AIdent + '=' + AValue);
+end;
+
+
+procedure TUDKIniFile.DeleteDuplicateKeys(const ASection, AIdent: string);
+var
+ sectionIndex: Integer;
+ keyIndex: Integer;
+ Strings: TStrings;
+
+begin
+ sectionIndex := Sections.IndexOf(ASection);
+ if sectionIndex > -1 then
+ begin
+ strings := TStrings(Sections.Objects[sectionIndex]);
+
+ for keyIndex := Pred(strings.Count) downto 0 do
+ begin
+ if (CaseSensitive and (strings.Names[keyIndex] = AIdent)) or
+ ((not CaseSensitive) and SameText(strings.Names[keyIndex], AIdent)) then
+ begin
+ strings.Delete(keyIndex)
+ end;
+ end;
+ end;
+end;
+
+
+function TUDKIniFile.AddSection(const ASection: string): TStrings;
+begin
+ Result := THashedStringList.Create;
+ try
+ THashedStringList(Result).CaseSensitive := CaseSensitive;
+ FSections.AddObject(ASection, Result);
+ except
+ FreeAndNil(Result);
+ raise;
+ end;
+end;
+
+
+procedure TUDKIniFile.LoadValues;
+var
+ size: Integer;
+ buffer: TBytes;
+ list: TStringList;
+ stream: TFileStream;
+
+begin
+ if (Length(FileName) > 0) and FileExists(FileName) then
+ begin
+ stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
+ try
+ { Load file into buffer and detect encoding }
+ size := stream.Size - stream.Position;
+ SetLength(buffer, size);
+ stream.Read(buffer[0], size);
+
+ size := TEncoding.GetBufferEncoding(buffer, FEncoding);
+
+ { Load strings from buffer }
+ list := TStringList.Create;
+ try
+ list.Text := FEncoding.GetString(buffer, size, Length(buffer) - size);
+ SetStrings(list);
+ finally
+ FreeAndNil(list);
+ end;
+ finally
+ FreeAndNil(stream);
+ end;
+ end else
+ Clear;
+end;
+
+
+function TUDKIniFile.GetCaseSensitive: Boolean;
+begin
+ Result := Sections.CaseSensitive;
+end;
+
+
+procedure TUDKIniFile.SetCaseSensitive(Value: Boolean);
+var
+ sectionIndex: Integer;
+ sectionList: TUDKHashedStringList;
+
+begin
+ if Value <> Sections.CaseSensitive then
+ begin
+ Sections.CaseSensitive := Value;
+
+ for sectionIndex := 0 to Pred(Sections.Count) do
+ begin
+ sectionList := TUDKHashedStringList(Sections.Objects[sectionIndex]);
+ sectionList.CaseSensitive := Value;
+ sectionList.Changed;
+ end;
+
+ TUDKHashedStringList(Sections).Changed;
+ end;
+end;
+
+end.
diff --git a/source/model/Game.Base.pas b/source/model/Game.Base.pas
index b3580c3..ded4a8e 100644
--- a/source/model/Game.Base.pas
+++ b/source/model/Game.Base.pas
@@ -10,8 +10,9 @@ type
private
FLocation: string;
FLoaded: Boolean;
+ FModified: Boolean;
protected
- procedure Notify(const APropertyName: string); virtual;
+ procedure PropertyChanged(const APropertyName: string); virtual;
procedure DoLoad; virtual; abstract;
procedure DoSave; virtual; abstract;
@@ -26,6 +27,7 @@ type
property Loaded: Boolean read FLoaded;
property Location: string read FLocation;
+ property Modified: Boolean read FModified;
end;
TCustomGameClass = class of TCustomGame;
@@ -53,20 +55,44 @@ end;
procedure TCustomGame.Load;
+var
+ wasModified: Boolean;
+
begin
+ wasModified := FModified;
+
DoLoad;
FLoaded := True;
+
+ FModified := False;
+ if wasModified then
+ TBindings.Notify(Self, 'Modified');
end;
procedure TCustomGame.Save;
+var
+ wasModified: Boolean;
+
begin
+ wasModified := FModified;
+
DoSave;
+
+ FModified := False;
+ if wasModified then
+ TBindings.Notify(Self, 'Modified');
end;
-procedure TCustomGame.Notify(const APropertyName: string);
+procedure TCustomGame.PropertyChanged(const APropertyName: string);
begin
+ if not FModified then
+ begin
+ FModified := True;
+ TBindings.Notify(Self, 'Modified');
+ end;
+
TBindings.Notify(Self, APropertyName);
end;
diff --git a/source/model/Game.Chivalry.MedievalWarfare.pas b/source/model/Game.Chivalry.MedievalWarfare.pas
index 33b21d2..6d7c639 100644
--- a/source/model/Game.Chivalry.MedievalWarfare.pas
+++ b/source/model/Game.Chivalry.MedievalWarfare.pas
@@ -15,7 +15,7 @@ type
in the base Chivalry class. }
TChivalryMedievalWarfareGame = class(TChivalryGame)
protected
- procedure LoadSupportedMapList(AList: TList); override;
+ procedure LoadPredefinedMapList(AList: TList); override;
public
class function GameName: string; override;
class function AutoDetect: TCustomGame; override;
@@ -81,7 +81,7 @@ begin
end;
-procedure TChivalryMedievalWarfareGame.LoadSupportedMapList(AList: TList);
+procedure TChivalryMedievalWarfareGame.LoadPredefinedMapList(AList: TList);
var
mapListFileName: string;
mapList: TMemIniFile;
diff --git a/source/model/Game.Chivalry.pas b/source/model/Game.Chivalry.pas
index b9505c2..b000094 100644
--- a/source/model/Game.Chivalry.pas
+++ b/source/model/Game.Chivalry.pas
@@ -18,13 +18,13 @@ type
FServerName: string;
FMessageOfTheDay: string;
- FSupportedMapList: TObjectList;
+ FPredefinedMapList: TObjectList;
FMapList: TObjectList;
protected
procedure DoLoad; override;
procedure DoSave; override;
- procedure LoadSupportedMapList(AList: TList); virtual; abstract;
+ procedure LoadPredefinedMapList(AList: TList); virtual; abstract;
function CreateGameMap(const AMapName: string): TGameMap; virtual;
public
@@ -55,8 +55,22 @@ type
property MessageOfTheDay: string read GetMessageOfTheDay write SetMessageOfTheDay;
{ IGameMapList }
- function GetSupportedMapList: TList;
- function GetMapList: TList;
+ function GetPredefinedMapList: TEnumerable;
+ function GetMapList: TEnumerable;
+
+ function GetMapCount: Integer;
+ function GetMap(Index: Integer): TGameMap;
+
+ procedure AddMap(AMap: TGameMap);
+ procedure InsertMap(Index: Integer; AMap: TGameMap);
+ procedure DeleteMap(AIndex: Integer);
+ procedure RemoveMap(AMap: TGameMap);
+ procedure MoveMap(ASourceIndex, ATargetIndex: Integer);
+
+ property PredefinedMapList: TEnumerable read GetPredefinedMapList;
+ property MapList: TEnumerable read GetMapList;
+ property MapCount: Integer read GetMapCount;
+ property Map[Index: Integer]: TGameMap read GetMap;
end;
@@ -64,7 +78,9 @@ implementation
uses
System.Classes,
System.IniFiles,
- System.SysUtils;
+ System.SysUtils,
+
+ UDKIniFile;
const
@@ -92,17 +108,17 @@ constructor TChivalryGame.Create(const ALocation: string);
begin
inherited Create(ALocation);
- FSupportedMapList := TObjectList.Create(True);
+ FPredefinedMapList := TObjectList.Create(True);
FMapList := TObjectList.Create(True);
- LoadSupportedMapList(FSupportedMapList);
+ LoadPredefinedMapList(FPredefinedMapList);
end;
destructor TChivalryGame.Destroy;
begin
FreeAndNil(FMapList);
- FreeAndNil(FSupportedMapList);
+ FreeAndNil(FPredefinedMapList);
inherited Destroy;
end;
@@ -110,40 +126,45 @@ end;
procedure TChivalryGame.DoLoad;
var
- gameSettings: TMemIniFile;
- engineSettings: TMemIniFile;
+ gameSettings: TUDKIniFile;
+ engineSettings: TUDKIniFile;
mapListValues: TStringList;
- valueIndex: Integer;
+ mapName: string;
+ mapListChanged: Boolean;
begin
- gameSettings := TMemIniFile.Create(Location + GameSettingsFileName);
+ gameSettings := TUDKIniFile.Create(Location + GameSettingsFileName);
try
- FServerPort := gameSettings.ReadInteger(GameURL, GameURLPort, GameURLPortDefault);
- FPeerPort := gameSettings.ReadInteger(GameURL, GameURLPeerPort, GameURLPeerPortDefault);
+ SetServerPort(gameSettings.ReadInteger(GameURL, GameURLPort, GameURLPortDefault));
+ SetPeerPort(gameSettings.ReadInteger(GameURL, GameURLPeerPort, GameURLPeerPortDefault));
- FServerName := gameSettings.ReadString(GameReplicationInfo, GameReplicationInfoServerName, '');
- FMessageOfTheDay := gameSettings.ReadString(GameReplicationInfo, GameReplicationInfoMessageOfTheDay, '');
+ SetServerName(gameSettings.ReadString(GameReplicationInfo, GameReplicationInfoServerName, ''));
+ SetMessageOfTheDay(gameSettings.ReadString(GameReplicationInfo, GameReplicationInfoMessageOfTheDay, ''));
+
+ mapListChanged := (FMapList.Count > 0);
{ Maplist is special; it occurs multiple times and order matters }
mapListValues := TStringList.Create;
try
- gameSettings.ReadSectionValues(GameAOCGame, mapListValues);
-
- for valueIndex := 0 to Pred(mapListValues.Count) do
+ gameSettings.ReadDuplicateStrings(GameAOCGame, GameAOCGameMapList, mapListValues);
+ for mapName in mapListValues do
begin
- if SameText(mapListValues.Names[valueIndex], GameAOCGameMapList) then
- FMapList.Add(CreateGameMap(mapListValues.ValueFromIndex[valueIndex]));
+ FMapList.Add(CreateGameMap(mapName));
+ mapListChanged := True;
end;
finally
FreeAndNil(mapListValues);
end;
+
+ if mapListChanged then
+ PropertyChanged('MapList');
finally
FreeAndNil(gameSettings);
end;
- engineSettings := TMemIniFile.Create(Location + EngineSettingsFileName);
+ engineSettings := TUDKIniFile.Create(Location + EngineSettingsFileName);
try
- FQueryPort := engineSettings.ReadInteger(EngineSteam, EngineSteamQueryPort, EngineSteamQueryPortDefault);
+ SetQueryPort(engineSettings.ReadInteger(EngineSteam, EngineSteamQueryPort, EngineSteamQueryPortDefault));
finally
FreeAndNil(engineSettings);
end;
@@ -151,8 +172,39 @@ end;
procedure TChivalryGame.DoSave;
+var
+ gameSettings: TUDKIniFile;
+ engineSettings: TUDKIniFile;
+ map: TGameMap;
+
begin
- // #ToDo1 -oMvR: 30-6-2014: save to INI files
+ gameSettings := TUDKIniFile.Create(Location + GameSettingsFileName);
+ try
+ gameSettings.WriteInteger(GameURL, GameURLPort, ServerPort);
+ gameSettings.WriteInteger(GameURL, GameURLPeerPort, PeerPort);
+
+ gameSettings.WriteString(GameReplicationInfo, GameReplicationInfoServerName, ServerName);
+ gameSettings.WriteString(GameReplicationInfo, GameReplicationInfoMessageOfTheDay, MessageOfTheDay);
+
+ { Remove all Maplist references before rewriting the list }
+ gameSettings.DeleteDuplicateKeys(GameAOCGame, GameAOCGameMapList);
+
+ for map in MapList do
+ gameSettings.WriteDuplicateString(GameAOCGame, GameAOCGameMapList, map.Name);
+
+ gameSettings.UpdateFile;
+ finally
+ FreeAndNil(gameSettings);
+ end;
+
+ engineSettings := TUDKIniFile.Create(Location + EngineSettingsFileName);
+ try
+ engineSettings.WriteInteger(EngineSteam, EngineSteamQueryPort, QueryPort);
+
+ engineSettings.UpdateFile;
+ finally
+ FreeAndNil(engineSettings);
+ end;
end;
@@ -163,7 +215,7 @@ var
begin
Result := nil;
- for map in GetSupportedMapList do
+ for map in PredefinedMapList do
if SameText(map.Name, AMapName) then
begin
Result := TGameMap.Create(map);
@@ -199,7 +251,7 @@ begin
if Value <> FServerPort then
begin
FServerPort := Value;
- Notify('ServerPort');
+ PropertyChanged('ServerPort');
end;
end;
@@ -209,7 +261,7 @@ begin
if Value <> FPeerPort then
begin
FPeerPort := Value;
- Notify('PeerPort');
+ PropertyChanged('PeerPort');
end;
end;
@@ -219,7 +271,7 @@ begin
if Value <> FQueryPort then
begin
FQueryPort := Value;
- Notify('QueryPort');
+ PropertyChanged('QueryPort');
end;
end;
@@ -241,7 +293,7 @@ begin
if Value <> FServerName then
begin
FServerName := Value;
- Notify('ServerName');
+ PropertyChanged('ServerName');
end;
end;
@@ -251,20 +303,70 @@ begin
if Value <> FMessageOfTheDay then
begin
FMessageOfTheDay := Value;
- Notify('MessageOfTheDay');
+ PropertyChanged('MessageOfTheDay');
end;
end;
-function TChivalryGame.GetMapList: TList;
+function TChivalryGame.GetPredefinedMapList: TEnumerable;
+begin
+ Result := FPredefinedMapList;
+end;
+
+
+function TChivalryGame.GetMapList: TEnumerable;
begin
Result := FMapList;
end;
-function TChivalryGame.GetSupportedMapList: TList;
+function TChivalryGame.GetMapCount: Integer;
begin
- Result := FSupportedMapList;
+ Result := FMapList.Count;
+end;
+
+
+function TChivalryGame.GetMap(Index: Integer): TGameMap;
+begin
+ Result := FMapList[Index];
+end;
+
+
+procedure TChivalryGame.AddMap(AMap: TGameMap);
+begin
+ FMapList.Add(AMap);
+ PropertyChanged('MapList');
+end;
+
+
+procedure TChivalryGame.InsertMap(Index: Integer; AMap: TGameMap);
+begin
+ FMapList.Insert(Index, AMap);
+ PropertyChanged('MapList');
+end;
+
+
+procedure TChivalryGame.DeleteMap(AIndex: Integer);
+begin
+ FMapList.Delete(AIndex);
+ PropertyChanged('MapList');
+end;
+
+
+procedure TChivalryGame.RemoveMap(AMap: TGameMap);
+begin
+ if FMapList.Remove(AMap) > -1 then
+ PropertyChanged('MapList');
+end;
+
+
+procedure TChivalryGame.MoveMap(ASourceIndex, ATargetIndex: Integer);
+begin
+ if ATargetIndex <> ASourceIndex then
+ begin
+ FMapList.Move(ASourceIndex, ATargetIndex);
+ PropertyChanged('MapList');
+ end;
end;
end.
diff --git a/source/model/Game.Intf.pas b/source/model/Game.Intf.pas
index d5ebfad..a521a0d 100644
--- a/source/model/Game.Intf.pas
+++ b/source/model/Game.Intf.pas
@@ -54,8 +54,23 @@ type
IGameMapList = interface
['{E8552B4C-9447-4FAD-BB20-C5EB3AF07B0E}']
- function GetSupportedMapList: TList;
- function GetMapList: TList;
+ function GetPredefinedMapList: TEnumerable;
+ function GetMapList: TEnumerable;
+
+ function GetMapCount: Integer;
+ function GetMap(Index: Integer): TGameMap;
+
+ procedure AddMap(AMap: TGameMap);
+ procedure InsertMap(Index: Integer; AMap: TGameMap);
+ procedure DeleteMap(AIndex: Integer);
+ procedure RemoveMap(AMap: TGameMap);
+ procedure MoveMap(ASourceIndex, ATargetIndex: Integer);
+
+ property PredefinedMapList: TEnumerable read GetPredefinedMapList;
+ property MapList: TEnumerable read GetMapList;
+
+ property MapCount: Integer read GetMapCount;
+ property Map[Index: Integer]: TGameMap read GetMap;
end;
diff --git a/source/view/Forms.Main.dfm b/source/view/Forms.Main.dfm
index 24e0778..03268ff 100644
--- a/source/view/Forms.Main.dfm
+++ b/source/view/Forms.Main.dfm
@@ -15,6 +15,7 @@ object MainForm: TMainForm
OldCreateOrder = False
Position = poScreenCenter
ShowHint = True
+ OnCloseQuery = FormCloseQuery
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
@@ -65,7 +66,7 @@ object MainForm: TMainForm
Top = 76
Width = 565
Height = 428
- ActivePage = tsMapList
+ ActivePage = tsAbout
Align = alClient
Style = tsButtons
TabOrder = 2
@@ -175,7 +176,6 @@ object MainForm: TMainForm
Align = alRight
BevelOuter = bvNone
TabOrder = 2
- ExplicitLeft = 415
inline frmMapPreview: TMapPreviewFrame
Left = 0
Top = 0
@@ -183,7 +183,6 @@ object MainForm: TMainForm
Height = 130
Align = alTop
TabOrder = 0
- ExplicitTop = 216
end
object btnMapListSave: TButton
AlignWithMargins = True
@@ -198,9 +197,6 @@ object MainForm: TMainForm
Action = actMapListSave
Align = alTop
TabOrder = 2
- ExplicitLeft = 28
- ExplicitTop = 168
- ExplicitWidth = 75
end
object btnMapListLoad: TButton
AlignWithMargins = True
@@ -215,9 +211,6 @@ object MainForm: TMainForm
Action = actMapListLoad
Align = alTop
TabOrder = 1
- ExplicitLeft = 28
- ExplicitTop = 168
- ExplicitWidth = 75
end
end
end
@@ -600,135 +593,167 @@ object MainForm: TMainForm
ImageIndex = 4
object lblJCL: TLabel
Left = 12
- Top = 222
+ Top = 262
Width = 319
Height = 13
Caption = 'Uses JEDI Code Library (JCL) and Visual Component Library (JVCL)'
end
object lblVirtualTreeview: TLabel
Left = 12
- Top = 334
+ Top = 358
Width = 179
Height = 13
Caption = 'Uses Virtual Treeview by Mike Lischke'
end
object lblChivalry: TLabel
Left = 12
- Top = 16
+ Top = 80
Width = 246
Height = 13
Caption = 'Chivalry: Medieval Warfare by Torn Banner Studios'
end
object lblPixelophilia: TLabel
Left = 12
- Top = 72
+ Top = 128
Width = 293
Height = 13
Caption = 'Various icons are part of the Pixelophilia packs by '#214'mer '#199'etin'
end
object lblGentleface: TLabel
Left = 12
- Top = 147
+ Top = 195
Width = 217
Height = 13
Caption = 'Toolbar icons are part of the Gentleface pack'
end
object lblSuperObject: TLabel
Left = 12
- Top = 278
+ Top = 310
Width = 176
Height = 13
Caption = 'Uses SuperObject by Henri Gourvest'
end
+ object lblProductName: TLabel
+ Left = 12
+ Top = 12
+ Width = 148
+ Height = 13
+ Caption = ''
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -11
+ Font.Name = 'Tahoma'
+ Font.Style = [fsBold]
+ ParentFont = False
+ end
+ object lblCopyright: TLabel
+ Left = 12
+ Top = 27
+ Width = 104
+ Height = 13
+ Caption = ''
+ end
object llJCL: TLinkLabel
Left = 12
- Top = 241
+ Top = 277
Width = 101
Height = 17
Caption = 'www.delphi-jedi.org'
- TabOrder = 5
+ TabOrder = 6
TabStop = True
OnLinkClick = llLinkClick
end
object llVirtualTreeview: TLinkLabel
Left = 12
- Top = 353
+ Top = 373
Width = 274
Height = 17
Caption =
'www.soft-gems.net/index.php/controls/virtual-treeview'
- TabOrder = 7
+ TabOrder = 8
TabStop = True
OnLinkClick = llLinkClick
end
object llChivalry: TLinkLabel
Left = 12
- Top = 35
+ Top = 95
Width = 109
Height = 17
Caption = 'www.tornbanner.com'
- TabOrder = 0
+ TabOrder = 1
TabStop = True
OnLinkClick = llLinkClick
end
object llPixelophilia: TLinkLabel
Left = 12
- Top = 91
+ Top = 143
Width = 128
Height = 17
Caption =
'omercetin.deviantart.' +
'com'
- TabOrder = 1
+ TabOrder = 2
TabStop = True
OnLinkClick = llLinkClick
end
object llGentleface: TLinkLabel
Left = 12
- Top = 166
+ Top = 210
Width = 172
Height = 17
Caption =
'gentleface.co' +
'm/free_icon_set.html'
- TabOrder = 3
+ TabOrder = 4
TabStop = True
OnLinkClick = llLinkClick
end
object llPixelophiliaCC: TLinkLabel
Left = 12
- Top = 110
+ Top = 158
Width = 303
Height = 17
Caption =
'Crea' +
'tive Commons Attribution-Noncommercial-No Derivate 3.0'
- TabOrder = 2
+ TabOrder = 3
TabStop = True
OnLinkClick = llLinkClick
end
object llGentlefaceCC: TLinkLabel
Left = 12
- Top = 185
+ Top = 225
Width = 242
Height = 17
Caption =
'Creativ' +
'e Commons Attribution-Noncommercial 3.0'
- TabOrder = 4
+ TabOrder = 5
TabStop = True
OnLinkClick = llLinkClick
end
object llSuperObject: TLinkLabel
Left = 12
- Top = 297
+ Top = 325
Width = 157
Height = 17
Caption =
'code.google.com' +
'/p/superobject'
- TabOrder = 6
+ TabOrder = 7
+ TabStop = True
+ OnLinkClick = llLinkClick
+ end
+ object llWebsite: TLinkLabel
+ Left = 12
+ Top = 43
+ Width = 213
+ Height = 17
+ Caption =
+ 'wiki' +
+ '.x2software.net/chivalryserverlauncher'
+ TabOrder = 0
TabStop = True
OnLinkClick = llLinkClick
end
@@ -1689,7 +1714,7 @@ object MainForm: TMainForm
end
object btnSave: TButton
AlignWithMargins = True
- Left = 533
+ Left = 434
Top = 0
Width = 91
Height = 25
@@ -1701,6 +1726,20 @@ object MainForm: TMainForm
Align = alRight
TabOrder = 2
end
+ object btnRevert: TButton
+ AlignWithMargins = True
+ Left = 533
+ Top = 0
+ Width = 91
+ Height = 25
+ Margins.Left = 0
+ Margins.Top = 0
+ Margins.Right = 8
+ Margins.Bottom = 0
+ Action = actRevert
+ Align = alRight
+ TabOrder = 3
+ end
end
object mbpMenuPainter: TX2MenuBarmusikCubePainter
Left = 44
@@ -2469,11 +2508,16 @@ object MainForm: TMainForm
end
object actSave: TAction
Caption = '&Save changes'
+ OnExecute = actSaveExecute
end
object actClose: TAction
Caption = '&Close'
OnExecute = actCloseExecute
end
+ object actRevert: TAction
+ Caption = '&Revert changes'
+ OnExecute = actRevertExecute
+ end
end
object pmnLaunch: TPopupMenu
Left = 144
diff --git a/source/view/Forms.Main.pas b/source/view/Forms.Main.pas
index 22f87f5..e4a7e20 100644
--- a/source/view/Forms.Main.pas
+++ b/source/view/Forms.Main.pas
@@ -134,9 +134,15 @@ type
btnMapListLoad: TButton;
actMapListLoad: TAction;
actMapListSave: TAction;
+ btnRevert: TButton;
+ actRevert: TAction;
+ llWebsite: TLinkLabel;
+ lblProductName: TLabel;
+ lblCopyright: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
+ procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure mbMenuCollapsing(Sender: TObject; Group: TX2MenuBarGroup; var Allowed: Boolean);
procedure mbMenuSelectedChanged(Sender: TObject; Item: TX2CustomMenuBarItem);
procedure llLinkClick(Sender: TObject; const Link: string; LinkType: TSysLinkType);
@@ -159,6 +165,8 @@ type
procedure actMapRemoveExecute(Sender: TObject);
procedure actMapUpExecute(Sender: TObject);
procedure actMapDownExecute(Sender: TObject);
+ procedure actSaveExecute(Sender: TObject);
+ procedure actRevertExecute(Sender: TObject);
private type
TBindingExpressionList = TList;
TPageMenuDictionary = TDictionary;
@@ -167,10 +175,14 @@ type
FPageMenuMap: TPageMenuDictionary;
FUIBindings: TBindingExpressionList;
+ function GetModified: Boolean;
procedure SetActiveGame(const Value: TCustomGame);
+ procedure SetModified(const Value: Boolean);
protected
procedure EnablePageActions;
procedure ActiveGameChanged;
+ procedure SaveActiveGame;
+ procedure RevertActiveGame;
procedure ClearUIBindings;
procedure Bind(const APropertyName: string; ADestObject: TObject; const ADestPropertyName: string);
@@ -181,6 +193,8 @@ type
procedure UpdateGameList;
procedure UpdateMapList;
+ procedure SaveGameList;
+
function FindMapNode(AMap: TGameMap): PVirtualNode;
procedure HandleMapSelection(ANodes: TNodeArray; ATargetIndex: Integer; ACopy: Boolean);
@@ -190,6 +204,8 @@ type
property ActiveGame: TCustomGame read FActiveGame write SetActiveGame;
property PageMenuMap: TPageMenuDictionary read FPageMenuMap;
property UIBindings: TBindingExpressionList read FUIBindings;
+ public
+ property Modified: Boolean read GetModified write SetModified;
end;
@@ -204,6 +220,7 @@ uses
Winapi.ShellAPI,
Winapi.Windows,
+ X2UtApp,
X2UtGraphics,
Forms.Game,
@@ -280,6 +297,9 @@ begin
lightBtnFace := BlendColors(clBtnFace, clWindow, 196);
pnlGamesWarning.Color := lightBtnFace;
+ lblProductName.Caption := App.Version.FormatVersion(False, True);
+ lblCopyright.Caption := App.Version.Strings.LegalCopyright;
+
{ Load games }
userGamesFileName := Resources.GetUserDataPath(Resources.UserGamesFileName);
@@ -310,6 +330,13 @@ begin
end;
+procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
+begin
+ if Modified then
+ CanClose := (MessageBox(Self.Handle, 'Your changes will not be saved. Do you want to exit?', 'Close', MB_YESNO or MB_ICONQUESTION) = ID_YES);
+end;
+
+
procedure TMainForm.EnablePageActions;
const
ActionListState: array[Boolean] of TActionListState = (asSuspended, asNormal);
@@ -329,7 +356,14 @@ begin
if Assigned(ActiveGame) and (not ActiveGame.Loaded) then
ActiveGame.Load;
- // #ToDo1 -oMvR: 30-6-2014: attach observer to monitor changes
+ { Bind Modified property }
+ UIBindings.Add(TBindings.CreateManagedBinding(
+ [TBindings.CreateAssociationScope([Associate(ActiveGame, 'src')])],
+ 'src.Modified',
+ [TBindings.CreateAssociationScope([Associate(Self, 'dst')])],
+ 'dst.Modified',
+ nil, nil, [coNotifyOutput, coEvaluate]));
+
if Supports(ActiveGame, IGameNetwork) then
BindGameNetwork;
@@ -344,6 +378,26 @@ begin
end;
+procedure TMainForm.SaveActiveGame;
+begin
+ if Assigned(ActiveGame) then
+ ActiveGame.Save;
+end;
+
+
+procedure TMainForm.RevertActiveGame;
+begin
+ if Assigned(ActiveGame) then
+ begin
+ ActiveGame.Load;
+
+ // #ToDo2 -oMvR: 2-7-2014: This should be based on the observer pattern used by TBindings,
+ // but I haven't figured out how that works yet. For now, manually refresh.
+ UpdateMapList;
+ end;
+end;
+
+
procedure TMainForm.ClearUIBindings;
var
binding: TBindingExpression;
@@ -425,12 +479,19 @@ var
begin
if Supports(ActiveGame, IGameMapList, gameMapList) then
- vstMapList.RootNodeCount := gameMapList.GetMapList.Count
+ vstMapList.RootNodeCount := gameMapList.MapCount
else
vstMapList.Clear;
end;
+procedure TMainForm.SaveGameList;
+begin
+ TGameListPersist.Save(Resources.GetUserDataPath(Resources.UserGamesFileName), TGameList.Instance);
+ UpdateGameList;
+end;
+
+
function TMainForm.FindMapNode(AMap: TGameMap): PVirtualNode;
var
gameMapList: IGameMapList;
@@ -443,7 +504,7 @@ begin
Result := vstMapList.IterateSubtree(nil,
procedure(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean)
begin
- Abort := (gameMapList.GetMapList[Node^.Index] = AMap);
+ Abort := (gameMapList.Map[Node^.Index] = AMap);
end,
nil);
end;
@@ -483,8 +544,8 @@ begin
{ Copy map nodes }
Inc(sourceIndex, sourceShift);
- map := TGameMap.Create(gameMapList.GetMapList[sourceIndex]);
- gameMapList.GetMapList.Insert(targetIndex, map);
+ map := TGameMap.Create(gameMapList.Map[sourceIndex]);
+ gameMapList.InsertMap(targetIndex, map);
selectedMaps.Add(map);
Inc(targetIndex);
@@ -507,8 +568,8 @@ begin
Inc(targetIndex);
end;
- selectedMaps.Add(gameMapList.GetMapList[sourceIndex]);
- gameMapList.GetMapList.Move(sourceIndex, newIndex);
+ selectedMaps.Add(gameMapList.Map[sourceIndex]);
+ gameMapList.MoveMap(sourceIndex, newIndex);
end;
end;
@@ -517,7 +578,7 @@ begin
vstMapList.IterateSubtree(nil,
procedure(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean)
begin
- if selectedMaps.Contains(gameMapList.GetMapList[Node^.Index]) then
+ if selectedMaps.Contains(gameMapList.Map[Node^.Index]) then
Sender.Selected[Node] := True;
end,
nil);
@@ -542,6 +603,12 @@ begin
end;
+function TMainForm.GetModified: Boolean;
+begin
+ Result := actSave.Enabled;
+end;
+
+
procedure TMainForm.SetActiveGame(const Value: TCustomGame);
begin
if Value <> FActiveGame then
@@ -552,6 +619,13 @@ begin
end;
+procedure TMainForm.SetModified(const Value: Boolean);
+begin
+ actSave.Enabled := Value;
+ actRevert.Enabled := Value;
+end;
+
+
procedure TMainForm.mbMenuCollapsing(Sender: TObject; Group: TX2MenuBarGroup; var Allowed: Boolean);
begin
Allowed := False;
@@ -589,6 +663,21 @@ begin
end;
+procedure TMainForm.actSaveExecute(Sender: TObject);
+begin
+ SaveActiveGame;
+end;
+
+
+procedure TMainForm.actRevertExecute(Sender: TObject);
+begin
+ if MessageBox(Self.Handle, 'Do you want to revert your changes?', 'Revert', MB_YESNO or MB_ICONQUESTION) = ID_NO then
+ exit;
+
+ RevertActiveGame;
+end;
+
+
procedure TMainForm.actGameAddExecute(Sender: TObject);
var
game: TCustomGame;
@@ -597,10 +686,7 @@ begin
if TGameForm.Insert(Self, game) then
begin
TGameList.Instance.Add(game);
-
- // #ToDo1 -oMvR: 30-6-2014: move to shared spot
- TGameListPersist.Save(Resources.GetUserDataPath(Resources.UserGamesFileName), TGameList.Instance);
- UpdateGameList;
+ SaveGameList;
ActiveGame := game;
end;
@@ -633,10 +719,7 @@ begin
ActiveGame := nil;
end;
- // #ToDo1 -oMvR: 30-6-2014: move to shared spot
- TGameListPersist.Save(Resources.GetUserDataPath(Resources.UserGamesFileName), TGameList.Instance);
-
- UpdateGameList;
+ SaveGameList;
finally
vstGames.EndUpdate;
end;
@@ -648,6 +731,22 @@ procedure TMainForm.actGameActiveExecute(Sender: TObject);
begin
if Assigned(vstGames.FocusedNode) then
begin
+ { For the sake of clarity, always save or revert changes when switching }
+ if Modified then
+ begin
+ case MessageBox(Self.Handle, 'Do you want to save your changes before switching the active game?', 'Set active game',
+ MB_YESNOCANCEL or MB_ICONQUESTION) of
+ ID_YES:
+ SaveActiveGame;
+
+ ID_NO:
+ RevertActiveGame;
+
+ ID_CANCEL:
+ exit;
+ end;
+ end;
+
vstGames.BeginUpdate;
try
ActiveGame := TGameList.Instance[vstGames.FocusedNode^.Index];
@@ -698,7 +797,7 @@ begin
if not Supports(ActiveGame, IGameMapList, gameMapList) then
exit;
- map := gameMapList.GetMapList[Node^.Index];
+ map := gameMapList.Map[Node^.Index];
case Column of
MapColumnName:
@@ -782,7 +881,10 @@ begin
if not Supports(ActiveGame, IGameMapList, gameMapList) then
exit;
- frmMapPreview.Load(gameMapList.GetMapList[Node^.Index].Name);
+ if Assigned(Node) then
+ frmMapPreview.Load(gameMapList.Map[Node^.Index].Name)
+ else
+ frmMapPreview.Clear;
end;
@@ -798,7 +900,7 @@ begin
if TMapForm.Insert(Self, gameMapList, map) then
begin
- gameMapList.GetMapList.Add(map);
+ gameMapList.AddMap(map);
UpdateMapList;
node := FindMapNode(map);
@@ -832,7 +934,7 @@ begin
selectedNodes := vstMapList.GetSortedSelection(True);
for nodeIndex := High(selectedNodes) downto Low(selectedNodes) do
- gameMapList.GetMapList.Delete(selectedNodes[nodeIndex]^.Index);
+ gameMapList.DeleteMap(selectedNodes[nodeIndex]^.Index);
UpdateMapList;
finally
diff --git a/source/view/Forms.Map.dfm b/source/view/Forms.Map.dfm
index df24f7f..184a92d 100644
--- a/source/view/Forms.Map.dfm
+++ b/source/view/Forms.Map.dfm
@@ -87,8 +87,6 @@ object MapForm: TMapForm
OnFocusChanging = vstMapFocusChanging
OnGetText = vstMapGetText
OnPaintText = vstMapPaintText
- ExplicitLeft = 8
- ExplicitWidth = 561
Columns = <>
end
object pnlMapName: TPanel
@@ -105,7 +103,6 @@ object MapForm: TMapForm
AutoSize = True
BevelOuter = bvNone
TabOrder = 2
- ExplicitTop = 475
DesignSize = (
561
21)
@@ -173,8 +170,6 @@ object MapForm: TMapForm
Align = alLeft
BevelOuter = bvNone
TabOrder = 4
- ExplicitLeft = 0
- ExplicitHeight = 342
inline frmMapPreview: TMapPreviewFrame
AlignWithMargins = True
Left = 0
@@ -187,7 +182,7 @@ object MapForm: TMapForm
Margins.Bottom = 0
Align = alTop
TabOrder = 0
- ExplicitWidth = 342
+ ExplicitTop = 8
end
end
end
diff --git a/source/view/Forms.Map.pas b/source/view/Forms.Map.pas
index f5f5d68..da1a45d 100644
--- a/source/view/Forms.Map.pas
+++ b/source/view/Forms.Map.pas
@@ -47,7 +47,7 @@ type
function GetMapName: string;
procedure SetMapName(const Value: string);
protected
- procedure LoadSupportedMapList(AGame: IGameMapList);
+ procedure LoadPredefinedMapList(AGame: IGameMapList);
function CreateMap: TGameMap;
function FindMapNode(const AMapName: string): PVirtualNode;
@@ -82,7 +82,7 @@ class function TMapForm.Insert(AOwner: TComponent; AGame: IGameMapList; out AMap
begin
with Self.Create(AOwner) do
try
- LoadSupportedMapList(AGame);
+ LoadPredefinedMapList(AGame);
Result := (ShowModal = mrOk);
if Result then
@@ -99,7 +99,7 @@ begin
end;
-procedure TMapForm.LoadSupportedMapList(AGame: IGameMapList);
+procedure TMapForm.LoadPredefinedMapList(AGame: IGameMapList);
var
map: TGameMap;
categoryNodes: TDictionary;
@@ -113,7 +113,7 @@ begin
categoryNodes := TDictionary.Create;
try
- for map in AGame.GetSupportedMapList do
+ for map in AGame.PredefinedMapList do
begin
if categoryNodes.ContainsKey(map.Category) then
parentNode := categoryNodes[map.Category]
@@ -130,6 +130,7 @@ begin
end;
finally
vstMap.FullExpand;
+ vstMap.EndUpdate;
node := vstMap.GetFirstLevel(1);
if Assigned(node) then
@@ -137,8 +138,6 @@ begin
vstMap.FocusedNode := node;
vstMap.Selected[node] := True;
end;
-
- vstMap.EndUpdate;
end;
end;