Skip to content

Commit 9fae7e9

Browse files
authored
Merge pull request #136 from sirloreal/feature/file-handling
Refactoring the file and game loading mechanism, with some test coverage.
2 parents 970d0e3 + 84b5221 commit 9fae7e9

13 files changed

+29611
-75
lines changed

Engine/Core.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
1515
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
1616
<PackageReference Include="NeoLua" Version="1.3.14" />
17+
<InternalsVisibleTo Include="Engine.Tests" />
1718
</ItemGroup>
1819

1920
<ItemGroup>

Engine/src/SaveLoad/GameLoader.cs

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.IO;
3+
using Civ2engine.IO;
4+
using Civ2engine.SaveLoad.SavFile;
5+
using Model;
6+
using Model.Core;
7+
using Model.InterfaceActions;
8+
9+
namespace Civ2engine.SaveLoad;
10+
11+
public interface IGameLoader
12+
{
13+
IInterfaceAction LoadGame(IGame game, IUserInterface activeInterface);
14+
}
15+
16+
public class GameLoader : IGameLoader
17+
{
18+
private string path;
19+
private string savDirectory; // Only needed for when dealing with scenario loading, not used for normal files.
20+
private Rules rules;
21+
private Ruleset activeRuleSet;
22+
SavFileBase savFile;
23+
24+
public GameLoader(string path, string savDirectory, Rules rules, Ruleset activeRuleset, SavFileBase savFile)
25+
{
26+
this.path = path;
27+
this.savDirectory = savDirectory;
28+
this.rules = rules;
29+
this.activeRuleSet = activeRuleset;
30+
this.savFile = savFile;
31+
}
32+
33+
public IInterfaceAction LoadGame(IGame game, IUserInterface activeInterface)
34+
{
35+
// TODO: This was only possible for a classic sav file. Was this intentional?
36+
if (string.Equals(Path.GetExtension(path), ".scn", StringComparison.OrdinalIgnoreCase))
37+
{
38+
var scnName = Path.GetFileName(path);
39+
return activeInterface.HandleLoadScenario(game, scnName, savDirectory);
40+
}
41+
return activeInterface.HandleLoadGame(game, rules, activeRuleSet, savFile.ViewData!);
42+
}
43+
}

Engine/src/SaveLoad/LoadGame.cs

+26-52
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.IO;
43
using System.Linq;
5-
using System.Text.Json;
64
using Civ2engine.IO;
7-
using Civ2engine.OriginalSaves;
5+
using Civ2engine.SaveLoad.SavFile;
86
using Model;
9-
using Model.Core;
107
using Model.InterfaceActions;
118

