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;