Skip to content

Commit 89697bc

Browse files
authored
Merge pull request #474 from cortex-command-community/cf/468-add-proper-semantic-version-comparison-to-check-mod-compatibility
Add proper semantic version comparison to check mod compatibility
2 parents bc4555b + a6790d1 commit 89697bc

17 files changed

+1046
-22
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1010
</details>
1111

1212
<details><summary><b>Changed</b></summary>
13+
14+
- Unofficial modules (mods) now use [Semantic Versioning](https://semver.org/) to check which version of the game they target.
15+
As such, the `Index.ini` property `SupportedGameVersion` must now be a valid semantic version number. The game version has also been updated to match this standard.
16+
17+
The `SupportedGameVersion` version number must be of the form `X.Y.z`, where:
18+
19+
`X` matches the major version of the game,
20+
`Y` is the minimum minor version of the game the mod requires,
21+
`z` is the patch number, which is currently not enforced.
22+
23+
Mods published for any development builds must match that development version exactly.
24+
1325
</details>
1426

1527
<details><summary><b>Fixed</b></summary>

Menus/MainMenuGUI.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include "SettingsMan.h"
88
#include "ConsoleMan.h"
99

10+
#include "GameVersion.h"
11+
1012
#include "GUI.h"
1113
#include "AllegroScreen.h"
1214
#include "GUIInputWrapper.h"
@@ -114,7 +116,7 @@ namespace RTE {
114116
}
115117

116118
m_VersionLabel = dynamic_cast<GUILabel *>(m_MainMenuScreenGUIControlManager->GetControl("VersionLabel"));
117-
m_VersionLabel->SetText("Community Project\n" + std::string(c_MajorGameVersion) + std::string(c_MinorGameVersion));
119+
m_VersionLabel->SetText("Community Project\nv" + c_GameVersion.str());
118120
m_VersionLabel->SetPositionAbs(10, g_WindowMan.GetResY() - m_VersionLabel->GetTextHeight() - 5);
119121
}
120122

RTEA.vcxproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@
621621
<ClInclude Include="System\Constants.h" />
622622
<ClInclude Include="System\Controller.h" />
623623
<ClInclude Include="System\Entity.h" />
624+
<ClInclude Include="System\GameVersion.h" />
624625
<ClInclude Include="System\GenericSavedData.h" />
625626
<ClInclude Include="System\InputMapping.h" />
626627
<ClInclude Include="System\InputScheme.h" />
@@ -629,6 +630,8 @@
629630
<ClInclude Include="System\Gamepad.h" />
630631
<ClInclude Include="Menus\InventoryMenuGUI.h" />
631632
<ClInclude Include="System\PieQuadrant.h" />
633+
<ClInclude Include="System\Semver200\semver200.h" />
634+
<ClInclude Include="System\Semver200\version.h" />
632635
<ClInclude Include="System\SpatialPartitionGrid.h" />
633636
<ClInclude Include="System\StandardIncludes.h" />
634637
<ClInclude Include="System\Box.h" />
@@ -854,6 +857,9 @@
854857
<ClCompile Include="System\InputScheme.cpp" />
855858
<ClCompile Include="System\GraphicalPrimitive.cpp" />
856859
<ClCompile Include="System\PieQuadrant.cpp" />
860+
<ClCompile Include="System\Semver200\Semver200_comparator.cpp" />
861+
<ClCompile Include="System\Semver200\Semver200_modifier.cpp" />
862+
<ClCompile Include="System\Semver200\Semver200_parser.cpp" />
857863
<ClCompile Include="System\Serializable.cpp" />
858864
<ClCompile Include="System\SpatialPartitionGrid.cpp" />
859865
<ClCompile Include="System\StandardIncludes.cpp">
@@ -987,6 +993,7 @@
987993
</ItemGroup>
988994
<ItemGroup>
989995
<None Include="cpp.hint" />
996+
<None Include="System\Semver200\version.inl" />
990997
</ItemGroup>
991998
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
992999
<ImportGroup Label="ExtensionTargets">

