Added: game list - adding, removing and persisting

Added: auto-detection of game location
This commit is contained in:
Mark van Renswoude 2014-06-30 16:07:40 +00:00
parent 75f07a0680
commit 736cb4b231
10 changed files with 308 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,7 +59,6 @@ object GameForm: TGameForm
Style = csDropDownList
Anchors = [akLeft, akTop, akRight]
TabOrder = 0
ExplicitWidth = 509
end
object deLocation: TJvDirectoryEdit
Left = 108

View File

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

View File

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

View File

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