Added: tracking of Modified state

Added: saving / reverting changes
Fixed: access violation on preview with an empty map list
This commit is contained in:
Mark van Renswoude 2014-07-02 14:54:01 +00:00
parent 47129c0b32
commit 12eba2e832
12 changed files with 835 additions and 105 deletions

View File

@ -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}

View File

@ -44,7 +44,7 @@
<VerInfo_Keys>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</VerInfo_Keys>
<VerInfo_MinorVer>1</VerInfo_MinorVer>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1043</VerInfo_Locale>
<VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_MajorVer>0</VerInfo_MajorVer>
<Icon_MainIcon>resources\icons\MainIcon.ico</Icon_MainIcon>
<Manifest_File>None</Manifest_File>
@ -69,6 +69,7 @@
<DCC_RemoteDebug>true</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<DCC_DebugDCUs>true</DCC_DebugDCUs>
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
@ -104,6 +105,7 @@
<FormType>dfm</FormType>
<DesignClass>TFrame</DesignClass>
</DCCReference>
<DCCReference Include="source\UDKIniFile.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>

Binary file not shown.

444
source/UDKIniFile.pas Normal file
View File

@ -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.

View File

@ -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;

View File

@ -15,7 +15,7 @@ type
in the base Chivalry class. }
TChivalryMedievalWarfareGame = class(TChivalryGame)
protected
procedure LoadSupportedMapList(AList: TList<TGameMap>); override;
procedure LoadPredefinedMapList(AList: TList<TGameMap>); override;
public
class function GameName: string; override;
class function AutoDetect: TCustomGame; override;
@ -81,7 +81,7 @@ begin
end;
procedure TChivalryMedievalWarfareGame.LoadSupportedMapList(AList: TList<TGameMap>);
procedure TChivalryMedievalWarfareGame.LoadPredefinedMapList(AList: TList<TGameMap>);
var
mapListFileName: string;
mapList: TMemIniFile;

View File

@ -18,13 +18,13 @@ type
FServerName: string;
FMessageOfTheDay: string;
FSupportedMapList: TObjectList<TGameMap>;
FPredefinedMapList: TObjectList<TGameMap>;
FMapList: TObjectList<TGameMap>;
protected
procedure DoLoad; override;
procedure DoSave; override;
procedure LoadSupportedMapList(AList: TList<TGameMap>); virtual; abstract;
procedure LoadPredefinedMapList(AList: TList<TGameMap>); 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<TGameMap>;
function GetMapList: TList<TGameMap>;
function GetPredefinedMapList: TEnumerable<TGameMap>;
function GetMapList: TEnumerable<TGameMap>;
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<TGameMap> read GetPredefinedMapList;
property MapList: TEnumerable<TGameMap> 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<TGameMap>.Create(True);
FPredefinedMapList := TObjectList<TGameMap>.Create(True);
FMapList := TObjectList<TGameMap>.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<TGameMap>;
function TChivalryGame.GetPredefinedMapList: TEnumerable<TGameMap>;
begin
Result := FPredefinedMapList;
end;
function TChivalryGame.GetMapList: TEnumerable<TGameMap>;
begin
Result := FMapList;
end;
function TChivalryGame.GetSupportedMapList: TList<TGameMap>;
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.

View File

@ -54,8 +54,23 @@ type
IGameMapList = interface
['{E8552B4C-9447-4FAD-BB20-C5EB3AF07B0E}']
function GetSupportedMapList: TList<TGameMap>;
function GetMapList: TList<TGameMap>;
function GetPredefinedMapList: TEnumerable<TGameMap>;
function GetMapList: TEnumerable<TGameMap>;
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<TGameMap> read GetPredefinedMapList;
property MapList: TEnumerable<TGameMap> read GetMapList;
property MapCount: Integer read GetMapCount;
property Map[Index: Integer]: TGameMap read GetMap;
end;

View File

@ -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 = '<runtime: product name>'
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 = '<runtime: copyright>'
end
object llJCL: TLinkLabel
Left = 12
Top = 241
Top = 277
Width = 101
Height = 17
Caption = '<a href="http://www.delphi-jedi.org/">www.delphi-jedi.org</a>'
TabOrder = 5
TabOrder = 6
TabStop = True
OnLinkClick = llLinkClick
end
object llVirtualTreeview: TLinkLabel
Left = 12
Top = 353
Top = 373
Width = 274
Height = 17
Caption =
'<a href="http://www.soft-gems.net/index.php/controls/virtual-tre' +
'eview">www.soft-gems.net/index.php/controls/virtual-treeview</a>'
TabOrder = 7
TabOrder = 8
TabStop = True
OnLinkClick = llLinkClick
end
object llChivalry: TLinkLabel
Left = 12
Top = 35
Top = 95
Width = 109
Height = 17
Caption = '<a href="http://www.tornbanner.com/">www.tornbanner.com</a>'
TabOrder = 0
TabOrder = 1
TabStop = True
OnLinkClick = llLinkClick
end
object llPixelophilia: TLinkLabel
Left = 12
Top = 91
Top = 143
Width = 128
Height = 17
Caption =
'<a href="http://omercetin.deviantart.com/">omercetin.deviantart.' +
'com</a>'
TabOrder = 1
TabOrder = 2
TabStop = True
OnLinkClick = llLinkClick
end
object llGentleface: TLinkLabel
Left = 12
Top = 166
Top = 210
Width = 172
Height = 17
Caption =
'<a href="http://gentleface.com/free_icon_set.html">gentleface.co' +
'm/free_icon_set.html</a>'
TabOrder = 3
TabOrder = 4
TabStop = True
OnLinkClick = llLinkClick
end
object llPixelophiliaCC: TLinkLabel
Left = 12
Top = 110
Top = 158
Width = 303
Height = 17
Caption =
'<a href="http://creativecommons.org/licenses/by-nc-nd/3.0/">Crea' +
'tive Commons Attribution-Noncommercial-No Derivate 3.0</a>'
TabOrder = 2
TabOrder = 3
TabStop = True
OnLinkClick = llLinkClick
end
object llGentlefaceCC: TLinkLabel
Left = 12
Top = 185
Top = 225
Width = 242
Height = 17
Caption =
'<a href="http://creativecommons.org/licenses/by-nc/3.0/">Creativ' +
'e Commons Attribution-Noncommercial 3.0</a>'
TabOrder = 4
TabOrder = 5
TabStop = True
OnLinkClick = llLinkClick
end
object llSuperObject: TLinkLabel
Left = 12
Top = 297
Top = 325
Width = 157
Height = 17
Caption =
'<a href="https://code.google.com/p/superobject/">code.google.com' +
'/p/superobject</a>'
TabOrder = 6
TabOrder = 7
TabStop = True
OnLinkClick = llLinkClick
end
object llWebsite: TLinkLabel
Left = 12
Top = 43
Width = 213
Height = 17
Caption =
'<a href="http://wiki.x2software.net/chivalryserverlauncher">wiki' +
'.x2software.net/chivalryserverlauncher</a>'
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

View File

@ -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<TBindingExpression>;
TPageMenuDictionary = TDictionary<TTabSheet, TX2MenuBarItem>;
@ -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

View File

@ -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

View File

@ -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<string, PVirtualNode>;
@ -113,7 +113,7 @@ begin
categoryNodes := TDictionary<string, PVirtualNode>.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;