129
namespace Civ2engine.SaveLoad;
@@ -15,67 +12,44 @@ public static class LoadGame
1512
{
1613
public static IInterfaceAction LoadFrom(string path, IMain mainApp)
1714
{
18-
var savDirectory = Path.GetDirectoryName(path);
19-
var root = Settings.SearchPaths.FirstOrDefault(savDirectory.StartsWith) ?? Settings.SearchPaths[0];
20-
var fileData = File.ReadAllBytes(path);
21-
bool classicSave = fileData[0] == 67; // Classic saves start with the word CIVILIZE so if we see a C treat it as old
22-
23-
//TODO: File.ReadAllBytes above will throw a FileNotFoundException if the sav file doesn't exist.
24-
//So the fileData.Length check is not checking the right thing and the throw statement isn't really needed.
25-
if (fileData.Length == 0)
26-
throw new FileNotFoundException($"File {path} not found");
27-
28-
var extendedMetadata = new Dictionary<string, string>();
29-
30-
JsonDocument jsonDocument = null!;
31-
if (classicSave)
15+
if (File.Exists(path))
3216
{
33-
var scnNames = new string[] { "Original", "SciFi", "Fantasy" };
34-
if (fileData[10] > 44)
35-
{
36-
extendedMetadata.Add("TOT-Scenario", scnNames[fileData[982]]);
37-
}
17+
return LoadFromInternal(path, mainApp);
3818
}
3919
else
4020
{
41-
// We're in new territory...
42-
jsonDocument = JsonDocument.Parse(fileData);
43-
44-
var metaData = jsonDocument.RootElement.GetProperty("extendedMetadata");
45-
foreach (var meta in metaData.EnumerateObject())
46-
{
47-
extendedMetadata[meta.Name] = meta.Value.GetString() ?? string.Empty;
48-
}
21+
//TODO: Keeping this here so we can handle and show this as an error in the UI.
22+
throw new FileNotFoundException($"File {path} not found. Check the filename and try again.");
4923
}
24+
}
5025

26+
private static IInterfaceAction LoadFromInternal(string path, IMain mainApp)
27+
{
28+
var fileData = File.ReadAllBytes(path);
29+
bool classicSave = fileData[0] == 67; // Classic saves start with the word CIVILIZE so if we see a C treat it as old
30+
31+
var extendedMetadata = new Dictionary<string, string>();
32+
33+
var savDirectory = Path.GetDirectoryName(path);
34+
var root = Settings.SearchPaths.FirstOrDefault(savDirectory.StartsWith) ?? Settings.SearchPaths[0];
5135
var activeInterface = mainApp.SetActiveRulesetFromFile(root, savDirectory, extendedMetadata);
52-
var rules = RulesParser.ParseRules(activeInterface.MainApp.ActiveRuleSet);
36+
var activeRuleSet = activeInterface.MainApp.ActiveRuleSet;
37+
var rules = RulesParser.ParseRules(activeRuleSet);
5338

54-
var viewData = new Dictionary<string, string?>();
55-
IGame game;
5639
if (classicSave)
5740
{
58-
game = Read.ClassicSav(fileData, activeInterface.MainApp.ActiveRuleSet, rules, viewData);
59-
60-
if (string.Equals(Path.GetExtension(path), ".scn", StringComparison.OrdinalIgnoreCase))
61-
{
62-
var scnName = Path.GetFileName(path);
63-
return activeInterface.HandleLoadScenario(game, scnName, savDirectory);
64-
}
41+
var classicSavFile = new ClassicSavFile();
42+
var gameLoader = new GameLoader(path, savDirectory, rules, activeRuleSet, classicSavFile);
43+
var game = classicSavFile.LoadGame(fileData, activeRuleSet, rules);
44+
return gameLoader.LoadGame(game, activeInterface);
6545
}
6646
else
6747
{
68-
69-
if (jsonDocument.RootElement.TryGetProperty("viewData", out var viewDataElement))
70-
{
71-
foreach (var prop in viewDataElement.EnumerateObject())
72-
{
73-
viewData[prop.Name] = prop.Value.GetString();
74-
}
75-
}
76-
game = GameSerializer.Read(jsonDocument.RootElement.GetProperty("game"), activeInterface.MainApp.ActiveRuleSet, rules);
48+
// We're in new territory...
49+
var jsonSavFile = new JsonSavFile();
50+
var gameLoader = new GameLoader(path, savDirectory, rules, activeRuleSet, jsonSavFile);
51+
var game = jsonSavFile.LoadGame(fileData, activeRuleSet, rules);
52+
return gameLoader.LoadGame(game, activeInterface);
7753
}
78-
79-
return activeInterface.HandleLoadGame(game, rules, activeInterface.MainApp.ActiveRuleSet, viewData);
8054
}
8155
}
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json;
3+
using Civ2engine.IO;
4+
using Civ2engine.OriginalSaves;
5+
using Model.Core;
6+
7+
namespace Civ2engine.SaveLoad.SavFile;
8+
9+
public interface ISavFile
10+
{
11+
public IGame LoadGame(byte[] fileData, Ruleset activeRuleSet, Rules rules);
12+
}
13+
14+
public abstract class SavFileBase : ISavFile
15+
{
16+
protected Dictionary<string, string> viewData = new Dictionary<string, string>();
17+
public Dictionary<string, string> ViewData => viewData;
18+
19+
protected Dictionary<string, string> extendedMetadata = new Dictionary<string, string>();
20+
public abstract IGame LoadGame(byte[] fileData, Ruleset activeRuleSet, Rules rules);
21+
}
22+
23+
public class ClassicSavFile : SavFileBase
24+
{
25+
public override IGame LoadGame(byte[] fileData, Ruleset activeRuleSet, Rules rules)
26+
{
27+
var scnNames = new string[] { "Original", "SciFi", "Fantasy" };
28+
if (fileData[10] > 44)
29+
{
30+
extendedMetadata.Add("TOT-Scenario", scnNames[fileData[982]]);
31+
}
32+
33+
return Read.ClassicSav(fileData, activeRuleSet, rules, viewData);
34+
}
35+
}
36+
37+
public class JsonSavFile : SavFileBase
38+
{
39+
public override IGame LoadGame(byte[] fileData, Ruleset activeRuleSet, Rules rules)
40+
{
41+
var jsonDocument = JsonDocument.Parse(fileData);
42+
43+
var metaData = jsonDocument.RootElement.GetProperty("extendedMetadata");
44+
foreach (var meta in metaData.EnumerateObject())
45+
{
46+
extendedMetadata[meta.Name] = meta.Value.GetString() ?? string.Empty;
47+
}
48+
49+
if (jsonDocument.RootElement.TryGetProperty("viewData", out var viewDataElement))
50+
{
51+
foreach (var prop in viewDataElement.EnumerateObject())
52+
{
53+
viewData[prop.Name] = prop.Value.GetString();
54+
}
55+
}
56+
return GameSerializer.Read(jsonDocument.RootElement.GetProperty("game"), activeRuleSet, rules);
57+
}
58+
}

