From 736cb4b2318351173a2e1df8dd58d1a70944a707 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Mon, 30 Jun 2014 16:07:40 +0000 Subject: [PATCH] Added: game list - adding, removing and persisting Added: auto-detection of game location --- source/Resources.pas | 12 ++ source/model/Game.Base.pas | 31 ++++- .../model/Game.Chivalry.MedievalWarfare.pas | 49 ++++++- source/model/Game.List.pas | 21 ++- source/model/Game.Registry.pas | 17 +++ source/persist/Persist.GameList.pas | 54 +++++++- source/view/Forms.Game.dfm | 1 - source/view/Forms.Game.pas | 2 +- source/view/Forms.Main.dfm | 22 ++- source/view/Forms.Main.pas | 126 ++++++++++++++++-- 10 files changed, 308 insertions(+), 27 deletions(-) diff --git a/source/Resources.pas b/source/Resources.pas index 310e2d2..d7cf7c9 100644 --- a/source/Resources.pas +++ b/source/Resources.pas @@ -3,9 +3,15 @@ unit Resources; interface const AssetsPath = 'assets\'; + AssetChivalryMedievalWarfareMapListFileName = 'Chivalry.MedievalWarfare.MapList.ini'; + + + UserDataPath = 'Chivalry Server Launcher\'; + UserGamesFileName = 'Games.json'; function GetAssetPath(const AAsset: string): string; + function GetUserDataPath(const AAsset: string): string; implementation uses @@ -17,4 +23,10 @@ begin Result := App.Path + AssetsPath + AAsset; end; + +function GetUserDataPath(const AAsset: string): string; +begin + Result := App.UserPath + UserDataPath + AAsset; +end; + end. diff --git a/source/model/Game.Base.pas b/source/model/Game.Base.pas index 7b88cfa..b3580c3 100644 --- a/source/model/Game.Base.pas +++ b/source/model/Game.Base.pas @@ -9,16 +9,22 @@ type TCustomGame = class(TInterfacedPersistent) private FLocation: string; + FLoaded: Boolean; protected procedure Notify(const APropertyName: string); virtual; + + procedure DoLoad; virtual; abstract; + procedure DoSave; virtual; abstract; public - class function GetGameName: string; virtual; abstract; + class function GameName: string; virtual; abstract; + class function AutoDetect: TCustomGame; virtual; constructor Create(const ALocation: string); virtual; - procedure Load; virtual; abstract; - procedure Save; virtual; abstract; + procedure Load; virtual; + procedure Save; virtual; + property Loaded: Boolean read FLoaded; property Location: string read FLocation; end; @@ -40,6 +46,25 @@ begin end; +class function TCustomGame.AutoDetect: TCustomGame; +begin + Result := nil; +end; + + +procedure TCustomGame.Load; +begin + DoLoad; + FLoaded := True; +end; + + +procedure TCustomGame.Save; +begin + DoSave; +end; + + procedure TCustomGame.Notify(const APropertyName: string); begin TBindings.Notify(Self, APropertyName); diff --git a/source/model/Game.Chivalry.MedievalWarfare.pas b/source/model/Game.Chivalry.MedievalWarfare.pas index 33a59e9..bdcae52 100644 --- a/source/model/Game.Chivalry.MedievalWarfare.pas +++ b/source/model/Game.Chivalry.MedievalWarfare.pas @@ -4,6 +4,7 @@ interface uses System.Generics.Collections, + Game.Base, Game.Chivalry, Game.Intf; @@ -16,7 +17,8 @@ type protected procedure LoadSupportedMapList(AList: TList); override; public - class function GetGameName: string; override; + class function GameName: string; override; + class function AutoDetect: TCustomGame; override; end; @@ -25,19 +27,60 @@ uses System.Classes, System.IniFiles, System.SysUtils, + System.Win.Registry, + Winapi.Windows, Resources, Game.Map, Game.Registry; +const + SteamKey = '\Software\Valve\Steam'; + SteamValuePath = 'SteamPath'; + + ChivalryServerSteamPath = 'steamapps\common\chivalry_ded_server\'; + + { TChivalryMedievalWarfareGame } -class function TChivalryMedievalWarfareGame.GetGameName: string; +class function TChivalryMedievalWarfareGame.GameName: string; begin Result := 'Chivalry: Medieval Warfare'; end; +class function TChivalryMedievalWarfareGame.AutoDetect: TCustomGame; +var + registry: TRegistry; + steamPath: string; + serverPath: string; + +begin + steamPath := ''; + + registry := TRegistry.Create(KEY_READ); + try + registry.RootKey := HKEY_CURRENT_USER; + if registry.OpenKeyReadOnly(SteamKey) then + try + if registry.ValueExists(SteamValuePath) then + steamPath := StringReplace(registry.ReadString(SteamValuePath), '/', '\', [rfReplaceAll]); + finally + registry.CloseKey; + end; + finally + FreeAndNil(registry); + end; + + if (Length(steamPath) > 0) then + begin + serverPath := IncludeTrailingPathDelimiter(steamPath) + ChivalryServerSteamPath; + if DirectoryExists(serverPath) then + Result := TChivalryMedievalWarfareGame.Create(serverPath); + end; +end; + + procedure TChivalryMedievalWarfareGame.LoadSupportedMapList(AList: TList); var mapListFileName: string; @@ -48,7 +91,7 @@ var mapIndex: Integer; begin - mapListFileName := GetAssetPath('Chivalry.MedievalWarfare.MapList.ini'); + mapListFileName := Resources.GetAssetPath(Resources.AssetChivalryMedievalWarfareMapListFileName); if not FileExists(mapListFileName) then exit; diff --git a/source/model/Game.List.pas b/source/model/Game.List.pas index 6494aa2..103a322 100644 --- a/source/model/Game.List.pas +++ b/source/model/Game.List.pas @@ -15,12 +15,16 @@ type class procedure Finalize; public class function Instance: TGameList; + + procedure AutoDetect; end; implementation uses - System.SysUtils; + System.SysUtils, + + Game.Registry; { TGameList } @@ -39,6 +43,21 @@ begin end; +procedure TGameList.AutoDetect; +var + gameClass: TCustomGameClass; + game: TCustomGame; + +begin + for gameClass in TGameRegistry.RegisteredGames do + begin + game := gameClass.AutoDetect; + if Assigned(game) then + Add(game); + end; +end; + + initialization finalization TGameList.Finalize; diff --git a/source/model/Game.Registry.pas b/source/model/Game.Registry.pas index 7c55960..adbe61d 100644 --- a/source/model/Game.Registry.pas +++ b/source/model/Game.Registry.pas @@ -22,6 +22,7 @@ type class procedure Finalize; public class function RegisteredGames: TGameRegistryEnumerable; + class function ByClassName(const AClassName: string): TCustomGameClass; class procedure Register(AGameClass: TCustomGameClass); class procedure Unregister(AGameClass: TCustomGameClass); @@ -54,6 +55,22 @@ begin end; +class function TGameRegistry.ByClassName(const AClassName: string): TCustomGameClass; +var + gameClass: TCustomGameClass; + +begin + Result := nil; + + for gameClass in RegisteredGames do + if SameText(gameClass.ClassName, AClassName) then + begin + Result := gameClass; + break; + end; +end; + + class procedure TGameRegistry.Register(AGameClass: TCustomGameClass); begin if not SRegisteredGames.Contains(AGameClass) then diff --git a/source/persist/Persist.GameList.pas b/source/persist/Persist.GameList.pas index 344adf4..6608baf 100644 --- a/source/persist/Persist.GameList.pas +++ b/source/persist/Persist.GameList.pas @@ -9,19 +9,71 @@ type TGameListPersist = class(TObject) public class procedure Load(const AFileName: string; AList: TGameList); + class procedure Save(const AFileName: string; AList: TGameList); end; implementation uses - System.SysUtils; + System.IOUtils, + System.SysUtils, + + superobject, + + Game.Base, + Game.Registry; + + +const + KeyClassName = 'className'; + KeyLocation = 'location'; { TGameListPersist } class procedure TGameListPersist.Load(const AFileName: string; AList: TGameList); +var + inputList: ISuperObject; + inputGame: ISuperObject; + gameClass: TCustomGameClass; + begin if not FileExists(AFileName) then exit; + + inputList := SO(TFile.ReadAllText(AFileName)); + if Assigned(inputList) then + begin + for inputGame in inputList do + begin + gameClass := TGameRegistry.ByClassName(inputGame.S[KeyClassName]); + if Assigned(gameClass) then + AList.Add(gameClass.Create(inputGame.S[KeyLocation])); + end; + end; +end; + + +class procedure TGameListPersist.Save(const AFileName: string; AList: TGameList); +var + outputList: ISuperObject; + outputGame: ISuperObject; + game: TCustomGame; + +begin + outputList := SA([]); + + for game in AList do + begin + outputGame := SO(); + outputGame.S[KeyClassName] := game.ClassName; + outputGame.S[KeyLocation] := game.Location; +// outputGame.B['active'] := True; + + outputList.AsArray.Add(outputGame); + end; + + if ForceDirectories(ExtractFilePath(AFileName)) then + TFile.WriteAllText(AFileName, outputList.AsJSon(True)); end; end. diff --git a/source/view/Forms.Game.dfm b/source/view/Forms.Game.dfm index 021e4fc..25cde63 100644 --- a/source/view/Forms.Game.dfm +++ b/source/view/Forms.Game.dfm @@ -59,7 +59,6 @@ object GameForm: TGameForm Style = csDropDownList Anchors = [akLeft, akTop, akRight] TabOrder = 0 - ExplicitWidth = 509 end object deLocation: TJvDirectoryEdit Left = 108 diff --git a/source/view/Forms.Game.pas b/source/view/Forms.Game.pas index 5a7f19a..2da2803 100644 --- a/source/view/Forms.Game.pas +++ b/source/view/Forms.Game.pas @@ -77,7 +77,7 @@ begin cmbGame.Items.Clear; for gameClass in TGameRegistry.RegisteredGames do - cmbGame.Items.AddObject(gameClass.GetGameName, TObject(gameClass)); + cmbGame.Items.AddObject(gameClass.GameName, TObject(gameClass)); finally cmbGame.Items.EndUpdate; cmbGame.ItemIndex := 0; diff --git a/source/view/Forms.Main.dfm b/source/view/Forms.Main.dfm index c3d4004..8355e5e 100644 --- a/source/view/Forms.Main.dfm +++ b/source/view/Forms.Main.dfm @@ -43,7 +43,7 @@ object MainForm: TMainForm Top = 75 Width = 565 Height = 472 - ActivePage = tsAbout + ActivePage = tsGames Align = alClient Style = tsButtons TabOrder = 2 @@ -119,7 +119,6 @@ object MainForm: TMainForm object tsConfiguration: TTabSheet Caption = 'Server - Configuration' ImageIndex = 1 - ExplicitLeft = 6 object gbServerName: TGroupBox AlignWithMargins = True Left = 8 @@ -202,7 +201,7 @@ object MainForm: TMainForm object tsGames: TTabSheet Caption = 'Launcher - Game locations' ImageIndex = 3 - object VirtualStringTree2: TVirtualStringTree + object vstGames: TVirtualStringTree AlignWithMargins = True Left = 8 Top = 30 @@ -213,17 +212,29 @@ object MainForm: TMainForm Margins.Right = 8 Margins.Bottom = 8 Align = alClient - Header.AutoSizeIndex = 0 + Header.AutoSizeIndex = 1 Header.Font.Charset = DEFAULT_CHARSET Header.Font.Color = clWindowText Header.Font.Height = -11 Header.Font.Name = 'Tahoma' Header.Font.Style = [] - Header.Options = [hoColumnResize, hoDrag, hoShowSortGlyphs, hoVisible] + Header.Options = [hoAutoResize, hoColumnResize, hoDrag, hoShowSortGlyphs, hoVisible] TabOrder = 1 + TreeOptions.PaintOptions = [toHideFocusRect, toShowDropmark, toShowTreeLines, toThemeAware, toUseBlendedImages] + TreeOptions.SelectionOptions = [toFullRowSelect, toMiddleClickSelect, toRightClickSelect] + OnFocusChanged = vstGamesFocusChanged + OnGetText = vstGamesGetText + OnInitNode = vstGamesInitNode Columns = < item Position = 0 + Width = 200 + WideText = 'Game' + end + item + Position = 1 + Width = 337 + WideText = 'Location' end> end object pnlGamesWarning: TPanel @@ -240,6 +251,7 @@ object MainForm: TMainForm BevelOuter = bvLowered ParentBackground = False TabOrder = 2 + Visible = False object imgGamesWarning: TImage Left = 12 Top = 12 diff --git a/source/view/Forms.Main.pas b/source/view/Forms.Main.pas index b710851..10d8f5c 100644 --- a/source/view/Forms.Main.pas +++ b/source/view/Forms.Main.pas @@ -5,6 +5,7 @@ uses System.Bindings.Expression, System.Classes, System.Generics.Collections, + Vcl.ActnList, Vcl.ComCtrls, Vcl.Controls, Vcl.ExtCtrls, @@ -26,7 +27,7 @@ uses X2CLmusikCubeMenuBarPainter, Game.Base, - Game.Intf, Vcl.ActnList; + Game.Intf; type @@ -85,7 +86,7 @@ type tsMapList: TTabSheet; tsNetwork: TTabSheet; vstMapList: TVirtualStringTree; - VirtualStringTree2: TVirtualStringTree; + vstGames: TVirtualStringTree; alMain: TActionList; actGameAdd: TAction; actGameRemove: TAction; @@ -101,9 +102,12 @@ type procedure EditChange(Sender: TObject); procedure actGameAddExecute(Sender: TObject); procedure actGameRemoveExecute(Sender: TObject); + procedure vstGamesInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); + procedure vstGamesGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string); + procedure vstGamesFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); private type - TBindingExpressionList = TList; - TPageMenuMap = TDictionary; + TBindingExpressionList = TList; + TPageMenuMap = TDictionary; private FActiveGame: TCustomGame; FPageMenuMap: TPageMenuMap; @@ -120,6 +124,8 @@ type procedure BindGameMapList; procedure UpdateMenu; + procedure UpdateGameList; + property ActiveGame: TCustomGame read FActiveGame write SetActiveGame; property PageMenuMap: TPageMenuMap read FPageMenuMap; property UIBindings: TBindingExpressionList read FUIBindings; @@ -144,7 +150,8 @@ uses Forms.Game, Game.Chivalry.MedievalWarfare, Game.List, - Persist.GameList; + Persist.GameList, + Resources; type @@ -159,11 +166,18 @@ type end; + PCustomGame = ^TCustomGame; + + const INIHintPrefix = 'INI:'; INIHintSeparator = '>'; + GameColumnName = 0; + GameColumnLocation = 1; + + {$R *.dfm} @@ -174,7 +188,7 @@ var pageIndex: Integer; menuGroup: TX2MenuBarGroup; menuItem: TX2MenuBarItem; -// game: TCustomGame; + userGamesFileName: string; begin FUIBindings := TBindingExpressionList.Create; @@ -202,11 +216,19 @@ begin lightBtnFace := BlendColors(clBtnFace, clWindow, 196); pnlGamesWarning.Color := lightBtnFace; - // #ToDo1 -oMvR: 30-6-2014: load last active game -// game := TChivalryMedievalWarfareGame.Create('D:\Steam\steamapps\common\chivalry_ded_server'); -// game.Load; -// ActiveGame := game; + { Load games } + userGamesFileName := Resources.GetUserDataPath(Resources.UserGamesFileName); + if FileExists(userGamesFileName) then + TGameListPersist.Load(userGamesFileName, TGameList.Instance) + else + TGameList.Instance.AutoDetect; + + UpdateGameList; + + // #ToDo1 -oMvR: 30-6-2014: load last active game + if TGameList.Instance.Count > 0 then + ActiveGame := TGameList.Instance.First; { Initialize menu } mbpMenuPainter.GroupColors.Hot.Assign(mbpMenuPainter.GroupColors.Normal); @@ -227,6 +249,10 @@ end; procedure TMainForm.ActiveGameChanged; begin ClearUIBindings; + vstMapList.Clear; + + if Assigned(ActiveGame) and (not ActiveGame.Loaded) then + ActiveGame.Load; // #ToDo1 -oMvR: 30-6-2014: attach observer to monitor changes @@ -317,6 +343,15 @@ begin end; +procedure TMainForm.UpdateGameList; +begin + vstGames.NodeDataSize := SizeOf(TCustomGame); + vstGames.RootNodeCount := TGameList.Instance.Count; + + pnlGamesWarning.Visible := (TGameList.Instance.Count = 0); +end; + + procedure TMainForm.SetActiveGame(const Value: TCustomGame); begin if Value <> FActiveGame then @@ -364,14 +399,81 @@ var 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; + + ActiveGame := game; end; end; procedure TMainForm.actGameRemoveExecute(Sender: TObject); +var + nodeData: PCustomGame; + begin - // + if not Assigned(vstGames.FocusedNode) then + exit; + + nodeData := vstGames.GetNodeData(vstGames.FocusedNode); + if MessageBox(Self.Handle, 'Do you want to remove the selected game?', 'Remove', MB_YESNO or MB_ICONQUESTION) = ID_YES then + begin + vstGames.BeginUpdate; + try + TGameList.Instance.Remove(nodeData^); + + if nodeData^ = ActiveGame then + begin + if TGameList.Instance.Count > 0 then + ActiveGame := TGameList.Instance.First + else + ActiveGame := nil; + end; + + // #ToDo1 -oMvR: 30-6-2014: move to shared spot + TGameListPersist.Save(Resources.GetUserDataPath(Resources.UserGamesFileName), TGameList.Instance); + + UpdateGameList; + finally + vstGames.EndUpdate; + end; + end; +end; + + +procedure TMainForm.vstGamesInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); +var + nodeData: PCustomGame; + +begin + nodeData := Sender.GetNodeData(Node); + nodeData^ := TGameList.Instance[Node^.Index]; +end; + + +procedure TMainForm.vstGamesGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string); +var + nodeData: PCustomGame; + +begin + nodeData := Sender.GetNodeData(Node); + + case Column of + GameColumnName: + CellText := nodeData^.GameName; + + GameColumnLocation: + CellText := nodeData^.Location; + end; +end; + + +procedure TMainForm.vstGamesFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); +begin + actGameRemove.Enabled := Assigned(Sender.FocusedNode); end;