RTEA.vcxproj.filters

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<Filter Include="Lua">
2929
<UniqueIdentifier>{ac503e37-087b-40b8-b1b8-427ab55a4f9e}</UniqueIdentifier>
3030
</Filter>
31+
<Filter Include="System\Semver200">
32+
<UniqueIdentifier>{60f9b310-81f7-4bf6-a829-c893dc59537a}</UniqueIdentifier>
33+
</Filter>
3134
<Filter Include="GUI\Wrappers">
3235
<UniqueIdentifier>{14ff3fdb-4366-4bed-8b6e-ddcf72987b81}</UniqueIdentifier>
3336
</Filter>
@@ -567,6 +570,15 @@
567570
<ClInclude Include="Resources\Credits.h">
568571
<Filter>Resources</Filter>
569572
</ClInclude>
573+
<ClInclude Include="System\Semver200\semver200.h">
574+
<Filter>System\Semver200</Filter>
575+
</ClInclude>
576+
<ClInclude Include="System\Semver200\version.h">
577+
<Filter>System\Semver200</Filter>
578+
</ClInclude>
579+
<ClInclude Include="System\GameVersion.h">
580+
<Filter>System</Filter>
581+
</ClInclude>
570582
</ItemGroup>
571583
<ItemGroup>
572584
<ClCompile Include="System\Box.cpp">
@@ -1098,6 +1110,15 @@
10981110
<ClCompile Include="Managers\WindowMan.cpp">
10991111
<Filter>Managers</Filter>
11001112
</ClCompile>
1113+
<ClCompile Include="System\Semver200\Semver200_comparator.cpp">
1114+
<Filter>System\Semver200</Filter>
1115+
</ClCompile>
1116+
<ClCompile Include="System\Semver200\Semver200_modifier.cpp">
1117+
<Filter>System\Semver200</Filter>
1118+
</ClCompile>
1119+
<ClCompile Include="System\Semver200\Semver200_parser.cpp">
1120+
<Filter>System\Semver200</Filter>
1121+
</ClCompile>
11011122
</ItemGroup>
11021123
<ItemGroup>
11031124
<Image Include="Resources\ccicon.ico">
@@ -1111,5 +1132,8 @@
11111132
</ItemGroup>
11121133
<ItemGroup>
11131134
<None Include="cpp.hint" />
1135+
<None Include="System\Semver200\version.inl">
1136+
<Filter>System\Semver200</Filter>
1137+
</None>
11141138
</ItemGroup>
11151139
</Project>