Engine/src/Settings.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Runtime.InteropServices;
55
using System.Text;
66
using System.Text.Json;
7-
using static System.Environment.SpecialFolder;
87

98
namespace Civ2engine
109
{
@@ -21,7 +20,7 @@ public class Settings
2120
// Game settings from App.config
2221
public static string Civ2Path { get; private set; }
2322

24-
public static string[] SearchPaths { get; private set; }
23+
public static string[] SearchPaths { get; internal set; }
2524

2625
public static int TextureFilter { get; private set; }
2726

@@ -126,7 +125,7 @@ public static bool AddPath(string path)
126125
{
127126
SearchPaths = SearchPaths.Append(path).ToArray();
128127
}
129-
Save();
128+
Save();// This overwrites the appsettings.
130129
return true;
131130
}
132131

Tests/Engine.Tests/Engine.Tests.csproj

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@
77
<IsPackable>false</IsPackable>
88
</PropertyGroup>
99

10+
<ItemGroup>
11+
<None Remove="TestFiles\Labels.txt" />
12+
<None Remove="TestFiles\RULES.TXT" />
13+
<None Remove="TestFiles\test_classic.sav" />
14+
<None Remove="TestFiles\test_json.sav" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<EmbeddedResource Include="TestFiles\Labels.txt">
19+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
20+
</EmbeddedResource>
21+
<EmbeddedResource Include="TestFiles\RULES.TXT">
22+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
23+
</EmbeddedResource>
24+
<EmbeddedResource Include="TestFiles\test_classic.sav">
25+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
26+
</EmbeddedResource>
27+
<EmbeddedResource Include="TestFiles\test_json.sav">
28+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
29+
</EmbeddedResource>
30+
</ItemGroup>
31+
1032
<ItemGroup>
1133
<PackageReference Include="coverlet.collector" Version="6.0.2" />
1234
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />

Tests/Engine.Tests/GameLoaderTests.cs

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using Civ2engine;
2+
using Civ2engine.Enums;
3+
using Civ2engine.Events;
4+
using Civ2engine.IO;
5+
using Civ2engine.MapObjects;
6+
using Civ2engine.SaveLoad;
7+
using Civ2engine.SaveLoad.SavFile;
8+
using Civ2engine.Units;
9+
using Engine.Tests.TestFiles;
10+
using Model;
11+
using Model.Core;
12+
using Model.InterfaceActions;
13+
14+
namespace Engine.Tests;
15+
16+
public class GameLoaderTests
17+
{
18+
[Fact]
19+
public void TestLoadGameHandlesScenarioFile()
20+
{
21+
// Arrange
22+
var path = TestFileUtils.GetTestFilePath("test_scenario.scn");
23+
var savDirectory = TestFileUtils.GetTestFileDirectory();
24+
var rules = new Rules();
25+
var activeRuleSet = new Ruleset(
26+
"mock",
27+
new Dictionary<string, string>(),
28+
[savDirectory]);
29+
var savFile = new MockSavFile();
30+
var gameLoader = new GameLoader(path, savDirectory, rules, activeRuleSet, savFile);
31+
var game = new MockGame();
32+
var activeInterface = new MockInterface();
33+
34+
// Act
35+
var result = gameLoader.LoadGame(game, activeInterface);
36+
37+
// Assert
38+
Assert.NotNull(result);
39+
Assert.IsAssignableFrom<IInterfaceAction>(result);
40+
}
41+
42+
[Fact]
43+
public void TestLoadGameHandlesNormalFile()
44+
{
45+
// Arrange
46+
var path = TestFileUtils.GetTestFilePath("test_game.sav");
47+
var savDirectory = TestFileUtils.GetTestFileDirectory();
48+
var rules = new Rules();
49+
var activeRuleSet = new Ruleset(
50+
"mock",
51+
new Dictionary<string, string>(),
52+
[savDirectory]);
53+
var savFile = new MockSavFile();
54+
var gameLoader = new GameLoader(path, savDirectory, rules, activeRuleSet, savFile);
55+
var game = new MockGame();
56+
var activeInterface = new MockInterface();
57+
58+
// Act
59+
var result = gameLoader.LoadGame(game, activeInterface);
60+
61+
// Assert
62+
Assert.NotNull(result);
63+
Assert.IsAssignableFrom<IInterfaceAction>(result);
64+
}
65+
}
66+
67+
internal class MockSavFile : SavFileBase
68+
{
69+
public override IGame LoadGame(byte[] fileData, Ruleset activeRuleSet, Rules rules)
70+
{
71+
return new MockGame();
72+
}
73+
}
74+
75+
internal class MockGame : IGame
76+
{
77+
public FastRandom Random => throw new NotImplementedException();
78+
public Civilization GetPlayerCiv => throw new NotImplementedException();
79+
public IDictionary<int, TerrainImprovement> TerrainImprovements => throw new NotImplementedException();
80+
public Rules Rules => throw new NotImplementedException();
81+
public Civilization GetActiveCiv => throw new NotImplementedException();
82+
public Options Options => throw new NotImplementedException();
83+
public Scenario ScenarioData => throw new NotImplementedException();
84+
public IPlayer ActivePlayer => throw new NotImplementedException();
85+
public IScriptEngine Script => throw new NotImplementedException();
86+
public IList<Map> Maps => throw new NotImplementedException();
87+
public IHistory History => throw new NotImplementedException();
88+
public Dictionary<string, List<string>?> CityNames => throw new NotImplementedException();
89+
90+
public event EventHandler<PlayerEventArgs> OnPlayerEvent;
91+
public event EventHandler<UnitEventArgs> OnUnitEvent;
92+
93+
public void ConnectPlayer(IPlayer player) => throw new NotImplementedException();
94+
public string Order2String(int unitOrder) => throw new NotImplementedException();
95+
public void ChooseNextUnit() => throw new NotImplementedException();
96+
public bool ProcessEndOfTurn() => throw new NotImplementedException();
97+
public void ChoseNextCiv() => throw new NotImplementedException();
98+
public void TriggerMapEvent(MapEventType updateMap, List<Tile> tiles) => throw new NotImplementedException();
99+
public double MaxDistance => throw new NotImplementedException();
100+
public int DifficultyLevel { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
101+
public IGameDate Date => throw new NotImplementedException();
102+
public int TurnNumber => throw new NotImplementedException();
103+
public List<City> AllCities => throw new NotImplementedException();
104+
public IPlayer[] Players => throw new NotImplementedException();
105+
public int PollutionSkulls => throw new NotImplementedException();
106+
public int GlobalTempRiseOccured => throw new NotImplementedException();
107+
public int NoOfTurnsOfPeace => throw new NotImplementedException();
108+
public int BarbarianActivity => throw new NotImplementedException();
109+
public int NoMaps => throw new NotImplementedException();
110+
public List<Civilization> AllCivilizations => throw new NotImplementedException();
111+
public void TriggerUnitEvent(UnitEventType eventType, IUnit triggerUnit, BlockedReason reason = BlockedReason.NotBlocked) => throw new NotImplementedException();
112+
public void TriggerUnitEvent(UnitEventArgs combatEventArgs) => throw new NotImplementedException();
113+
public void SetHumanPlayer(int playerCivId) => throw new NotImplementedException();
114+
public void StartPlayerTurn(IPlayer activePlayer) => throw new NotImplementedException();
115+
public void StartNextTurn() => throw new NotImplementedException();
116+
}

0 commit comments

Comments
 (0)