System/Constants.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ namespace RTE {
99
typedef int MID; //!< Distinctive type definition for Material IDs.
1010
#pragma endregion
1111

12-
#pragma region Game Version
13-
static constexpr const char *c_MajorGameVersion = "Pre-Release 5";
14-
static constexpr const char *c_MinorGameVersion = ".1";
15-
#pragma endregion
16-
1712
#pragma region Userdata Constants
1813
static const std::string c_UserScenesModuleName = "UserScenes.rte"; //!< Module name where user created Scenes are saved.
1914
static const std::string c_UserScriptedSavesModuleName = "UserSavedGames.rte"; //!< Module name where user scripted Activity saves are saved.

System/DataModule.cpp

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#include "PresetMan.h"
33
#include "SceneMan.h"
44
#include "LuaMan.h"
5+
#include "GameVersion.h"
6+
7+
#include <System/Semver200/semver200.h>
58

69
namespace RTE {
710

@@ -15,7 +18,7 @@ namespace RTE {
1518
m_FriendlyName.clear();
1619
m_Author.clear();
1720
m_Description.clear();
18-
m_SupportedGameVersion.clear();
21+
m_SupportedGameVersion = nullptr;
1922
m_Version = 1;
2023
m_ModuleID = -1;
2124
m_IconFile.Reset();
@@ -49,18 +52,14 @@ namespace RTE {
4952
// NOTE: This looks for the MergedIndex.ini generated by the index merger tool. The tool is mostly superseded by disabling loading visuals, but still provides some benefit.
5053
if (std::filesystem::exists(mergedIndexPath)) { indexPath = mergedIndexPath; }
5154

55+
// If the module is a mod, read only its `index.ini` to validate its SupportedGameVersion.
56+
if (m_ModuleID >= g_PresetMan.GetOfficialModuleCount() && !m_IsUserdata && ReadModuleProperties(moduleName, progressCallback) >= 0) {
57+
CheckSupportedGameVersion();
58+
}
59+
5260
if (reader.Create(indexPath, true, progressCallback) >= 0) {
5361
int result = Serializable::Create(reader);
5462

55-
size_t lastPeriodPosition = m_SupportedGameVersion.find_last_of(".");
56-
std::string supportedMajorGameVersion = m_SupportedGameVersion.substr(0, lastPeriodPosition);
57-
std::string supportedMinorGameVersion = lastPeriodPosition == std::string::npos ? ".0" : m_SupportedGameVersion.substr(lastPeriodPosition, m_SupportedGameVersion.length());
58-
if (m_ModuleID >= g_PresetMan.GetOfficialModuleCount() && !m_IsUserdata && (supportedMajorGameVersion != c_MajorGameVersion || supportedMinorGameVersion > c_MinorGameVersion)) {
59-
RTEAssert(!m_SupportedGameVersion.empty(), m_FileName + " does not specify a supported Cortex Command version, so it is not compatible with this version of Cortex Command (" + c_MajorGameVersion + c_MinorGameVersion + ").\nPlease contact the mod author or ask for help in the CCCP discord server.");
60-
RTEAssert(supportedMinorGameVersion <= c_MinorGameVersion, m_FileName + " supports Cortex Command version " + m_SupportedGameVersion + ", which is ahead of this version of Cortex Command (" + c_MajorGameVersion + c_MinorGameVersion + ").\nPlease update your game to the newest version to use this mod.");
61-
RTEAbort(m_FileName + " supports Cortex Command version " + m_SupportedGameVersion + ", so it is not compatible with this version of Cortex Command (" + c_MajorGameVersion + c_MinorGameVersion + ").\nPlease contact the mod author or ask for help in the CCCP discord server.");
62-
}
63-
6463
// Print an empty line to separate the end of a module from the beginning of the next one in the loading progress log.
6564
if (progressCallback) { progressCallback(" ", true); }
6665

@@ -95,6 +94,7 @@ namespace RTE {
9594
for (const PresetEntry &preset : m_PresetList){
9695
delete preset.m_EntityPreset;
9796
}
97+
delete m_SupportedGameVersion;
9898
Clear();
9999
}
100100

@@ -145,7 +145,16 @@ namespace RTE {
145145
reader >> m_IsMerchant;
146146
if (m_IsMerchant) { m_IsFaction = false; }
147147
} else if (propName == "SupportedGameVersion") {
148-
reader >> m_SupportedGameVersion;
148+
std::string versionText;
149+
reader >> versionText;
150+
// TODO: Need to proceed reading the includes after ReadModuleProperties so we don't read the properties again when fully creating.
151+
if (!m_SupportedGameVersion) {
152+
try {
153+
m_SupportedGameVersion = new version::Semver200_version(versionText);
154+
} catch (version::Parse_error &) {
155+
reader.ReportError("Couldn't parse the supported game version from the value provided: \"" + versionText + "\"!\nThe supported game version must be a valid semantic version number.\n");
156+
}
157+
}
149158
} else if (propName == "Version") {
150159
reader >> m_Version;
151160
} else if (propName == "ScanFolderContents") {
@@ -204,7 +213,7 @@ namespace RTE {
204213
writer.NewPropertyWithValue("Author", m_Author);
205214
writer.NewPropertyWithValue("Description", m_Description);
206215
writer.NewPropertyWithValue("IsFaction", m_IsFaction);
207-
writer.NewPropertyWithValue("SupportedGameVersion", m_SupportedGameVersion);
216+
writer.NewPropertyWithValue("SupportedGameVersion", m_SupportedGameVersion->str());
208217
writer.NewPropertyWithValue("Version", m_Version);
209218
writer.NewPropertyWithValue("IconFile", m_IconFile);
210219

@@ -481,4 +490,29 @@ namespace RTE {
481490
}
482491
return true;
483492
}
493+
494+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
495+
496+
void DataModule::CheckSupportedGameVersion() const {
497+
if (*m_SupportedGameVersion != c_GameVersion) {
498+
static const std::string contactAuthor = "Please contact the mod author or ask for help in the CCCP discord server.";
499+
500+
RTEAssert(*m_SupportedGameVersion != version::Semver200_version(), m_FileName + " does not specify a supported Cortex Command version, so it is not compatible with this version of Cortex Command (" + c_GameVersion.str() + ").\n\n" + contactAuthor);
501+
502+
bool modulePrereleaseVersionMismatch = !m_SupportedGameVersion->prerelease().empty();
503+
bool moduleBuildVersionMismatch = !m_SupportedGameVersion->build().empty();
504+
RTEAssert(!modulePrereleaseVersionMismatch && !moduleBuildVersionMismatch, m_FileName + " was developed for pre-release build of Cortex Command v" + m_SupportedGameVersion->str() + ", this game version (v" + c_GameVersion.str() + ") is incompatible.\n\nMods developed on a pre-release must match the game version exactly.\n" + contactAuthor);
505+
506+
bool gamePrereleaseVersionMismatch = !c_GameVersion.prerelease().empty();
507+
bool gameBuildVersionMismatch = !c_GameVersion.build().empty();
508+
RTEAssert(!gamePrereleaseVersionMismatch && !gameBuildVersionMismatch, m_FileName + " was developed for Cortex Command v" + m_SupportedGameVersion->str() + ", this pre-release version of the game (v" + c_GameVersion.str() + ") may not support it.\n\nMods must match the game version exactly to use pre-release builds.\n" + contactAuthor);
509+
510+
// Game engine is the same major version as the Module
511+
bool majorVersionMatch = c_GameVersion.major() == m_SupportedGameVersion->major();
512+
// Game engine is at least the minor version the Module requires (allow patch mismatch)
513+
bool minorVersionInRange = m_SupportedGameVersion->inc_minor() <= c_GameVersion.inc_minor();
514+
515+
RTEAssert(majorVersionMatch && minorVersionInRange, m_FileName + " was developed for Cortex Command v" + m_SupportedGameVersion->str() + ", so this version of Cortex Command (v" + c_GameVersion.str() + ") may not support it.\n" + contactAuthor);
516+
}
517+
}
484518
}

System/DataModule.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
//struct DATAFILE; // DataFile loading not implemented.
88
struct BITMAP;
99

10+
namespace version {
11+
class Semver200_version;
12+
}
13+
1014
namespace RTE {
1115

1216
class Entity;
@@ -320,7 +324,7 @@ namespace RTE {
320324
std::string m_ScriptPath; //!< Path to script to execute when this module is loaded.
321325
bool m_IsFaction; //!< Whether this data module is considered a faction.
322326
bool m_IsMerchant; //!< Whether this data module is considered a merchant.
323-
std::string m_SupportedGameVersion; //!< Game version this DataModule supports. Needs to match exactly for this DataModule to be allowed. Base DataModules don't need this.
327+
version::Semver200_version *m_SupportedGameVersion; //!< Game version this DataModule supports. Needs to satisfy Caret Version Range for this DataModule to be allowed. Base DataModules don't need this.
324328
int m_Version; //!< Version number, starting with 1.
325329
int m_ModuleID; //!< ID number assigned to this upon loading, for internal use only, don't reflect in ini's.
326330

@@ -355,6 +359,11 @@ namespace RTE {
355359
static const std::string c_ClassName; //!< A string with the friendly-formatted type name of this object.
356360

357361
#pragma region INI Handling
362+
/// <summary>
363+
/// Checks the module's supported game version against the current game version to ensure compatibility.
364+
/// </summary>
365+
void CheckSupportedGameVersion() const;
366+
358367
/// <summary>
359368
/// If ScanFolderContents is enabled in this DataModule's Index.ini, looks for any ini files in the top-level directory of the module and reads all of them in alphabetical order.
360369
/// </summary>

System/GameVersion.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#ifndef _RTEGAMEVERSION_
2+
#define _RTEGAMEVERSION_
3+
4+
#include "System/Semver200/semver200.h"
5+
6+
namespace RTE {
7+
8+
#pragma region Game Version
9+
static constexpr const char *c_VersionString = "5.1.0";
10+
static const version::Semver200_version c_GameVersion = version::Semver200_version(c_VersionString);
11+
#pragma endregion
12+
}
13+
#endif

System/Reader.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ namespace RTE {
4949

5050
m_CanFail = failOK;
5151

52-
m_Stream = std::make_unique<std::ifstream>(fileName);
53-
if (!m_CanFail) { RTEAssert(System::PathExistsCaseSensitive(fileName) && m_Stream->good(), "Failed to open data file \"" + m_FilePath + "\"!"); }
52+
m_Stream = std::make_unique<std::ifstream>(m_FilePath);
53+
if (!m_CanFail) { RTEAssert(System::PathExistsCaseSensitive(m_FilePath) && m_Stream->good(), "Failed to open data file \"" + m_FilePath + "\"!"); }
5454

5555
m_OverwriteExisting = overwrites;
5656

System/Reader.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ namespace RTE {
9191
/// <summary>
9292
/// Set whether this reader should skip included files.
9393
/// </summary>
94-
/// <param name="skip>To make reader skip included files pass true, pass false otherwise.</param>
94+
/// <param name="skip">To make reader skip included files pass true, pass false otherwise.</param>
9595
void SetSkipIncludes(bool skip) { m_SkipIncludes = skip; };
9696
#pragma endregion
9797

0 commit comments

Comments
 (0)